import BLOCK_DEFINITIONS from '../config/blocks';
import { buildIdentifier } from './createBlockMenu';
import type {
  BlockMenuEntry,
  BlockMenuItem,
  BlockMenuOverride,
  CommandItem,
  EditorSettings,
  EditorSettingsOverride,
} from '../types';

const isPlainObject = (value: unknown): value is Record<string, unknown> =>
  typeof value === 'object' && value !== null && !Array.isArray(value);

/** CommandItem の識別子。`identifier` が無ければ `name` (type 名) で代用。 */
const identifierOf = (cmd: CommandItem): string => cmd.identifier ?? cmd.name;

interface FlatCommand {
  group: string;
  command: CommandItem;
}

interface GroupMeta {
  name: string;
  title: string;
}

interface FlattenedBlockMenus {
  commands: Map<string, FlatCommand>;
  groupMetaByTitle: Map<string, GroupMeta>;
  baseGroupOrder: string[];
}

const FALLBACK_GROUP_TITLE = 'その他';

/**
 * BlockMenuItem[] (グループ入れ子) を識別子をキーにしたフラットマップに展開。
 * 同 type の class 違いも独立したエントリとして扱える。
 */
const flattenBlockMenus = (blockMenus: BlockMenuItem[]): FlattenedBlockMenus => {
  const commands = new Map<string, FlatCommand>();
  const groupMetaByTitle = new Map<string, GroupMeta>();
  const baseGroupOrder: string[] = [];

  for (const group of blockMenus) {
    if (!groupMetaByTitle.has(group.title)) {
      groupMetaByTitle.set(group.title, { name: group.name, title: group.title });
      baseGroupOrder.push(group.title);
    }
    for (const command of group.commands) {
      commands.set(identifierOf(command), { group: group.title, command });
    }
  }

  return { commands, groupMetaByTitle, baseGroupOrder };
};

/**
 * allow / deny で base からフィルタする。識別子で厳密マッチ。
 */
const applyAllowDenyFilter = (
  baseCommands: Map<string, FlatCommand>,
  override: BlockMenuOverride
): Map<string, FlatCommand> => {
  let working = new Map(baseCommands);

  if (Array.isArray(override.allow)) {
    const allowed = new Set(override.allow);
    working = new Map(Array.from(working).filter(([identifier]) => allowed.has(identifier)));
  }

  if (Array.isArray(override.deny)) {
    const denied = new Set(override.deny);
    for (const identifier of Array.from(working.keys())) {
      if (denied.has(identifier)) {
        working.delete(identifier);
      }
    }
  }

  return working;
};

const regroupCommands = (
  filtered: Map<string, FlatCommand>,
  groupMetaByTitle: Map<string, GroupMeta>,
  baseGroupOrder: string[]
): BlockMenuItem[] => {
  const groupCommands = new Map<string, CommandItem[]>();
  const appearanceOrder: string[] = [];

  for (const entry of filtered.values()) {
    if (!groupCommands.has(entry.group)) {
      groupCommands.set(entry.group, []);
      appearanceOrder.push(entry.group);
    }
    groupCommands.get(entry.group)!.push(entry.command);
  }

  const orderedTitles = [
    ...baseGroupOrder.filter((title) => groupCommands.has(title)),
    ...appearanceOrder.filter((title) => !baseGroupOrder.includes(title)),
  ];

  return orderedTitles.map((title) => {
    const meta = groupMetaByTitle.get(title) ?? { name: title, title };
    return {
      name: meta.name,
      title: meta.title,
      commands: groupCommands.get(title) ?? [],
    };
  });
};

/**
 * `blocks` (BlockMenuEntry[]) を base 無視で「完全置換」のメニューに組み立てる。
 * 各 entry の type は TS マスター (BLOCK_DEFINITIONS) に存在する必要があり、無ければ除外。
 * group は明示指定された値を使い、未指定 (or 空文字) なら直前 entry の group を引き継ぐ。
 * 同 identifier (type + class) が複数あれば後勝ち（最後の entry の位置・内容で上書き）。
 */
