import type { BlockDefinition } from '@ablogcms/block-editor/types';
import { readForwardedHostAttributes } from './element-helpers';

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

async function loadAllBlockDefinitions(): Promise<Record<string, BlockDefinition>> {
  const [{ tiptap }, { getBlockDefinitions }, { mergeBlockDefinitions }] = await Promise.all([
    import(/* webpackChunkName: "block-editor" */ '@ablogcms/block-editor/tiptap'),
    import(/* webpackChunkName: "block-editor" */ '../features/block-editor/block-definitions'),
    import(/* webpackChunkName: "block-editor" */ '@ablogcms/block-editor/merge-block-definitions'),
  ]);
  return mergeBlockDefinitions(getBlockDefinitions(tiptap));
}

/**
 * `menuCategory`（slug。プラグイン提供で未指定なら `PLUGIN_BLOCK_MENU_CATEGORY.slug`）ごとに
 * type をグループ化する。`Object.entries` の列挙順（コアは定義順、プラグインは登録順）を
 * そのまま optgroup 内の option 順・optgroup 自体の出現順に使う。
 */
function groupByMenuCategory(
  definitions: Record<string, BlockDefinition>,
  pluginCategorySlug: string
): Map<string, [string, BlockDefinition][]> {
  const groups = new Map<string, [string, BlockDefinition][]>();
  for (const [type, definition] of Object.entries(definitions)) {
    const categorySlug = definition.menuCategory ?? pluginCategorySlug;
    const entries = groups.get(categorySlug) ?? [];
    entries.push([type, definition]);
    groups.set(categorySlug, entries);
  }
  return groups;
}

async function buildSelect(definitions: Record<string, BlockDefinition>): Promise<HTMLSelectElement> {
  const { getBlockMenuCategoryLabel, PLUGIN_BLOCK_MENU_CATEGORY } = await import(
    /* webpackChunkName: "block-editor" */ '@ablogcms/block-editor/block-menu-categories'
  );

  // name 属性は付けない。実際にサーバーへ送信するのは同梱の hidden input で、
  // この select は編集用の UI（表示・選択操作）専用として切り離す。
  const select = document.createElement('select');

  groupByMenuCategory(definitions, PLUGIN_BLOCK_MENU_CATEGORY.slug).forEach((entries, categorySlug) => {
    const optgroup = document.createElement('optgroup');
    optgroup.label = getBlockMenuCategoryLabel(categorySlug);
    entries.forEach(([type, definition]) => {
      const option = document.createElement('option');
      option.value = type;
      option.textContent = definition.label;
      optgroup.append(option);
    });
    select.append(optgroup);
  });

  return select;
}

/**
 * `<acms-block-editor-menu-type-select>` カスタム要素。
 *
 * 編集設定（ブロックエディター設定画面）の「ブロック」type select を、コアの `BLOCK_DEFINITIONS`
 * とプラグイン提供の `registerBlockDefinition` から `<select>`/`<optgroup>`/`<option>` ごと
 * 動的にレンダリングする。以前はテンプレート側にコアの type 一覧・ラベルをベタ書きしており、
 * `config/blocks/{type}.ts`（コア側の一次情報）と二重管理になっていたため、この要素が唯一の
 * レンダリング元になるよう変更した。`<select>` 自体は Customized built-in element
 * （`<select is="...">`）にすると Safari が一切対応していないため、既存の `elements/` パターンと
 * 同じ Autonomous custom element で `<select>` を自身の中に生成するラッパーとして実装する。
 *
 * `menuCategory`（`BlockTypeMaster.menuCategory`。値は `config/blockMenuCategories.ts` の
 * `BLOCK_MENU_CATEGORIES` が定義する英語スラッグ。コアは basic/insert/layout、プラグイン提供で
 * 未指定なら `PLUGIN_BLOCK_MENU_CATEGORY`）ごとに `<optgroup>` へ振り分ける。optgroup の
 * ラベル（日本語）はスラッグから `getBlockMenuCategoryLabel()` で解決する。
 *
 * サーバー駆動の原則（HOOKS.md「レジストリ一覧」）により、どの type を実際にメニューへ出すかは
 * 引き続き管理者がこの画面で選び config として保存する。ここで行うのは「選べる選択肢を描画する」
 * ことだけで、保存経路（フォーム送信・`ACMS_GET_Admin_BlockEditor_SettingsJson` が返す config 値）
 * は一切変更しない（`getBlockMenuSetting()` はもともと未知の type もそのまま通す設計のため、
 * PHP 側の改修は不要）。
 *
 * ブロックエディター本体（Tiptap/PM、webpackChunkName: "block-editor"）を admin.js の初期バンドルに
 * 巻き込まないため、動的 import は `connectedCallback` 内でのみ行う。
 *
 * 複製行（`js-fieldgroup-sortable` によるユニット追加）でこの要素ごと複製されても、ブラウザ標準の
 * Custom Elements ライフサイクルが自動的に `connectedCallback` を呼ぶため、
 * `acmsAdminDelayedContents` の購読は不要（他のカスタム要素と同じ設計。詳細は README 参照）。
 *
 * `admin/config/block-editor.html` は、実際にサーバーへ送信する値をこの要素の子孫の
 * `<input type="hidden">`（`name`/`value` はテンプレートが出力する本物の form field）として
 * あらかじめ配置する。`js-fieldgroup-sortable`（`custom-field-maker`）が行追加・並べ替え時に
 * name 属性の連番（`foo[0]`, `foo[1]`, ...）を振り直す処理は `<input>` 要素だけを対象にしており、
 * この要素が動的に生成する `<select>` には効かない。そのため `<select>` 自身には name を持たせず
 * 編集用 UI に徹し、選択結果を change イベントで hidden input の value に反映する
 * （name 連番の対象はあくまで最初から DOM にある hidden input 側）。
 */
export class BlockEditorMenuTypeSelectElement extends HTMLElement {
  connectedCallback(): void {
    // jQuery UI sortable（行の並べ替え）は要素を実際に DOM 移動するため、並べ替えのたびに
    // disconnectedCallback → connectedCallback が再発火する。select は disconnectedCallback で
    // 削除しない（子ノードとして保持されたまま DOM 移動される）ため、既に select がある場合は
    // 並べ替えによる再接続とみなして再描画をスキップする。これにより、管理者が並べ替え前に
    // 選んだ未保存の選択（select.value）が hidden input の初期値へ巻き戻ることも防げる
    // （複製行は select を持たない新しいインスタンスなので、通常どおり構築・復元される）。
    if (this.querySelector('select')) {
      return;
    }

    const hiddenInput = this.querySelector<HTMLInputElement>('input[type="hidden"]');
    if (!hiddenInput) {
      console.error('[block-editor] acms-block-editor-menu-type-select に子孫の hidden input がありません。');
      return;
    }

    loadAllBlockDefinitions()
      .then(async (definitions) => {
        const select = await buildSelect(definitions);

        const forwarded = readForwardedHostAttributes(this, FORWARDED_HOST_ATTRIBUTES);
        if (forwarded.className) {
          select.className = forwarded.className;
        }

        if (hiddenInput.value) {
          select.value = hiddenInput.value;
        }
        // 保存値と一致する option が無く先頭 option が既定選択された場合も含め、
        // 実際に選択されている値を hidden input 側へ同期する。
        hiddenInput.value = select.value;

        select.addEventListener('change', () => {
          hiddenInput.value = select.value;
        });

        this.append(select);
      })
      .catch((err) => {
        console.error('[block-editor] 編集設定のブロック type 選択肢の読み込みに失敗しました。', err);
      });
  }
}
