/**
 * エラー内容を選ぶ窓の絞り込み（純粋関数。指示書BE §4-1・§8）。
 * キーワードと分類は同時に効く（AND）。文字列は `t()` に通さない。
 */
import type { Lang } from "@/lib/i18n";
import type { ErrorListItem } from "@/lib/types";

export interface ErrorListCategory {
  /** 大分類の日英を組にした識別子（表示には使わない） */
  key: string;
  ja: string;
  en: string;
}

/** `<option value>` に載せるので、分類名に出ない区切りにする（NUL は値から落ちることがある） */
const CATEGORY_KEY_SEP = "\u241e";

export function errorListCategoryKey(item: ErrorListItem): string {
  return `${item.error_category_main_ja}${CATEGORY_KEY_SEP}${item.error_category_main_en}`;
}

/** 大分類を id 順に、初めて出た順で並べる */
export function errorListCategories(items: readonly ErrorListItem[]): ErrorListCategory[] {
  const seen = new Map<string, ErrorListCategory>();
  const sorted = [...items].sort((a, b) => a.id - b.id);
  for (const item of sorted) {
    const key = errorListCategoryKey(item);
    if (!seen.has(key)) {
      seen.set(key, {
        key,
        ja: item.error_category_main_ja,
        en: item.error_category_main_en,
      });
    }
  }
  return [...seen.values()];
}

/** ひらがな（ぁ U+3041 〜 ゖ U+3096）→ カタカナ（ァ U+30A1 〜 ヶ U+30F6）。差は 0x60 */
const HIRAGANA_FIRST = 0x3041;
const HIRAGANA_LAST = 0x3096;
const HIRAGANA_TO_KATAKANA = 0x60;

/**
 * キーワードと詳細の両側に掛ける正規化。
 * 1. NFKC（全角英数 → 半角、半角カナ → 全角カナ（濁点も1文字に）、全角スペース → 半角スペース）
 * 2. 小文字にする
 * 3. ひらがな → カタカナ（`ばってりー` で `バッテリー` に当てるため）
 * ★記号・空白は除かない★（AND 検索にもしない。空白は1つの文字として部分一致に入る）。
 */
export function normalizeErrorListKeyword(text: string): string {
  let out = "";
  for (const ch of text.normalize("NFKC").toLowerCase()) {
    const code = ch.codePointAt(0)!;
    out +=
      code >= HIRAGANA_FIRST && code <= HIRAGANA_LAST
        ? String.fromCodePoint(code + HIRAGANA_TO_KATAKANA)
        : ch;
  }
  return out;
}

/**
 * ★キーワードはエラー内容の詳細だけに当てる★（`error_detail_ja` と `error_detail_en` の両方。★画面の言語によらない★）。
 * ★小分類・大分類の名前には当てない★（分類は分類の選択で絞る）。
 * 部分一致。比べる前に両側を `normalizeErrorListKeyword()` に通す（全角・半角、大文字・小文字、ひらがな・カタカナを区別しない）。
 * キーワードの前後の空白（全角スペースを含む）は除く。空ならキーワードでは絞らない。
 * ★分類とは AND★ — `categoryKey` が null でなければ、その大分類の外の行はキーワードに当たっても返さない
 * （`categoryKey === null` はすべての分類）。並びは渡された順（API の id 順）のまま。
 * `lang` は今は絞り込みに使わない（呼び出し側の形を変えないために受け取る）。
 */
export function filterErrorLists(
  items: readonly ErrorListItem[],
  input: { lang: Lang; keyword: string; categoryKey: string | null }
): ErrorListItem[] {
  const keyword = normalizeErrorListKeyword(input.keyword).trim();
  return items.filter((item) => {
    if (input.categoryKey !== null && errorListCategoryKey(item) !== input.categoryKey) {
      return false;
    }
    if (keyword === "") return true;
    return [item.error_detail_ja, item.error_detail_en].some((text) =>
      normalizeErrorListKeyword(text).includes(keyword)
    );
  });
}

export interface ErrorListGroup {
  heading: string;
  items: ErrorListItem[];
}

/** 見出しは「大分類 / 小分類」。同じ見出しが続いているあいだは1つの塊にする（id 順のまま） */
export function groupErrorLists(lang: Lang, items: readonly ErrorListItem[]): ErrorListGroup[] {
  const groups: ErrorListGroup[] = [];
  for (const item of items) {
    const main = lang === "ja" ? item.error_category_main_ja : item.error_category_main_en;
    const sub = lang === "ja" ? item.error_category_sub_ja : item.error_category_sub_en;
    const heading = `${main} / ${sub}`;
    const last = groups[groups.length - 1];
    if (last && last.heading === heading) last.items.push(item);
    else groups.push({ heading, items: [item] });
  }
  return groups;
}

/**
 * 背景のクリックで閉じてよいか（指示書BE §4-1）。
 * ★押した（mousedown）のも離した（click）のも背景そのもののときだけ★閉じる。
 * キーワード入力の中で文字を選び、背景の上で離したときに閉じないため。
 */
export function shouldCloseOnBackdrop(downOnOverlay: boolean, clickOnOverlay: boolean): boolean {
  return downOnOverlay && clickOnOverlay;
}

/**
 * Tab でフォーカスを窓の中に閉じ込める（指示書BE §4-1）。
 * `count` は押した時点のフォーカスできる要素の数（絞り込みで変わるので毎回数える）、
 * `activeIndex` は今のフォーカスの位置（窓の外なら -1）。
 * 戻り値はフォーカスを移す位置。null はブラウザの既定の移動に任せる。
 */
export function trapFocusIndex(count: number, activeIndex: number, backward: boolean): number | null {
  if (count <= 0) return null;
  const last = count - 1;
  if (backward) return activeIndex <= 0 || activeIndex > last ? last : null;
  return activeIndex < 0 || activeIndex >= last ? 0 : null;
}
