/**
 * エラー一覧（`ErrorReportItem`。`GET /errors`。指示書AW）を組み立てるときの変換ロジック（純粋関数のみ）。
 *
 * ★`lib/db/precheck-mapping.ts` と同じ作法★ — DB アクセスを混ぜず、データ層（`lib/db/index.ts` の
 *   fixtures 版・Prisma 版）から呼ぶだけにして、テストは純粋関数だけで書けるようにする。
 *   ★fixtures と Prisma は同じ関数（行 → API の値・絞り込み・並べ替え）を通す★（指示書AW D13 / `CLAUDE.md` §9.5）。
 *
 * ★`precheck-mapping.ts` に足さず別ファイルにした理由★: `/alerts`（事前チェックの行だけ）とは対象が違う
 *   （`robot_error_reports` の全行。D-160 項1）。★状態コード・検知時刻・分数の規則は `precheck-mapping.ts` のものを
 *   そのまま使う★（2箇所に書かない。指示書AW D2 / D3 / D4）。
 *
 * ★2段階で読む★（指示書AW D8）:
 *   1段目 = キーだけ（`ROBOT_ERROR_LIST_KEY_SELECT` → `errorReportKeyFromRow()`）で絞り込み・並べ替え・ページ分け、
 *   2段目 = そのページの行だけ列を足して読む（`ROBOT_ERROR_LIST_ROW_SELECT` → `errorReportItemFromRow()`）。
 *   ★`errored_at ?? created_at` の順は Prisma のクエリビルダで書けない★（`COALESCE` の `orderBy` が無い。生SQL は禁止）。
 *
 * テストは `lib/db/__tests__/error-report-mapping.test.ts`（表1 は監督確定。変えないこと）。
 */
import { dbDateRange } from "@/lib/db/date-range";
import {
  detectedAtFromError,
  precheckMinutesFromCode,
  resolutionStatusFromRobotErrorStatus,
  type PrecheckDetectedAtSource,
} from "@/lib/db/precheck-mapping";
import { errorReportStatusCodes } from "@/lib/error-reports/query";
import { jstIsoFromDb } from "@/lib/format/date";
import type { Prisma } from "@/lib/generated/prisma/client";
import type { ErrorReportDbQuery, ErrorReportItem, ErrorScheduleLink } from "@/lib/types";

/** 1段目の行（`ROBOT_ERROR_LIST_KEY_SELECT` の結果がそのまま入る形） */
export interface ErrorReportKeySource extends PrecheckDetectedAtSource {
  error_id: string;
  facility_id: bigint;
  error_type: string;
  status: number;
}

/** 絞り込み・並べ替えに使うキー（★`status` は生の数値★。表2 F12） */
export interface ErrorReportKey {
  error_id: string;
  facility_id: number;
  error_type: string;
  /** `jstIsoFromDb(detectedAtFromError(row))` */
  detected_at: string;
  status: number;
}

/** 2段目の行（`ROBOT_ERROR_LIST_ROW_SELECT` の結果がそのまま入る形） */
export interface ErrorReportRowSource extends PrecheckDetectedAtSource {
  error_id: string;
  clean_timetable_id: bigint | null;
  serial_number: string;
  facility_id: bigint;
  facility: { facility_name: string };
  error_type: string;
  error_level: string;
  status: number;
  responded_at: Date | null;
  resolved_at: Date | null;
  comment: string | null;
}

/** 予定（本表・履歴のどちらか。`findTimetables*()` の `select: { id: true, precheck: true }`） */
export interface ErrorTimetableRef {
  id: bigint;
  precheck: number;
}

/**
 * 1段目の行 → 絞り込み・並べ替えのキー。
 *
 * ★`status` は生の数値のまま持つ★ — ここで `resolutionStatusFromRobotErrorStatus()` に通すと、
 *   0/1/2 以外（`UnsignedTinyInt` なので3以上が入りうる）の行が1件あるだけで一覧全体が500になる。
 *   一覧では `status IN`（SQL）と `filterSortErrorReportKeys()`（生の数値で比べる）で外れる（表2 F12。§11 Q3）。
 * ★検知時刻は `errored_at ?? created_at`★（`detectedAtFromError()`。D3）を `jstIsoFromDb()` に通す —
 *   DB の値は JST の壁時計値（`CLAUDE.md` §3）。★9時間を足さない・引かない★。
 */
export function errorReportKeyFromRow(row: ErrorReportKeySource): ErrorReportKey {
  return {
    error_id: row.error_id,
    // ★BigInt を残さない★（キーは number で比べる。`filterSortErrorReportKeys()` の facility_ids と揃える）
    facility_id: Number(row.facility_id),
    error_type: row.error_type,
    detected_at: jstIsoFromDb(detectedAtFromError(row)),
    status: row.status,
  };
}

/**
 * エラー行の `clean_timetable_id` と引き当てた予定 → 「紐づく予定」の4区分（指示書AW D4）。
 *
 * | 条件 | `schedule_link` | `precheck_minutes` |
 * |---|---|---|
 * | `clean_timetable_id` が NULL | `none`（予定なし） | null |
 * | ★本表にも履歴にも無い id★（`timetable` が `undefined`） | `unknown`（予定不明） | null |
 * | `precheckMinutesFromCode(precheck)` が null | `main`（本清掃） | null |
 * | それ以外 | `precheck`（事前チェック） | ★生の分数★（設定の 60/30 に無い値でも出す。D-105 項1） |
 *
 * ★`CLEAN_TIMETABLE_PRECHECK.main` を自分で比べない★（`precheckMinutesFromCode()` の1本）。
 * ★予定の id と `clean_timetable_id` が違えば例外★ — 呼び出し側の引き当ての誤り（別の行の予定を貼った）であり、
 *   黙って区分を出すと誰も気づけない。
 *
 * @param cleanTimetableId `robot_error_reports.clean_timetable_id`
 * @param timetable 本表・履歴から id で引いた予定（★同じ id は本表が勝つ★。D5）。見つからなければ `undefined`
 */
