import { CLEAN_TASK_STATUS, FACILITY_STATUS_THRESHOLDS, RUN_COMPLETION_RATE } from "@/lib/config";
import type { FacilityStatus, KpiValue } from "@/lib/types";

/**
 * KPI4指標の計算（CLAUDE.md 4.2 / D-021）
 *
 *   稼働率       = 正常終了（status = 4）の回数 ÷ 清掃予定回数 × 100   ← ★第11回 P-6=A★
 *   出発成功率   = 開始できた回数（レポート行がある回数）÷ 清掃予定回数 × 100   ← ★時刻の条件なし（P-6=A）★
 *   清掃完了率   = Σ 1回ごとの完了率 ÷ 1回ごとの完了率が null でない清掃の回数（1回ごとの完了率 = `runCompletionRate()`）
 *   エラー発生率 = エラー発生回数 ÷ 清掃予定回数 × 100   ← ★分母は予定回数（P-6=B）★
 *
 * ★★3つの率はサマリ（`operation_ratio` / `launch_ratio` / `error_ratio`）と同じ式★★（第11回 P-6。D-158）。
 *   旧: 稼働率 ＝ 実施回数 ÷ 予定・出発成功率 ＝ 定刻に出発した回数 ÷ 予定・エラー発生率 ＝ エラー ÷ 実施回数。
 *
 * ★★清掃完了率は D-155 で TeamWorks のサマリと同じ式にした（第11回で確定。D-158）★★（第10回 N-3。2026-10-02）—
 *   ★1回ごとの完了率を決めるのは `runCompletionRate()`★、★それを畳む（単純平均する）のは
 *   `meanCleanRatio()`★。どちらも1関数だけ。
 *   ★期間の合計どうしを割る面積比（Σ清掃面積 ÷ Σ清掃対象面積）は D-151 で廃止したまま★（使わない）。
 *   ★旧: D-151（2026-09-30）では `clean_task_reports.clean_ratio` 列の単純平均だった★が、
 *   TeamWorks から「列の計算式が間違っている」と回答があった（第10回 N-3）ため、列は完了率に使わない。
 *
 * ★戻り値は丸める前の生値★
 *   表示するときだけ `displayRate()` で整数に丸めます。
 *
 * 分母が0のときは null を返します（0%ではない）。
 * 「予定が1件もない期間」と「予定はあったが全部失敗した」は区別する必要があるためです。
 */

/** 分母0を null にする除算 */
function rate(numerator: number, denominator: number): number | null {
  if (denominator === 0) return null;
  return (numerator / denominator) * 100;
}

/** 稼働率 = 正常終了（`status = 4`）の回数 ÷ 清掃予定回数（P-6=A。分子は `isCompletedTask()` で数える） */
export function operationRate(completed: number, planned: number): number | null {
  return rate(completed, planned);
}

/**
 * 出発成功率 = 開始できた回数（レポート行がある回数）÷ 清掃予定回数。★時刻の条件なし（P-6=A）★。
 * 旧: 「定刻に出発した回数 ÷ 予定」と書いていたが、実際は時刻を見ていなかった。
 */
export function launchRate(started: number, planned: number): number | null {
  return rate(started, planned);
}

/**
 * ★その清掃が正常終了したか★（稼働率の分子。第11回 P-6=A。送付版 `:87`）。
 *
 * - ★status だけで決める。面積は見ない★（指示書AU 表2 K8: `clean_area = 0` でも status が正常終了なら数える）
 * - ★「最新の1行」で見る★ — 呼び出し側が `latestReportByTimetableId()` で選んだ行を渡す
 * - ★◎○△× の判定には使わない★（D-155 項2 は変わらない。判定は完了率で決める）
 * - ★正常終了かを決める述語はこれ1つだけ★（指示書AU D5。`lib/db/` の中で `report.status === …` と書かない）
 * - 構造的な型にしてある（`lib/judge/` は `lib/db/` を import しない。`runCompletionRate()` と同じ作法）
 *
 * @param report `clean_task_reports` の1行（無ければ undefined ＝ 未実施）
 */
