import {
  mountCategorySelectFromAttributes,
  type CategorySelectFieldController,
  type CategorySelectFieldOptions,
} from '@features/category/mount-category-select';
import {
  upgradeProperty,
  resetMountMarker,
  readAttributesAsDataset,
  readForwardedHostAttributes,
} from './element-helpers';

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

const ATTRIBUTE_NAMES = [
  'target',
  'default-value',
  'narrow-down',
  'no-option',
  'is-creatable',
  'is-clearable',
  'mt-option',
  'input-id',
  'is-disabled',
  'form',
  'name',
  'menu-portal-target',
];

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

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

  private controller: CategorySelectFieldController | null = null;

  private targetInputs: HTMLInputElement[] = [];

  /** narrow-down 等と同じ「初期表示のみ」のオプション。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 result = mountCategorySelectFromAttributes(this, attrs, this.ownerDocument ?? document);
      if (!result) {
        return;
      }
      const { controller, inputs } = result;
      this.controller = controller;
      this.targetInputs = inputs;
      this.staticOptions = {
        narrowDown: attrs.narrowDown === 'true',
        isCreatable: attrs.isCreatable === 'true',
        isClearable: attrs.isClearable !== 'false',
        mtOption: attrs.mtOption === 'true',
        noOption: attrs.noOption === 'true',
        // 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,
        ...readForwardedHostAttributes(this, FORWARDED_HOST_ATTRIBUTES),
      };

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

      console.error(error);
    }
  }

  disconnectedCallback(): void {
    const controller = this.controller;
    // Why not: この要素はカスタムユニットの中に埋め込まれる等で、自身を内包する外側の
    // React ツリーによって物理的に DOM 移動されることがある。Custom Elements の仕様上、
    // 物理移動でも 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.targetInputs = [];
      resetMountMarker(this);
    });
  }

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

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

  get value(): string {
    return this.targetInputs[0]?.value ?? this.getAttribute('default-value') ?? '';
  }

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

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

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