/**
 * エラー内容の選択肢（`error_lists`）の読み取りと、エラー行（`robot_error_reports.error_list_id`）の名前解決（指示書BD §5-3。D-184）。
 *
 * ★2つの用途で規則が違う★（指示書BD §3-1 / §3-2）:
 *   - 選択肢（`GET /error-lists`）= ★有効な行だけ★（`inactive = 0` かつ `deleted_at IS NULL`）・★id の昇順★
 *     → `activeErrorListItems()`（★これが正★。Prisma 版の `ACTIVE_ERROR_LIST_WHERE` / `orderBy` は写し）
 *   - 名前解決（`ErrorReportItem.error_list`）= ★絞らない★（無効化・削除済みの行でも解決する。施設名と同じ。罠7）
 *     → `errorListIndex()` / `resolveErrorList()`。★`loadErrorListIndexByIds()` に `inactive` / `deleted_at` の条件を足さないこと★
 *
 * ★N+1 にしない★ — 一覧・1件とも、行の `error_list_id`（非 null）を `errorListIdsOf()` で集めて
 *   ★`errorList.findMany({ where: { id: { in } } })` を1回★だけ撃つ（id が0件なら撃たない）。
 *   ★`in` の要素数は `error_lists` の行数（決定稿 131行）を超えない★（同じ id は1つに畳む）ので分割しない。
 *
 * ★文字列は DB の値のまま★（`t()` に通さない・変換しない。D-077 と同じ。指示書BD §2）。
 * ★BigInt → number は `Number()`★（`facility_id` 等と同じ。`lib/db/error-report-mapping.ts`）。
 * ★Map のキーは `bigint.toString()`★（`bigint` は Map のキーにならない。予定の引き当てと同じ。指示書AW 4-9）。
 *
 * テストは `lib/db/__tests__/error-list-source.test.ts`。
 */
import { ERROR_LIST_NOTE_REQUIRED_IDS, ERROR_LIST_UNSELECTABLE_ERROR_TYPES } from "@/lib/config";
import type { Prisma } from "@/lib/generated/prisma/client";
import type { ErrorListItem } from "@/lib/types";

/**
 * `error_lists` から読む列（選択肢・名前解決の共通）。
 * ★`inactive` / `deleted_at` は `activeErrorListItems()`（選択肢の絞り込みの正）が見る★。名前解決では見ない。
 * ★`created_at` / `updated_at` は読まない★（API に出さない）。
 */
export const ERROR_LIST_SELECT = {
  id: true,
  error_category_main_en: true,
  error_category_main_ja: true,
  error_category_sub_en: true,
  error_category_sub_ja: true,
  error_detail_en: true,
  error_detail_ja: true,
  inactive: true,
  deleted_at: true,
} as const satisfies Prisma.ErrorListSelect;

/** `ERROR_LIST_SELECT` の結果の形（fixtures の `FixtureErrorListRow` もこれを満たす） */
export interface ErrorListRowSource {
  id: bigint;
  error_category_main_en: string;
  error_category_main_ja: string;
  error_category_sub_en: string;
  error_category_sub_ja: string;
  error_detail_en: string;
  error_detail_ja: string;
  inactive: number;
  deleted_at: Date | null;
}

/**
 * 選択肢の `where`（★SQL 側の写し。正は `isActiveErrorListRow()`★）。
 * ★名前解決（`loadErrorListIndexByIds()`）には使わない★。
 */
export const ACTIVE_ERROR_LIST_WHERE = {
  inactive: 0,
  deleted_at: null,
} as const satisfies Prisma.ErrorListWhereInput;

/** 選択肢に出す行か（`inactive === 0` かつ `deleted_at === null`。★`inactive` は 0 以外をすべて無効とみなす★） */
export function isActiveErrorListRow(row: Pick<ErrorListRowSource, "inactive" | "deleted_at">): boolean {
  return row.inactive === 0 && row.deleted_at === null;
}

/** 行 → `ErrorListItem`（★文字列は変換しない★。`note_required` は `ERROR_LIST_NOTE_REQUIRED_IDS` から） */
export function errorListItemFromRow(row: ErrorListRowSource): ErrorListItem {
  const id = Number(row.id);
  return {
    id,
    error_category_main_en: row.error_category_main_en,
    error_category_main_ja: row.error_category_main_ja,
    error_category_sub_en: row.error_category_sub_en,
    error_category_sub_ja: row.error_category_sub_ja,
    error_detail_en: row.error_detail_en,
    error_detail_ja: row.error_detail_ja,
    note_required: ERROR_LIST_NOTE_REQUIRED_IDS.includes(id),
  };
}