export function isCompletedTask(report: { status: number } | undefined): boolean {
  return report !== undefined && report.status === CLEAN_TASK_STATUS.completed;
}

/**
 * ★`meanCleanRatio()` の入力★ — ★1回ごとの完了率（`runCompletionRate()` の戻り）の**合計**と、その件数★。
 *
 * ★名前（`cleanRatioSum` / `cleanRatioCount`）は変えていない★（指示書AS D7）— 中身は
 *   ★`clean_task_reports.clean_ratio` 列ではない★。名前を変えると16ファイルの機械的な差分が
 *   中身の変更に混ざってレビューで読めなくなるため。暫定の式なので、戻すときにも名前を戻さずに済む。
 *
 * ★★平均そのものを持ち回らない理由★★ — ★率は加重平均できない★（`CLAUDE.md` §4.7 の
 *   「率は加重平均できないため合算できない」と同じ話）。★Σ と件数を別々に持てば、
 *   施設・屋号・ホールディングスをまたいで合算しても平均が壊れない★。
 *
 * ★構造的な型にしてある★ — `CleanStatsBucket`（`lib/db/report-mapping.ts`）はこの形を
 *   そのまま満たす。★`lib/judge/` から `lib/db/` を import しないため★（判定層はDBの都合を知らない）。
 */
export interface CleanRatioTally {
  /** ★1回ごとの完了率（`runCompletionRate()` の戻り）の合計★（百分率の和。★平均側では丸めない★） */
  cleanRatioSum: number;
  /**
   * 上の合計に足した件数（＝平均の★分母★）。
   * ★未実施の清掃は数えない★（`task_area = 0` は 0% として数える。P-5=B）
   */
  cleanRatioCount: number;
}

/**
 * ★清掃完了率の畳み方＝1回ごとの完了率の「清掃1回 = 1票」の単純平均★（指示書AQ 決定4 / §5-1・
 * D-155。★丸めない生値★。1回ごとの完了率は `runCompletionRate()`）。
 *
 * ★★完了率を複数行から出すときは、画面を問わず必ずこの関数を通すこと★★ —
 *   レポート画面（`lib/db/report-mapping.ts`）・総清掃面積 F-08（`lib/clean-area/aggregate.ts`）・
 *   エリア別完了率の全体率（`lib/clean-logs/areas.ts`）・要レスキューの急落判定 R-3
 *   （`lib/judge/rescue.ts`）が共用する。★2箇所で別々に畳むと、同じ施設が
 *   レポート画面で78%・F-08で120%と出る★（指示書AQ §5-1。出所: ユーザー提供CSV。
 *   期間の合計どうしの面積比90.2% ↔ `clean_ratio` 列の平均78.5%）。
 *
 * ★★なぜ単純平均で、面積加重にしないのか★★（出所を明記する。`CLAUDE.md` §6-6）:
 *   - ★加重に使える面積が信用できない★（★1回ごとに上限100で切る `runCompletionRate()` とは別の話★）
 *     — ユーザー提供CSV `clean_task_reports.csv`（189行。
 *     2026-09-30 受領）で ★`clean_area > task_area` が 129/189（68.3%）★、
 *     個別行で ★面積比 13.8% ↔ `clean_ratio` 100.0%★ / ★面積比 162.8% ↔ `clean_ratio` 61.0%★
 *     という乖離がある（指示書AQ 決定4の根拠表）。★`task_area` で重みを付けると、
 *     廃止したはずの面積が別の経路で完了率に効いてしまう★。
 *   - ★施設をまたいだ合算と一致する★ — Σ と件数を持ち回るので、
 *     「施設ごとの平均を平均する」ことにはならない（`sumCleanStatsBuckets()` が正確）。
 *
 * ★分母0（`cleanRatioCount === 0`）は `null`★ — 「0%で清掃した」ではなく
 *   ★「率を出せる清掃が1回も無い」★（このファイルの他の率と同じ規則）。
 *
 * ★★`rate()` を通さないこと★★ — あちらは `× 100` する。
 *   1回ごとの完了率は ★既に百分率★ なので、通すと ★百分率をもう一度100倍してしまう★。
 *
 * ★★面積比の `completionRate(cleanedArea, targetArea)` は削除した★★（2026-09-30）—
 *   ★期間の合計どうし（Σ`clean_area` ÷ Σ`task_area`）で割らないこと★（D-151 で廃止したまま）。
 *   ★1回ごとに割るのは `runCompletionRate()` の仕事★（D-155）。
 *   ★面積の合計（`clean_area` / `task_area`）そのものは総清掃面積 F-08 の表示に残る★（指示書AQ §1-3）。
 *
 * ★2026-09-30 に `lib/db/report-mapping.ts` から移した★（`CLAUDE.md` §9.1: KPI計算は `lib/judge/`）。
 *   挙動は移す前と同じ。
 */