const buildFromBlocksOverride = (entries: BlockMenuEntry[]): BlockMenuItem[] => {
  const byIdentifier = new Map<string, { group: string; command: CommandItem }>();
  let currentGroup = FALLBACK_GROUP_TITLE;

  for (const entry of entries) {
    if (!isPlainObject(entry) || typeof entry.type !== 'string' || entry.type === '') {
      continue;
    }
    const master = BLOCK_DEFINITIONS[entry.type];
    if (!master) {
      continue;
    }

    if (typeof entry.group === 'string' && entry.group !== '' && entry.group !== currentGroup) {
      currentGroup = entry.group;
    }

    const className = entry.class || undefined;
    const identifier = buildIdentifier(entry.type, className);
    const command: CommandItem = {
      name: entry.type,
      identifier,
      label: entry.label || master.label,
      description: master.description,
      iconName: entry.icon ?? master.icon,
      aliases: master.aliases,
      isTextMenu: master.isTextMenu,
      class: className,
      isDisabled: () => false,
    };

    // 後勝ち: 既存エントリを消してから入れ直すと、Map の挿入順も最後の位置に揃う
    byIdentifier.delete(identifier);
    byIdentifier.set(identifier, { group: currentGroup, command });
  }

  const commandsByGroup = new Map<string, CommandItem[]>();
  const groupOrder: string[] = [];

  for (const { group, command } of byIdentifier.values()) {
    if (!commandsByGroup.has(group)) {
      commandsByGroup.set(group, []);
      groupOrder.push(group);
    }
    commandsByGroup.get(group)!.push(command);
  }

  return groupOrder.map((title, index) => ({
    name: `group-${index + 1}`,
    title,
    commands: commandsByGroup.get(title) ?? [],
  }));
};

const mergeBlockMenus = (base: BlockMenuItem[], override: BlockMenuOverride): BlockMenuItem[] => {
  // `blocks` が配列で指定されたら base / allow / deny を一切無視して完全置換。
  if (Array.isArray(override.blocks)) {
    return buildFromBlocksOverride(override.blocks);
  }

  const flattened = flattenBlockMenus(base);
  const filtered = applyAllowDenyFilter(flattened.commands, override);
  return regroupCommands(filtered, flattened.groupMetaByTitle, flattened.baseGroupOrder);
};

/**
 * API 由来の EditorSettings に、エディター個別の上書き設定をマージする。
 *
 * - features は浅いマージで個別フラグだけを上書きする。
 * - blockMenus は宣言的スキーマ (allow / deny / blocks) を解釈する。
 *   allow / deny は識別子 (`type` または `type.class`) で base をフィルタする。
 *   blocks (配列) を指定した場合は base / allow / deny を無視して完全置換となる。
 * - その他のリスト系プロパティは配列ごと差し替える。
 */
export default function mergeSettings(base: EditorSettings, override?: EditorSettingsOverride): EditorSettings {
  if (!isPlainObject(override)) {
    return base;
  }

  return {
    features: { ...base.features, ...(isPlainObject(override.features) ? override.features : {}) },
    blockMenus: isPlainObject(override.blockMenus)
      ? mergeBlockMenus(base.blockMenus, override.blockMenus)
      : base.blockMenus,
    fontSize: Array.isArray(override.fontSize) ? override.fontSize : base.fontSize,
    fontFamily: Array.isArray(override.fontFamily) ? override.fontFamily : base.fontFamily,
    customClass: Array.isArray(override.customClass) ? override.customClass : base.customClass,
    imageSizes: Array.isArray(override.imageSizes) ? override.imageSizes : base.imageSizes,
    colorPalette: Array.isArray(override.colorPalette) ? override.colorPalette : base.colorPalette,
  };
}
