import {
  mountSubCategorySelectFromAttributes,
  type SubCategorySelectFieldController,
  type SubCategorySelectFieldOptions,
} from '@features/sub-category/mount-sub-category-select';
import {
  upgradeProperty,
  resetMountMarker,
  readAttributesAsDataset,
  readForwardedHostAttributes,
} from './element-helpers';

type StaticOptions = Omit<SubCategorySelectFieldOptions, 'defaultValue' | 'isDisabled'>;

const ATTRIBUTE_NAMES = [
  'target',
  'default-value',
  'default-label',
  'category-id',
  'input-id',
  'is-disabled',
  'form',
  'name',
  'menu-portal-target',
  'limit',
];

/**
 * host が持つ、実際にレンダリングされる SubCategorySelect のルート要素へ転送する独自属性。
 * host は display: contents（register.ts 参照）で自身のボックスを持たないため、host に標準の
 * class を当てても見た目には反映されない。テンプレート側は最初から `root-class` を独自属性として
 * 書く（詳細は README.md「共通の設計」参照）。
 */
const FORWARDED_HOST_ATTRIBUTES = ['root-class'] as const;

/**
 * `<acms-sub-category-select>` カスタム要素。
 *
 * `target`（CSS セレクタ）が指す外部の hidden input（フォーム送信の実体、テンプレート側が
 * 別途レンダリングする）へ選択値を同期する。要素自身は Light DOM に React（SubCategorySelect）だけを
 * マウントし、`.js-value` のような内部 hidden input は持たない。マウント処理そのものは
 * features/sub-category/mount-sub-category-select.tsx の mountSubCategorySelectFromAttributes を
 * 通じて行う。
 *
 * カテゴリー選択との連動は、`category-id` 属性（CSS セレクタ、既定値 `[name=category_id]`）が
 * 指す input の acms.category.change を購読することで行う。
 *
 * 外部 API:
 * - `value: string`（カンマ区切りのサブカテゴリー ID。target 先 input の値表現と同一）
 * - `disabled: boolean`（`disabled` 属性と同期）
 * - `change` イベント（CustomEvent, bubbles）: 内部の acms.sub-category.change を検知して発火する
 *
 * 属性は `admin/include/sub-category/select.html` が出力する独自属性（data-* ではない。カスタム
 * 要素自身の属性はその要素の公開 API であり HTML 標準要素向けの data-* 制約は適用されないため。
 * target / default-value / default-label / category-id / input-id / is-disabled / form / name /
 * menu-portal-target / limit）を connectedCallback 時に一度だけ読む。`data-id` のみ HTML 標準の
 * グローバル属性 `id` と衝突するため data-* のまま維持している。詳細は
 * ablogcms/js/src/elements/README.md を参照。
 */
export class SubCategorySelectElement extends HTMLElement {
  static get observedAttributes(): string[] {
    return ['disabled'];
  }

  private controller: SubCategorySelectFieldController | null = null;

  private targetInput: HTMLInputElement | null = null;

  /** defaultLabel 等と同じ「初期表示のみ」のオプション。defaultValue / isDisabled は毎回読み直す。 */
  private staticOptions: StaticOptions = {};

  connectedCallback(): void {
    if (this.controller) {
      return;
    }

    upgradeProperty(this, 'value');
    upgradeProperty(this, 'disabled');

    const attrs = {
      ...readAttributesAsDataset(this, ATTRIBUTE_NAMES),
      ...readForwardedHostAttributes(this, FORWARDED_HOST_ATTRIBUTES),
    };

    if (attrs.isDisabled === 'true' && !this.hasAttribute('disabled')) {
      this.setAttribute('disabled', '');
    }

    try {
      const { controller, input } = mountSubCategorySelectFromAttributes(this, attrs, this.ownerDocument ?? document);
      this.controller = controller;
      this.targetInput = input;
      this.staticOptions = {
        defaultLabel: attrs.defaultLabel,
        // data-id は HTML 標準のグローバル属性 id と衝突するため、他の属性と違い data-* のまま維持する。
        id: this.dataset.id,
        inputId: attrs.inputId,
        form: attrs.form,
        name: attrs.name,
        menuPortalTarget: attrs.menuPortalTarget
          ? document.querySelector<HTMLElement>(attrs.menuPortalTarget)
          : undefined,
        ...(attrs.limit !== undefined && { limit: Number(attrs.limit) }),
        ...readForwardedHostAttributes(this, FORWARDED_HOST_ATTRIBUTES),
      };

      input.addEventListener('acms.sub-category.change', () => {
        this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
      });
    } catch (error) {
      // 単一要素の初期化失敗が管理画面全体を止めないよう、ここで握りつぶして報告する。

      console.error(error);
    }
  }

  disconnectedCallback(): void {
    const controller = this.controller;
    // Why not: この要素がカスタムユニットの中に埋め込まれる等で物理的に DOM 移動されると、
    // disconnectedCallback → connectedCallback が同期的に再発火する。this.controller を
    // 同期的に null にすると、再発火した connectedCallback のガードが効かず、まだ unmount
    // されていない旧 root が残ったまま同じ host（React はここへ直接レンダリングしている）に
    // 新しい root を二重に作ってしまう。後片付け全体をマイクロタスクへ遅らせ、その時点で
    // this.isConnected を見て「本当に取り外されたのか、移動で戻ってきたのか」を判定する
    // （詳細は elements/block-editor-element.ts の同名コメント参照）。
    queueMicrotask(() => {
      if (this.isConnected) {
        return;
      }
      controller?.unmount();
      this.controller = null;
      this.targetInput = null;
      resetMountMarker(this);
    });
  }

  attributeChangedCallback(name: string): void {
    if (name === 'disabled') {
      this.rerender();
    }
  }

  /**
   * defaultValue は targetInput.value から毎回読み直す。ここを options に固定値として
   * キャッシュしてしまうと、disabled 切り替え等の再レンダリングでユーザーの選択済みの値が
   * 初期値に巻き戻ってしまうため（Suspense の key を変えて丸ごと再マウントする実装のため）。
   * defaultLabel は初期表示専用のため targetInput からは読み直さない（ID と付け替える術がない）。
   */
  private rerender(): void {
    if (!this.controller) {
      return;
    }
    this.controller.update({
      ...this.staticOptions,
      isDisabled: this.disabled,
      defaultValue: this.targetInput?.value ?? '',
    });
  }

  get value(): string {
    return this.targetInput?.value ?? this.getAttribute('default-value') ?? '';
  }

  set value(newValue: string) {
    if (!this.targetInput) {
      // 接続前の代入は upgradeProperty が connectedCallback で拾い、default-value 相当として使う。
      this.setAttribute('default-value', newValue);
      return;
    }
    this.targetInput.value = newValue;
    this.rerender();
  }

  get disabled(): boolean {
    return this.hasAttribute('disabled');
  }

  set disabled(isDisabled: boolean) {
    if (isDisabled) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }
}