export function meanCleanRatio(tally: CleanRatioTally): number | null {
  if (tally.cleanRatioCount === 0) return null;
  return tally.cleanRatioSum / tally.cleanRatioCount;
}

/**
 * 浮動小数の誤差とみなす桁（指示書AS D3）。★1e-6 ポイント未満の差は誤差として吸収する★。
 * 仕様の閾値ではなく計算上の都合なので `lib/config` には置かない（`RUN_COMPLETION_RATE` が式の部品）。
 */
const FLOAT_ERROR_DIGITS = 5;

/**
 * ★★1回の清掃の完了率（％）★★ — ★1回ごとの完了率を決めるのはこの関数だけ★（指示書AS D1）。
 *
 *   `min(clean_area ÷ task_area × 100, 100)` を ★小数第1位まで切り捨て★（`RUN_COMPLETION_RATE`）
 *
 * ★出所★: TeamWorks 第10回 N-3（サマリの式）。★当期（アプリがその場で集計する分）も同じ式にする★
 *   — 式が違うと、前期比（サマリ）と当期（アプリ）の差が別の式どうしの差になる（D-155 項1）。
 *
 * ★★確定★★（第11回 P-2=A「今の式のまま」・P-1「訂正後の `clean_ratio` 列もこの式と同じ値」。D-158）。
 *   旧: 暫定（D-155。ユーザー判断「一旦B」）。★`clean_ratio` 列に戻すときは、この関数の中身だけを直す★。
 *   そのため ★呼び出し側は行（`clean_ratio` も持っている）をそのまま渡すこと★
 *   （呼び出し側で `clean_area` / `task_area` だけに分解しない。指示書AS D6）。
 *
 * - ★`task_area === 0` は `0`★（P-5=B。第11回。D-158）。★`clean_area > 0` でも `0`★（割り算をしない。
 *   `10 / 0 = Infinity` を上限で 100 にしないため、★割る前に★見る）。旧: `null`（分子にも分母にも入れない。D-155 の暫定）。
 *   `task_area` は NOT NULL なので null の扱いは要らない
 * - ★`clean_area = 0`（`task_area > 0`）は `0`★ —「0%で実施」であって未実施（行が無い）ではない
 * - ★切り捨ての前に浮動小数の誤差を吸収する★（D3）— `0.57 / 1 * 100` は `56.99999999999999` になり、
 *   そのまま切り捨てると 56.9 になる（表1 R6）。★`Math.floor(raw * 10) / 10` と書かないこと★。
 *   ★`toFixed()` は四捨五入なので使わない★
 * - ★切り捨ては1回ごとの値だけ★ — 平均（`meanCleanRatio()`）は丸めない（D5。`CLAUDE.md` §4.3）
 *
 * ★★Σ面積 ÷ Σ面積（D-151 で廃止）とは別物★★ — こちらは ★1回ずつ割って上限100で切り★、
 *   それを清掃1回＝1票で平均する。面積で重み付けもしない。
 *
 * ★承知している弱点（変えないこと）★: `clean_area` は同じ場所を複数回清掃した面積の総和
 *   （第10回 N-4）。ユーザー提供CSV 189行では 68.3% の行が `clean_area > task_area` で、
 *   ★それらは全部 100 になる（◎が多めに出る）★。ユーザーはこれを承知で採用した（D-155 項1）。
 *   ★上限100を外したり、別の補正を入れたりしないこと★。
 *
 * @param report `clean_task_reports` の1行（構造的な型。`lib/judge/` は `lib/db/` を import しない）
 * @returns 1回ごとの完了率（小数第1位まで）
 */
