/**
 * `robot_error_reports` を UPDATE する唯一のファイル（指示書AT §3-2 / 段階4。指示書BD §5-4。D-184）。
 *
 * ★★メインDBへの書き込みはこのファイルを通すこと★★（D-042 / D-184 / `CLAUDE.md` §7）—
 *   書いてよい列は★経路ごとに違う★:
 *   - 事前チェック（`PATCH /prechecks/{id}/status`）: ★4列★（`status` / `comment` / `responded_at` / `resolved_at`）。
 *     `robotErrorColumnsFromPatch()` / `writeRobotErrorColumns()`
 *   - エラー一覧（`PATCH /errors/{error_id}/status`。D-Sec アラートの保存も同じ経路）: ★5列★（上の4列＋`error_list_id`）。
 *     `robotErrorColumnsFromErrorPatch()` / `writeErrorReportColumns()`。★`error_list_id` を書けるのはこの経路だけ★
 *     （D-Sec の画面は送らない。送っても `D-S_CONTROL_FAILURE` の行は選べないのでルートが422にする）
 *   更新用アカウントは列単位の GRANT なので、許された列以外を書くと権限エラーになる。
 *   ★`error_list_id` の UPDATE 権限は未確認★（指示書BD §8。接続回復後に `SHOW GRANTS` で確かめる）。
 *
 * ★クライアントを引数で受ける★ — `prismaSource.updatePrecheckErrorColumns()` / `updateErrorReportColumns()` は
 *   書き込み用の接続（`getWritePrisma()`）を、ガードテスト（`lib/db/__tests__/robot-error-write-sql.test.ts`）は
 *   ★本物の生成クライアント＋SQLを記録するだけの偽アダプタ★を渡す。★テスト用に別の書き方をしない★。
 *
 * ★★`updateMany`（1文の UPDATE）を使う。`update` を使わないこと★★（指示書AT §3-2）—
 *   `update` は SELECT → UPDATE → 全列の SELECT をトランザクションで包む（実測）。
 *   更新用アカウントに SELECT 権限があるかは分かっていない。`updateMany` なら要るのは
 *   「書く列の UPDATE」＋「WHERE で読む `error_id` の SELECT」だけ。
 *
 * ★★`prisma/schema.prisma` の `RobotErrorReport.updated_at` に `@updatedAt` を付けないこと★★
 *   （指示書AT §3-1）— 付けると `updateMany` でも Prisma が `updated_at = <アプリの時刻(UTC)>` を
 *   SET に足す。DB の ON UPDATE が自分で更新する。ガードテストが見張っている。
 *
 * ★生SQL（`$executeRaw`）は使わない★（`CLAUDE.md` §3 / D-122）。
 */
import { robotErrorStatusFromResolutionStatus } from "@/lib/db/precheck-mapping";
import { toDbJst } from "@/lib/format/date";
import type { PrismaClient } from "@/lib/generated/prisma/client";
import type { ErrorStatusPatch, PrecheckStatusPatch } from "@/lib/types";

/**
 * ★事前チェックの経路★（`PATCH /prechecks/{id}/status`）で書き込みを許された4列（D-042）。
 * ★ここに列を足さないこと★ — `error_list_id` は `ERROR_REPORT_WRITABLE_COLUMNS`（エラー一覧の経路だけ。D-184）
 */
export const ROBOT_ERROR_WRITABLE_COLUMNS = [
  "status",
  "comment",
  "responded_at",
  "resolved_at",
] as const;

export type RobotErrorWritableColumn = (typeof ROBOT_ERROR_WRITABLE_COLUMNS)[number];

/**
 * ★エラー一覧の経路★（`PATCH /errors/{error_id}/status`）で書き込みを許された5列（D-042 / D-184）。
 * ★5列目の `error_list_id` はこの経路だけ★（事前チェック・`robotErrorColumnsFromPatch()` は4列のまま）。
 * ★ここに列を足さないこと★（足すなら先に決定記録と `CLAUDE.md` §7 を直す）
 */
export const ERROR_REPORT_WRITABLE_COLUMNS = [...ROBOT_ERROR_WRITABLE_COLUMNS, "error_list_id"] as const;

export type ErrorReportWritableColumn = (typeof ERROR_REPORT_WRITABLE_COLUMNS)[number];

