/**
 * GET /clean-logs と GET /clean-logs/export で共有するクエリ処理。
 * 仕様の正は `d-watch-api.yaml` の /clean-logs。
 */
import { parseMasterIdFilters } from "@/lib/api/master-id";
import { fail, type QueryParseResult } from "@/lib/api/query-result";
import { MAX_DATE_RANGE_DAYS, PAGINATION } from "@/lib/config";
import { capConsecutiveDays } from "@/lib/format/consecutive-days";
import { toJstIso } from "@/lib/format/date";
import { includesFoldName, includesFoldPlain } from "@/lib/format/search-text";
import type { CleanLogItem, Grade } from "@/lib/types";

export type ResultFilter = "problem" | "critical" | "all";

export interface CleanLogQuery {
  dateFrom: Date | null;
  dateTo: Date | null;
  facilityName: string;
  model: string;
  serial: string;
  result: ResultFilter;
  /**
   * ホールディングス（`groups.id`）で絞り込み。★`null` = 絞らない★（指示書AK 段階2）。
   * ★`customerId` と同時に指定されたら AND★（`d-watch-api.yaml` の `GroupId`）。
   *
   * ★`CleanLogDbQuery` には載せない★ — データ層へは
   *   ★施設IDの配列（`facility_ids`）に解決してから★ 渡す約束（指示書AK §2-2 / 段階3）。
   *   施設名（`facilityName`）が `resolveFacilityIdsByName()` を通るのと同じ形。
   */
  groupId: number | null;
  /** 顧客（`customers.id`）で絞り込み。★`null` = 絞らない★ */
  customerId: number | null;
  page: number;
  perPage: number;
}

/**
 * YYYY-MM-DD を JST の 00:00 として解釈する。形式が違えば null。★存在しない日付（例 2026-02-31・2026-02-29）も null★（指示書BB A7・D-182）。
 * `/dashboard` の `date` でも同じ規則を使うため export している（実装を二重に持たない）。
 */
export function parseDate(value: string | null): Date | null {
  if (!value) return null;
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
  const d = new Date(`${value}T00:00:00+09:00`);
  if (Number.isNaN(d.getTime())) return null;
  // ★存在しない日付（2026-02-31 は 03-03 になる）を弾く★ — JST の日付に戻して入力と一致するか（指示書BB A7・D-182）。
  //   ★`toJstIso()` で戻す（実行環境のタイムゾーンに依存しない）★
  return toJstIso(d).slice(0, 10) === value ? d : null;
}

/**
 * `parseDate()` で作った `Date` を、`CleanLogDbQuery` に渡す JST の `YYYY-MM-DD` に戻す
 * （指示書AC 段階2 D-6 / ★§E-2★）。
 *
 * ★★生の `searchParams` の値を `where` に直接入れないための1段★★ —
 *   ★検証（形式・範囲）を通った値だけがデータ層に届くようにする★。
 *   ★`searchParams.get("date_from")` を `db.cleanLogs()` に渡さないこと★。
 *
 * ★`toJstIso()` を通す★ — `Date` は「真の瞬間」なので、`getFullYear()` 等の
 *   ★実行環境のタイムゾーンに依存する getter を使わないこと★（CI で日付が1日ずれる）。
 */
export function toDbQueryDate(date: Date | null): string | null {
  return date ? toJstIso(date).slice(0, 10) : null;
}

function toInt(value: string | null, fallback: number): number {
  if (!value) return fallback;
  const n = Number.parseInt(value, 10);
  return Number.isNaN(n) ? fallback : n;
}

/**
 * クエリパラメータをパースする。
 *
 * ★group_id / customer_id を読む（指示書AK 段階2）★
 *   検証は `parseMasterIdFilters()`（`lib/api/master-id.ts`）の1本に任せる。
 *   ★`Number.parseInt()` で書き直さないこと★（`1abc` が `1` として通る。D-123）。
 *   ★このファイルには D-053 の「無視する」コメントが元から無かった★
 *   （YAML は最初から宣言していて、コードが黙って捨てていた）。
 *
 * page / per_page は不正値でも失敗させず、既定値・上限へ丸める。
 */
export function parseCleanLogQuery(
  searchParams: URLSearchParams
): QueryParseResult<CleanLogQuery> {
  const dateFromRaw = searchParams.get("date_from");
  const dateToRaw = searchParams.get("date_to");
  const dateFrom = parseDate(dateFromRaw);
  const dateTo = parseDate(dateToRaw);

  if (dateFromRaw && !dateFrom) {
    return fail(422, "validation_failed", "date_fromの形式が不正です", {
      date_from: ["YYYY-MM-DD形式で指定してください"],
    });
  }
  if (dateToRaw && !dateTo) {
    return fail(422, "validation_failed", "date_toの形式が不正です", {
      date_to: ["YYYY-MM-DD形式で指定してください"],
    });
  }

  if (dateFrom && dateTo) {
    if (dateFrom > dateTo) {
      return fail(
        422,
        "validation_failed",
        "date_fromはdate_to以前の日付を指定してください"
      );
    }
    const diffDays =
      Math.floor((dateTo.getTime() - dateFrom.getTime()) / 86_400_000) + 1;
    if (diffDays > MAX_DATE_RANGE_DAYS) {
      return fail(
        422,
        "validation_failed",
        `date_from〜date_toは${MAX_DATE_RANGE_DAYS}日以内で指定してください`,
        { date_to: [`指定された範囲は${diffDays}日です`] }
      );
    }
  }

  const resultParam = (searchParams.get("result") ?? "all") as ResultFilter;
  if (!["problem", "critical", "all"].includes(resultParam)) {
    return fail(422, "validation_failed", "resultの値が不正です", {
      result: ["problem / critical / all のいずれかを指定してください"],
    });
  }

  const masters = parseMasterIdFilters(searchParams);
  if (!masters.ok) return masters;

  const page = Math.max(1, toInt(searchParams.get("page"), 1));
  const perPage = Math.min(
    PAGINATION.maxPerPage,
    Math.max(1, toInt(searchParams.get("per_page"), PAGINATION.defaultPerPage))
  );

  return {
    ok: true,
    query: {
      dateFrom,
      dateTo,
      facilityName: searchParams.get("facility_name")?.trim() ?? "",
      model: searchParams.get("model")?.trim() ?? "",
      serial: searchParams.get("serial")?.trim() ?? "",
      result: resultParam,
      groupId: masters.query.groupId,
      customerId: masters.query.customerId,
      page,
      perPage,
    },
  };
}