export function runCompletionRate(report: {
  clean_area: number;
  task_area: number;
}): number {
  if (report.task_area === 0) return 0;
  const { cap, decimals } = RUN_COMPLETION_RATE;
  const raw = Math.min((report.clean_area / report.task_area) * 100, cap);
  const absorbed = Math.round(raw * 10 ** (decimals + FLOAT_ERROR_DIGITS)) / 10 ** FLOAT_ERROR_DIGITS;
  return Math.floor(absorbed) / 10 ** decimals;
}

/**
 * エラー発生率 = エラー発生回数 ÷ ★清掃予定回数★（P-6=B。サマリの `error_count ÷ task_count` と同じ）。
 * ★上限で切らない★（指示書AU D9。1回の清掃に複数のエラーが紐づきうるので 100 を超えうる）。
 * 旧: 分母は実施回数。
 */
export function errorRate(errors: number, planned: number): number | null {
  return rate(errors, planned);
}

/**
 * 前期比つきのKPI値を組み立てる。
 *
 * 前期データがない（新規施設など）場合、prev と diff は null になり、
 * 画面では▲▼を表示しません。
 *
 * `value` も nullable です。上の4関数は分母0で null を返すため
 * （全施設定休日の週など「予定が1件もない期間」）、その値をそのまま渡せる必要があります。
 * **`value` / `prev` のどちらかが null なら `diff` も null**（差を取れないので0にしない）。
 */
export function toKpiValue(value: number | null, prev: number | null): KpiValue {
  return {
    value,
    prev,
    diff: value === null || prev === null ? null : value - prev,
  };
}

/**
 * 施設別パフォーマンスの 良好 / 注意 / 要改善 / 判定なし
 *
 * 算出基準は**稼働率ベース**。これは DFA 側の UI 判断であって、TeamWorks 待ちではない
 * ★当期の稼働率は P-6 で『正常終了 ÷ 予定』になった（以前は出発成功率と同じ値）。当期の施設ステータスは以前より低めに出る★
 * （閾値は据え置き。指示書AU §11 Q8）
 * （モックの 80 / 90 という区切りを、既存の FACILITY_STATUS_THRESHOLDS に寄せたもの）。
 * ★TeamWorks への照会は行っていない★ — 第2回〜第8回の確認書のどこにも該当する質問は無い
 * （2026-09-11 に全文検索で確認。ヒット0）。以前ここに「回答待ち」と書かれていたのは誤りで、
 * 来ない回答を次の人が待つ状態になっていた。
 * 見直すきっかけは「TeamWorks の回答が来たとき」ではなく、
 * **別の算出基準を採るという判断が出たとき**。
 * 閾値は `lib/config` の FACILITY_STATUS_THRESHOLDS から読む。
 *
 * `null`（分母0＝その期間に清掃予定が1件もない）は**評価できない**ので「判定なし」にする
 * （決定1。2026-09-11 ユーザー確定）。`clean_timetables` には1週間分の予定がまとめて
 * 登録される（D-106）ため、週の初め・月の初めは「予定はあるが実施0件」が正常な状態になる。
 * 以前はここを「要改善」に寄せていたが、それだと週の初めに全施設が赤い「要改善」バッジに
 * なってしまうため区別した。
 */
export function judgeFacilityStatus(
  operationRateValue: number | null
): FacilityStatus {
  if (operationRateValue === null) return "no_data";
  if (operationRateValue >= FACILITY_STATUS_THRESHOLDS.good) return "good";
  if (operationRateValue >= FACILITY_STATUS_THRESHOLDS.warning) return "warning";
  return "bad";
}
