/**
 * エリア別完了率（GET /clean-logs/{id}/areas）の純粋関数。
 *
 * ★仕様の正は `d-watch-api.yaml` の `/clean-logs/{id}/areas`★
 * （出所: TeamWorks 第8回 A9-2。2026-09-11 受領）。
 *
 * ★★このファイルに DB アクセスを混ぜないこと★★（`CLAUDE.md` §9.4。
 * `lib/clean-logs/query.ts` と同じ流儀）。行の取得は `DataSource.areaCompletions()`、
 * 絞り込み・並べ替え・全体率の計算はここ、という分担にしてある。
 *
 * ★★1エリア = 1清掃ロボット★★（`CLAUDE.md` §8）。
 * ★1回の清掃の中を売り場・通路・レジ前に区切った内訳ではない★ — その粒度は取得できない。
 */
import { fail, type QueryParseResult } from "@/lib/api/query-result";
import {
  isProblemGrade,
  judgeGrade,
  meanCleanRatio,
  type CleanRatioTally,
} from "@/lib/judge";
import type {
  AreaCompletionItem,
  AreaCompletionRow,
  AreaCompletionsResponse,
} from "@/lib/types";

export type AreaFilter = "problem" | "all";

const AREA_FILTERS: readonly AreaFilter[] = ["problem", "all"];

/**
 * `filter` クエリパラメータの解釈。★既定は `"problem"`★（YAML の `default`）。
 *
 * ★指定なし（キーが無い）だけが既定にフォールバックする★ —
 *   ★`filter=`（空文字）は不正値として 422★。`parseCleanLogQuery()` の `result` が
 *   `searchParams.get(...) ?? "all"` で同じ扱いをしているので、それに揃えた
 *   （空文字を既定に寄せると、打ち間違いが黙って既定で動くことになる）。
 */
export function parseAreaFilter(
  searchParams: URLSearchParams
): QueryParseResult<AreaFilter> {
  const raw = searchParams.get("filter") ?? "problem";
  if (!AREA_FILTERS.includes(raw as AreaFilter)) {
    return fail(422, "validation_failed", "filterの値が不正です", {
      filter: ["problem / all のいずれかを指定してください"],
    });
  }
  return { ok: true, query: raw as AreaFilter };
}

/**
 * その清掃ログと ★同じ施設・同じ日★ の行だけを取り出す（★その清掃ログ自身を含む★）。
 *
 * ★`{id}` はエリアを特定するキーではなく、施設と日付を特定するための入口★
 * （指示書Z 決定1）。呼び出し側は `{id}` の行から `facility_id` と JST 日付を取り出して渡す。
 *
 * ★`date` は JST の `YYYY-MM-DD`★ — DB 由来の値は `jstIsoFromDb()` を通してから
 * 先頭10文字を取ること（`start_at` の生の `Date` で突き合わせないこと。D-115）。
 */
export function selectAreaRowsForLog(
  rows: readonly AreaCompletionRow[],
  target: { facility_id: number; date: string }
): AreaCompletionRow[] {
  return rows.filter(
    (r) => r.facility_id === target.facility_id && r.date === target.date
  );
}

/**
 * 並び順の比較: ★完了率の昇順。未実施(null)は先頭。同値は `section_id` の昇順★
 *
 * ★悪いものから見る画面なので、いちばん悪い「未実施」を先頭に置く★（指示書Z 決定10）。
 * ★同値の順を `section_id` で固定するのは、実行ごとに並びが変わらないようにするため★。
 */
function compareByCompletion(
  a: AreaCompletionItem,
  b: AreaCompletionItem
): number {
  if (a.completion_rate === null || b.completion_rate === null) {
    if (a.completion_rate === b.completion_rate) return a.section_id - b.section_id;
    return a.completion_rate === null ? -1 : 1;
  }
  if (a.completion_rate !== b.completion_rate) {
    return a.completion_rate - b.completion_rate;
  }
  return a.section_id - b.section_id;
}

/**
 * ★全体率の材料（分子・分母）★を数える。★全体率の数え方の規則はこの関数だけが持つ★（指示書AR 段階4）。
 *
 * - 分子 `cleanRatioSum` ＝ ★`completion_rate` が `null` でない行の `completion_rate` の合計★
 * - 分母 `cleanRatioCount` ＝ ★その行数★（★未実施＝`completion_rate === null` の行は分子にも分母にも入れない★）
 *
 * ★`buildAreaCompletions()`（全体率を出す）と APIルートの `judge.areas` ログ（分子・分母を残す）の
 *   両方がこれを使う★ — ★ログ側で別の数え方をすると、ログと画面の全体率が食い違う★。
 *   ★割り算はしない★（`meanCleanRatio()` の仕事。件数0で `null`）。★丸めない★（`CLAUDE.md` §4.3）。
 *
 * ★渡すのは `filter` で絞る**前**の行★（`total_count` / `overall_*` と同じ母集団。決定8 / 決定12）。
 */