export function filterCleanLogs(
  rows: CleanLogItem[],
  query: CleanLogQuery
): CleanLogItem[] {
  return rows.filter((r) => {
    const scheduled = new Date(r.scheduled_at);
    if (query.dateFrom && scheduled < query.dateFrom) return false;
    if (query.dateTo) {
      const end = new Date(query.dateTo.getTime() + 86_400_000);
      if (scheduled >= end) return false;
    }
    // ★照合は `lib/format/search-text.ts` の1本に寄せる（指示書AG D-1）★
    //   施設名だけ記号（中点・長音・空白・ハイフン類）を無視する = `includesFoldName`。
    //   機種・シリアルは大文字小文字のみ = `includesFoldPlain`
    //   （記号を除くとシリアルの桁境界が消える。D-2。理由は search-text.ts 冒頭）。
    if (
      query.facilityName &&
      !includesFoldName(r.facility_name, query.facilityName)
    ) {
      return false;
    }
    // ★機種・シリアルが `null`（清掃エリアに有効なロボットがいない。指示書BB U2・D-182）の行は、絞り込みに当たらない★（表2 R7）
    if (query.model && !(r.model_name !== null && includesFoldPlain(r.model_name, query.model))) {
      return false;
    }
    if (query.serial && !(r.serial_number !== null && includesFoldPlain(r.serial_number, query.serial))) {
      return false;
    }

    // ★結果待ち（D-173）は「問題あり」「×のみ」に出さない★（ユーザー判断 2026-10-07。失敗に数えない）。
    //   `grade` は "×" のまま（不変条件）なので、★旗を見ないと混ざる★。`all` には出す（画面で「結果待ち」と見える）
    if (query.result !== "all" && r.result_pending) return false;
    if (query.result === "problem" && !isProblemGrade(r.grade)) {
      return false;
    }
    if (query.result === "critical" && r.grade !== "×") return false;

    return true;
  });
}

/**
 * ★「問題あり」（△×）の規則★（指示書AV 段階4-3）。
 *
 * ★★書き写さず、この関数を呼ぶこと★★ — `filterCleanLogs()` の `result=problem` と
 *   ダッシュボードの `problem_logs`（`app/api/dashboard/route.ts`）が共有する。
 *   片方だけ直すと、同じ「問題あり」が画面ごとに違う行を指す（例外もテスト失敗も出ない）。
 * ★出発失敗の行も含まれる★ — 出発していない行は完了率が `null` で `judgeGrade()` が × を返すため。
 * ★★結果待ち（`result_pending`。D-173）の行も `grade` は × なので、この関数だけでは除けない★★ —
 *   呼び出し側で旗を見て外すこと（`filterCleanLogs()` は外している）。
 * ★`critical`（× のみ）はここに含めない★（別の規則）。
 */
export function isProblemGrade(grade: Grade): boolean {
  return grade === "△" || grade === "×";
}

/**
 * `CleanLogItem[]` の `consecutive_problem_days` に★表示上限★をかける（指示書AF 段階6）。
 * ★純粋関数★。
 *
 * ★★呼ぶ場所は `filterCleanLogs()` → ページ切り出しの**後**★★ —
 *   絞り込みとページングは生値のまま行い、表示に載せる直前でだけ頭打ちにする。
 *
 * ★★`escalation_level` は触らない★★ — L1〜L3 は★上限をかける前の生値★で判定済み
 *   （`CLAUDE.md` §4.3 / 指示書AF 禁止事項9）。
 *
 * ★画面に出ないのに掛ける理由★（指示書AF 6-1）:
 *   「同じ施設・同じ日なのに清掃詳細ログとレポートで連続日数が違う」は実際に起きた不具合
 *   （D-109 項2 / D-103）。契約の片方だけ頭打ちにすると同じ形の食い違いが復活する。
 *
 * ★データソース（`lib/db/`）の中ではなくルート層から呼ぶ★ —
 *   `USE_FIXTURES` の両方で同じ挙動にするため（`CLAUDE.md` §9.5）。
 *
 * @param maxDays 表示上限（`loadConsecutiveDaysMax()` の戻り値を渡す。既定値を直書きしない）
 */
export function capCleanLogs(
  items: readonly CleanLogItem[],
  maxDays: number
): CleanLogItem[] {
  return items.map((item) => {
    const { days, capped } = capConsecutiveDays(item.consecutive_problem_days, maxDays);
    return {
      ...item,
      consecutive_problem_days: days,
      consecutive_problem_days_capped: capped,
    };
  });
}
