import type { Editor } from '@tiptap/core';
import { mountBlockEditorField, type BlockEditorFieldController } from '@features/block-editor/mount-block-editor';
import { resetMountMarker, readForwardedHostAttributes } from './element-helpers';

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

/**
 * `<acms-block-editor>` カスタム要素。
 *
 * 自身に直接 BlockEditor（tiptap ベース）をマウントし、子孫の `<input type="hidden">` へ HTML を
 * 同期する（`<acms-unit-editor>` と同じく、`target`/`html` のようなマウント先セレクタ属性は持たず、
 * 自身が直接マウント先になる）。マウント処理そのものは
 * features/block-editor/mount-block-editor.tsx の mountBlockEditorField を通じて行う。
 *
 * 外部 API:
 * - `editor: Editor | null`（get のみ）— tiptap の Editor インスタンス。マウント完了前は null。
 * - `change` イベント（CustomEvent, bubbles）: 入力内容の変更（子孫 hidden input への同期完了後）を
 *   検知して発火する
 *
 * `.js-block-editor`（https://developer.a-blogcms.jp/document/block-editor/ で公開されている
 * サイト構築者向け仕様）は custom-field-maker（submodules/custom-field-maker、コード生成ツール）が
 * 生成する HTML を経由してサイト制作者のテーマ・DB 設定（本リポジトリの外側）に存在するため撤去できない。
 * この外部仕様は `data-target`/`data-html` + 専用のマウント先コンテナ div という旧構造のままだが、
 * `upgradeLegacyBlockEditors`（`elements/upgrade-legacy-marks.ts`）が見つけ次第この要素の構造
 * （マウント先セレクタ属性を持たず、hidden input 以外の子ノードを除去したもの）へ変換したうえで
 * この要素へ置換することで、初期化経路をこの要素 1 本に統一している。詳細は
 * ablogcms/js/src/elements/README.md を参照。
 */
export class BlockEditorElement extends HTMLElement {
  private controller: BlockEditorFieldController | null = null;

  private editorInstance: Editor | null = null;

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

    try {
      this.controller = mountBlockEditorField(this, {
        onEditorReady: (editor) => {
          this.editorInstance = editor;
        },
        onChange: () => {
          this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
        },
        ...readForwardedHostAttributes(this, FORWARDED_HOST_ATTRIBUTES),
      });
    } catch (error) {
      // 単一要素の初期化失敗が管理画面全体を止めないよう、ここで握りつぶして報告する。

      console.error(error);
    }
  }

  disconnectedCallback(): void {
    const controller = this.controller;
    // Why not: この要素はグループユニットの外に出す操作等で、自身を内包する外側の React ツリー
    // （features/unit-editor の ServerSideRenderer）によって物理的に DOM 移動されることがある。
    // Custom Elements の仕様上、物理移動でも disconnectedCallback → connectedCallback が同期的に
    // 再発火する。ここで this.controller を同期的に null にすると、再発火した connectedCallback の
    // 「既にマウント済みなら何もしない」ガードが効かなくなり、まだ unmount されていない旧 root が
    // 残ったまま同じ host へ新しい root を二重に作ってしまう。加えて root.unmount() 自体を
    // 同期実行すると、外側の commit が完了する前に内側の root を畳もうとし、React が
    // 「Attempted to synchronously unmount a root while React was already rendering」という
    // 警告を出す。両方を避けるため、後片付け全体をマイクロタスクへ遅らせ、その時点で
    // this.isConnected を見て「本当に取り外されたのか、移動で戻ってきたのか」を判定する。
    queueMicrotask(() => {
      if (this.isConnected) {
        // マイクロタスクが実行される前に再接続された＝物理的な移動だった。既存の root は
        // そのまま生きているので何もしない。
        return;
      }
      controller?.unmount();
      this.controller = null;
      this.editorInstance = null;
      resetMountMarker(this);
    });
  }

  get editor(): Editor | null {
    return this.editorInstance;
  }
}