export function overallCompletionTally(
  rows: readonly AreaCompletionRow[]
): CleanRatioTally {
  let cleanRatioSum = 0;
  let cleanRatioCount = 0;
  for (const r of rows) {
    if (r.completion_rate === null) continue; // ★未実施は分母に入れない★
    cleanRatioSum += r.completion_rate;
    cleanRatioCount += 1;
  }
  return { cleanRatioSum, cleanRatioCount };
}

/**
 * 判定・並べ替え・件数・全体率・絞り込みをまとめてレスポンスの形にする。
 *
 * ★★行も全体も1回ごとの完了率から出す★★（2026-09-30 指示書AQ 決定4 / §5-1・2026-10-02 D-155）
 *
 * | | 出し方 |
 * |---|---|
 * | 各行の `completion_rate` | ★1回ごとの完了率（`runCompletionRate()`。データ層の `completionRateFromReport()` 経由）★ |
 * | `overall_completion_rate` | ★各行の完了率の単純平均（清掃1回＝1票）★。`meanCleanRatio()` |
 *
 * ★★以前は全体だけ面積ベース（Σclean_area ÷ Σtask_area）だった★★（指示書Z 決定7 の「意図した非対称」）。
 *   ★面積比は `clean_ratio` と相関しない★（CSV実測: `clean_area > task_area` が 129/189 行）ため、
 *   ★レポート画面・総清掃面積 F-08・R-3 と同じ `meanCleanRatio()`（`lib/judge/kpi.ts`）で畳む★
 *   （★画面ごとに完了率の式が違う状態をやめる★。§5-1）。
 *   ★面積加重にしないこと★ — 理由は `meanCleanRatio()` の docstring。
 *
 * ★★未実施（`completion_rate === null`）の行は平均の分母に入れない★★ —
 *   0% として混ぜると ★稼働率で既に数えている失敗を完了率でも二重に数える★
 *   （`aggregateCleanStatsByFacility()` と同じ扱い）。★全行が未実施なら `null`★。
 *
 * ★★`total_count` / `problem_count` / `overall_*` は `filter` で絞る**前**に計算する★★
 *   （「問題エリアのみ」を選んだら全体率まで変わる、という挙動にしないため。決定8 / 決定12）。
 *
 * ★★結果待ちは問題に数えない★★（D-173 と同じ。指示書BB A3）— `problem_count` と `filter=problem` は
 *   `isProblemArea()` の1本で判定する。`total_count` には数え、平均は元から null を分母に入れない。
 *
 * ★閾値は直書きしない★ — 判定は `judgeGrade()`、問題判定は `isProblemGrade()`、
 *   全体率は `meanCleanRatio()`（件数0で `null`）に任せる（`CLAUDE.md` §7）。
 *   ★★丸めないこと★★ — 表示の丸めは画面側（`CLAUDE.md` §4.3）。
 *
 * ★未実施は `completion_rate: null`（`0` と混ぜない。決定4）★。
 *   ★面積（`clean_area` / `task_area`）はこの関数では読まない★（完了率の計算に使わないため）。
 *   ★全体率の分子・分母の数え方は `overallCompletionTally()`★（ログと共用）。
 */
export function buildAreaCompletions(
  rows: readonly AreaCompletionRow[],
  filter: AreaFilter
): AreaCompletionsResponse {
  const items: AreaCompletionItem[] = rows
    .map((r) => ({
      section_id: r.section_id,
      section_name: r.section_name,
      completion_rate: r.completion_rate,
      grade: judgeGrade(r.completion_rate),
      result_pending: r.result_pending,
    }))
    .sort(compareByCompletion);

  const problemCount = items.filter(isProblemArea).length;

  // ★Σ と件数は `overallCompletionTally()` で数え、割るのは `meanCleanRatio()` に任せる★
  //   （自前で割らない。★`judge.areas` ログも同じ関数で数える★。指示書AR 段階4）
  const overall = meanCleanRatio(overallCompletionTally(rows));

  return {
    overall_completion_rate: overall,
    overall_grade: judgeGrade(overall),
    total_count: items.length,
    problem_count: problemCount,
    items: filter === "problem" ? items.filter(isProblemArea) : items,
  };
}

/**
 * ★問題エリアか★（`problem_count` と `filter=problem` が共有する1本。指示書BB A3・D-182）—
 *   判定が △・× で、★結果待ち（D-173）でない★こと。結果待ちは `grade: "×"` のままなので、旗を見ないと問題に入ってしまう
 *   （ダッシュボードの `problem_logs`・`/clean-logs` の `result=problem` と同じ扱い）。
 */
export function isProblemArea(item: AreaCompletionItem): boolean {
  return !item.result_pending && isProblemGrade(item.grade);
}