/**
 * 選択肢（`GET /error-lists`）: ★有効な行だけ・id の昇順★（fixtures 版・Prisma 版で共有。★これが正★）。
 * ★入力の配列を並べ替えない★（コピーしてから並べる）。
 */
export function activeErrorListItems(rows: readonly ErrorListRowSource[]): ErrorListItem[] {
  return rows
    .filter(isActiveErrorListRow)
    .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
    .map(errorListItemFromRow);
}

/** 名前解決の索引（キーは `error_lists.id` の `toString()`） */
export type ErrorListIndex = ReadonlyMap<string, ErrorListItem>;

/** 空の索引（`error_list_id` が全部 null のとき。★クエリを撃たない★） */
export const EMPTY_ERROR_LIST_INDEX: ErrorListIndex = new Map();

/** ★絞らずに★索引を作る（無効化・削除済みの行も入れる。名前解決は行の有無だけを見る） */
export function errorListIndex(rows: readonly ErrorListRowSource[]): ErrorListIndex {
  return new Map(rows.map((row) => [row.id.toString(), errorListItemFromRow(row)]));
}

/** エラー行の `error_list_id` のうち非 null のものを、重複なく集める（`in` に渡す。並びは初出順） */
export function errorListIdsOf(rows: readonly { error_list_id: bigint | null }[]): bigint[] {
  const seen = new Map<string, bigint>();
  for (const row of rows) {
    if (row.error_list_id !== null) seen.set(row.error_list_id.toString(), row.error_list_id);
  }
  return [...seen.values()];
}

/**
 * `error_list_id` → `ErrorListItem | null`。
 * ★null（未選択）→ null★／★索引に無い id（`error_lists` に無い）→ null★（画面は後者を「不明(id)」と出す。`error_list_id` は値を保つ）。
 */
export function resolveErrorList(errorListId: bigint | null, index: ErrorListIndex): ErrorListItem | null {
  if (errorListId === null) return null;
  return index.get(errorListId.toString()) ?? null;
}

/**
 * この行でエラー内容を選べるか（`ErrorReportItem.error_list_selectable`。指示書BD §3-2）。
 * ★`ERROR_LIST_UNSELECTABLE_ERROR_TYPES` との完全一致（`===`）★ — 大小文字・前後の空白を緩めない
 * （`D_SEC_ALERT_ERROR_TYPES` と同じ。緩めると、選べるはずの行が選べなくなる／その逆が静かに起きる）。
 * ★`PATCH /errors/{error_id}/status` の検証（指示書BD §3-3 手順2）も、行の `error_list_selectable` を通してこの1本を使う★。
 */
export function isErrorListSelectable(errorType: string): boolean {
  return !ERROR_LIST_UNSELECTABLE_ERROR_TYPES.includes(errorType);
}

/**
 * この関数が必要とする Prisma の最小限の形（★テストからスタブを渡せるようにするため★。`facility-name-source.ts` と同じ作法）。
 */
export interface ErrorListSourcePrisma {
  errorList: {
    findMany(args: {
      where: { id: { in: bigint[] } };
      select: typeof ERROR_LIST_SELECT;
    }): Promise<ErrorListRowSource[]>;
  };
}

/**
 * 名前解決の索引を Prisma から作る（一覧・1件で共有）。
 * ★`where` は `id IN` だけ★（`inactive` / `deleted_at` で絞らない。罠7と同じ。`lib/db/__tests__/error-list-source.test.ts` が固定）。
 * ★id が0件ならクエリを撃たない★。
 */
export async function loadErrorListIndexByIds(
  prisma: ErrorListSourcePrisma,
  ids: readonly bigint[]
): Promise<ErrorListIndex> {
  if (ids.length === 0) return EMPTY_ERROR_LIST_INDEX;
  const rows = await prisma.errorList.findMany({
    where: { id: { in: [...ids] } },
    select: ERROR_LIST_SELECT,
  });
  return errorListIndex(rows);
}