/** `updateMany` の `data` に渡す形（4列のうち変える列だけ） */
export interface RobotErrorColumnsData {
  status?: number;
  comment?: string | null;
  responded_at?: Date | null;
  resolved_at?: Date | null;
}

/** エラー一覧の経路の `data`（5列のうち変える列だけ）。`error_list_id` は bigint unsigned なので `bigint` */
export interface ErrorReportColumnsData extends RobotErrorColumnsData {
  error_list_id?: bigint;
}

/** パッチ（`PrecheckStatusPatch`）が持ってよいキー */
const PATCH_KEYS: readonly string[] = ["status", "note", "responded_at", "resolved_at"];

/** パッチ（`ErrorStatusPatch`）が持ってよいキー（★`error_list_id` はエラー一覧の経路だけ★） */
const ERROR_PATCH_KEYS: readonly string[] = [...PATCH_KEYS, "error_list_id"];

/** 書き込みに使うクライアントの最小限の形（本物の `PrismaClient` がそのまま入る） */
export type RobotErrorWriteClient = Pick<PrismaClient, "robotErrorReport">;

function dateOrNull(value: unknown, key: string): Date | null {
  if (value === null) return null;
  if (value instanceof Date && !Number.isNaN(value.getTime())) {
    // ★真の瞬間 → DB に書きたい壁時計値★（アダプタは `getUTC*` で文字列を作る。`toDbJst()` の docstring）
    return toDbJst(value);
  }
  throw new Error(`[robot-error-write] ${key} は Date か null で渡してください`);
}

/** パッチのキーが `allowed` の中だけか（★値は出さない★） */
function assertPatchKeys(patch: object, allowed: readonly string[]): void {
  for (const key of Object.keys(patch)) {
    if (!allowed.includes(key)) {
      throw new Error(`[robot-error-write] 書き込めないキーです: ${key}`);
    }
  }
}

/** 写した結果の列が `allowed` の中だけで、空でないか */
function assertDataColumns(data: object, allowed: readonly string[]): void {
  const columns = Object.keys(data);
  for (const column of columns) {
    if (!allowed.includes(column)) {
      throw new Error(`[robot-error-write] 書き込めない列です: ${column}`);
    }
  }
  if (columns.length === 0) {
    throw new Error("[robot-error-write] 書き込む列がありません（空パッチは書かないこと）");
  }
}

/** 4列ぶんの写し（キー・列の検査は呼び出し側）。★2つの経路で同じ規則を使うための内部関数★ */
function fourColumnsFromPatch(patch: PrecheckStatusPatch): RobotErrorColumnsData {
  const data: RobotErrorColumnsData = {};
  if ("status" in patch && patch.status !== undefined) {
    const status: unknown = robotErrorStatusFromResolutionStatus(patch.status);
    if (typeof status !== "number") {
      throw new Error("[robot-error-write] status の値が不正です");
    }
    data.status = status;
  }
  if ("note" in patch && patch.note !== undefined) {
    if (patch.note !== null && typeof patch.note !== "string") {
      throw new Error("[robot-error-write] note は文字列か null で渡してください");
    }
    data.comment = patch.note;
  }
  if ("responded_at" in patch && patch.responded_at !== undefined) {
    data.responded_at = dateOrNull(patch.responded_at, "responded_at");
  }
  if ("resolved_at" in patch && patch.resolved_at !== undefined) {
    data.resolved_at = dateOrNull(patch.resolved_at, "resolved_at");
  }
  return data;
}

/**
 * パッチ → `updateMany` の `data`（純粋関数）。★事前チェックの経路（4列）★。
 *
 * ★実行時の歯止め（型で防げていても残す。指示書AT 段階4-2）★:
 *   - パッチに4つ以外のキーがあれば例外（リクエスト本文のキーが DB に届く経路を作らない）。
 *     ★`error_list_id` も例外★ — 事前チェックの経路からは書かない（指示書BD §2。D-184）
 *   - 写した結果のキーが4列以外を含めば例外
 *   - 空なら例外（何も書かない要求はここまで来ない。ルートが手前で返す）
 * ★例外のメッセージに値を出さない★（備考の中身が載らないように）。
 */
export function robotErrorColumnsFromPatch(patch: PrecheckStatusPatch): RobotErrorColumnsData {
  assertPatchKeys(patch, PATCH_KEYS);
  const data = fourColumnsFromPatch(patch);
  assertDataColumns(data, ROBOT_ERROR_WRITABLE_COLUMNS);
  return data;
}