export function errorScheduleLink(
  cleanTimetableId: bigint | null,
  timetable: ErrorTimetableRef | undefined
): { schedule_link: ErrorScheduleLink; precheck_minutes: number | null } {
  if (cleanTimetableId === null) return { schedule_link: "none", precheck_minutes: null };
  if (timetable === undefined) return { schedule_link: "unknown", precheck_minutes: null };
  if (timetable.id !== cleanTimetableId) {
    throw new Error(
      `[error-report-mapping] 予定の引き当てが食い違っています（clean_timetable_id=${cleanTimetableId.toString()} に id=${timetable.id.toString()} の予定が渡された）`
    );
  }
  const minutes = precheckMinutesFromCode(timetable.precheck);
  if (minutes === null) return { schedule_link: "main", precheck_minutes: null };
  return { schedule_link: "precheck", precheck_minutes: minutes };
}

/**
 * 2段目の行 → `ErrorReportItem`（指示書AW 2-4）。
 *
 * ★一覧（`errorReports()`）と PATCH の前後の読み取り（`errorReportById()`）がこの1本を使う★ —
 *   同じ行の作り方を2箇所に書かない（片方だけ直すと、更新後に返す行と一覧の行が食い違う。AT 段階4-1 と同じ理由）。
 * ★`precheckItemFromErrorRow()`（`lib/db/precheck-mapping.ts`）と同じ書き方で揃える★（日時・BigInt の扱い）。
 *
 * ★`status` が 0/1/2 以外なら例外★（`resolutionStatusFromRobotErrorStatus()`）— 一覧では1段目で外れているので来ない。
 *   PATCH の1件読みで来たら500（fail-loud。§11 Q3）。
 *
 * @param row エラー行（`ROBOT_ERROR_LIST_ROW_SELECT`）
 * @param timetable `row.clean_timetable_id` で本表・履歴から引いた予定（無ければ `undefined`）
 */
export function errorReportItemFromRow(
  row: ErrorReportRowSource,
  timetable: ErrorTimetableRef | undefined
): ErrorReportItem {
  const link = errorScheduleLink(row.clean_timetable_id, timetable);
  return {
    // ★`error_id` を string のまま★（varchar(50)。D-105 項2）。★`Number()` を通さない★
    id: row.error_id,
    // ★`errored_at ?? created_at`（`detectedAtFromError()` に規則を閉じ込めてある）★
    detected_at: jstIsoFromDb(detectedAtFromError(row)),
    // ★BigInt を残すと `NextResponse.json` が例外を投げる★
    facility_id: Number(row.facility_id),
    facility_name: row.facility.facility_name,
    // ★生値のまま★（`'0'` も・区分も作らない。D-077 / D-160 項3）
    serial_number: row.serial_number,
    error_type: row.error_type,
    error_level: row.error_level,
    // ★画面には出さない★（「予定不明」の調査用。D18）。`clean_timetables.id` は BigInt なので number にする
    clean_timetable_id: row.clean_timetable_id === null ? null : Number(row.clean_timetable_id),
    schedule_link: link.schedule_link,
    precheck_minutes: link.precheck_minutes,
    // ★書き込み許可4カラム（D-042）。書くのは `lib/db/robot-error-write.ts`（指示書AT）★
    resolution_status: resolutionStatusFromRobotErrorStatus(row.status),
    responded_at: row.responded_at ? jstIsoFromDb(row.responded_at) : null,
    resolved_at: row.resolved_at ? jstIsoFromDb(row.resolved_at) : null,
    note: row.comment,
  };
}

/**
 * 1段目の `where`（★SQL 側の写し。絞り込みの「正」は `filterSortErrorReportKeys()`★。指示書AW 2-4）。
 *
 * - `status IN`: `errorReportStatusCodes()` から作る（★0/1/2 を直書きしない。D2★）
 * - `facility_id IN`: ★`!== null` で分岐する★（`null` = 絞らない / `[]` = 0件。罠1。`?.length` で書かない）
 * - 期間: ★`dbDateRange()` の1箇所★（DB の壁時計値。9時間を足さない・引かない）。
 *   ★`errored_at` が NULL の行は `created_at` で絞る★（`OR`）— `errored_at` だけにかけると、
 *   `errored_at` が NULL の行（表1 E7）が期間指定で消える（表2 F6）
 *
 * ★`error_type` は SQL に降ろさない★（D9。`LIKE` は `%` `_` のエスケープと DB の照合順序で fixtures と結果が割れる）。
 * ★`page` / `per_page` も降ろさない★（D8。並べ替えをアプリで行うため）。
 */
export function errorReportListWhere(query: ErrorReportDbQuery): Prisma.RobotErrorReportWhereInput {
  const range = dbDateRange(query.date_from, query.date_to);
  return {
    status: { in: errorReportStatusCodes(query.include_resolved) },
    ...(query.facility_ids !== null
      ? { facility_id: { in: query.facility_ids.map((id) => BigInt(id)) } }
      : {}),
    ...(range.gte || range.lt
      ? { OR: [{ errored_at: range }, { errored_at: null, created_at: range }] }
      : {}),
  };
}