/**
 * パッチ → `updateMany` の `data`（純粋関数）。★エラー一覧の経路（5列）★（指示書BD §5-4。D-184）。
 *
 * 4列の写しは `robotErrorColumnsFromPatch()` と★同じ内部関数★。加えて `error_list_id`（1以上の安全な整数）を `bigint` にする。
 * ★実行時の歯止め（4列版と同じ）★: 5つ以外のキー → 例外 / 写した結果が5列以外を含む → 例外 / 空 → 例外 /
 *   `error_list_id` が1以上の安全な整数でない → 例外（値の検証はルートの手前で済む。ここに来たら呼び出しの誤り）。
 * ★例外のメッセージに値を出さない★。
 */
export function robotErrorColumnsFromErrorPatch(patch: ErrorStatusPatch): ErrorReportColumnsData {
  assertPatchKeys(patch, ERROR_PATCH_KEYS);
  const { error_list_id: errorListId, ...rest } = patch;
  const data: ErrorReportColumnsData = fourColumnsFromPatch(rest);
  if (errorListId !== undefined) {
    if (typeof errorListId !== "number" || !Number.isSafeInteger(errorListId) || errorListId < 1) {
      throw new Error("[robot-error-write] error_list_id は1以上の整数で渡してください");
    }
    data.error_list_id = BigInt(errorListId);
  }
  assertDataColumns(data, ERROR_REPORT_WRITABLE_COLUMNS);
  return data;
}

/** `data` のキー（＝実際に SET する列）を、許可4列の並び順で返す（監査ログ用） */
export function writtenColumns(data: RobotErrorColumnsData): RobotErrorWritableColumn[] {
  return ROBOT_ERROR_WRITABLE_COLUMNS.filter((column) => column in data);
}

/** `data` のキー（＝実際に SET する列）を、許可5列の並び順で返す（エラー一覧の経路の監査ログ用） */
export function writtenErrorReportColumns(data: ErrorReportColumnsData): ErrorReportWritableColumn[] {
  return ERROR_REPORT_WRITABLE_COLUMNS.filter((column) => column in data);
}

/**
 * `error_id` の1行の4カラムを `updateMany` の1文で書く（★事前チェックの経路★。`error_list_id` は書けない）。
 *
 * ★戻り値の件数で存在を判定しないこと★（指示書AT §3-2）— 存在確認は呼び出し側が書く前に
 *   参照用の接続で行う。件数が 0 なら「確認と書き込みの間に行が消えた」とみなす（ルートが 404 にする）。
 */
export async function writeRobotErrorColumns(
  client: RobotErrorWriteClient,
  errorId: string,
  patch: PrecheckStatusPatch
): Promise<{ matched: number; columns: RobotErrorWritableColumn[] }> {
  const data = robotErrorColumnsFromPatch(patch);
  const result = await client.robotErrorReport.updateMany({
    where: { error_id: errorId },
    data,
  });
  return { matched: result.count, columns: writtenColumns(data) };
}

/**
 * `error_id` の1行の5列（変える列だけ）を `updateMany` の1文で書く（★エラー一覧の経路★。指示書BD §5-4）。
 * ★`writeRobotErrorColumns()` と同じ書き方★（`updateMany` の1文・件数で存在を判定しない）。違いは写しの関数だけ。
 */
export async function writeErrorReportColumns(
  client: RobotErrorWriteClient,
  errorId: string,
  patch: ErrorStatusPatch
): Promise<{ matched: number; columns: ErrorReportWritableColumn[] }> {
  const data = robotErrorColumnsFromErrorPatch(patch);
  const result = await client.robotErrorReport.updateMany({
    where: { error_id: errorId },
    data,
  });
  return { matched: result.count, columns: writtenErrorReportColumns(data) };
}

/** パッチが SET する列（監査ログの `columns`。書く前に分かるので、失敗時のログにも載せられる） */
export function columnsForPatch(patch: PrecheckStatusPatch): RobotErrorWritableColumn[] {
  return writtenColumns(robotErrorColumnsFromPatch(patch));
}

/** エラー一覧の経路のパッチが SET する列（監査ログの `columns`） */
export function columnsForErrorPatch(patch: ErrorStatusPatch): ErrorReportWritableColumn[] {
  return writtenErrorReportColumns(robotErrorColumnsFromErrorPatch(patch));
}
