/**
 * 清掃面積の★番人★のログ出力（指示書AN ★段階3★ / §2-5 / §6-3。落とし穴2）。
 *
 * ------------------------------------------------------------------------
 * ★★何を見張っているのか★★
 * ------------------------------------------------------------------------
 * ```
 * 1つの clean_timetable に clean_task_reports が2行以上あると、
 * SUM() が同じ清掃の面積を2回数える。
 * ★例外もテスト失敗も出ない。数字が大きくなるだけ。★
 * ```
 * （★旧: `clean_status_histories`。2026-09-30 に集計元を `clean_task_reports` に差し替えた。
 *   指示書AQ 決定2 / 決定3★）
 * 旧取得元の実データは全538行が1対1でしたが、★既存コードは「最新1行に畳んでから足す」実装★
 * （`latestReportByTimetableId()`。旧 `latestHistoryByTimetableId()`）＝ ★N行を想定しています★。
 * ★`clean_timetable_id` が NULL の行は見張りの対象外★（数え方の正は
 *   `countDuplicatedTimetables()`。指示書AQ 罠1 / §5-3）。
 * ★TeamWorks に照会中★（第9回 Q2-3）なので、★静かに壊れる代わりにログに出す★。
 *
 * もう1つ、`aggregateCleanArea()` が返す ★`unmatched_facility_ids`★
 * （施設マスタに無い `facility_id`）も同じ扱いにしてあります —
 * ★その面積は内訳にも全体合計にも入らない★ので、★黙って消さない★。
 *
 * ------------------------------------------------------------------------
 * ★★なぜ `lib/db/` ではなくここで `logEvent()` を呼ぶのか★★
 * ------------------------------------------------------------------------
 * `DataAnomalyLogEvent` は ★`route` / `method` が必須★でした（★D-178 で任意になった★。2026-10-16。
 * 詳細ログの事前チェックが3つ以上のときにデータ層から出すため）が、
 * ★データ層（`lib/db/`）はどのルートから呼ばれたかを知りません★。
 * ★不明な情報を書かない★のがこのリポジトリの作法で、同じ理由から
 * `lib/settings/advice.ts` は `route` / `method` を載せずにログを出しています。
 * ★ここではルートを載せられる層で出す設計を保つ★（型が任意になっても、ルートが分かるなら載せる）。
 *
 * ★★そこで「数えるのはデータ層・出すのはルート層」に分けてあります★★:
 *   - 数える … `DataSource.cleanArea()`（`duplicated_timetable_count`）
 *   - 出す   … ★この関数★（ルートが `ROUTE` / `"GET"` を渡す）
 * ★段階2 の `unmatched_facility_ids` が「戻り値で返して呼び出し側に出させる」設計なので、
 *   2つの異常を別々の層から出さないためにも、こちらに揃えてあります★。
 *
 * ★★ルート（段階4）は必ずこの関数を呼ぶこと★★ — 呼ばないと番人は何も言いません
 *   （★呼び忘れても例外もテスト失敗も出ない★ので、`CleanAreaSourceResult` の
 *   docstring にも同じ注意を書いてあります）。
 *
 * ★★原因の断定を書かないこと★★（`docs/logging-design.md` §7）—
 *   「TeamWorks の収集処理が二重登録している」のような推測を `metric` にも入れない。
 *   ★期待値（0）と実測だけを残す★（`app/api/dashboard/route.ts` の `data.anomaly` と同じ形）。
 */
import { logEvent } from "@/lib/logger";
import type { DataAnomalyLogEvent } from "@/lib/logger/events";

/**
 * ★`metric` の文字列はここ1箇所★ — ログを grep する側と実装で綴りがズレないようにする。
 *
 * ★テーブル名と数え方がそのまま読める名前にすること★
 * （`clean_log_rows_vs_scheduled`（D-049）と同じ流儀）。
 */
export const CLEAN_AREA_ANOMALY_METRICS = {
  /**
   * 1つの `clean_timetable_id` に `clean_task_reports` が2行以上ある件数（★NULL の行は数えない★）。
   * ★2026-10-01 に改名した★（ユーザー指示。D-151）。旧: `"clean_status_history_rows_per_timetable"`
   *   — 取得元を `clean_task_reports` に替えた後も旧テーブル名のままで、★警告を見た人が
   *   旧テーブルを調べに行ってしまう★ため。★改名の時点でこの `metric` を集計・監視している側は
   *   無かった★（出力先は標準出力のみ。`lib/logger/index.ts`）。
   * ★本番稼働後は改名しないこと★ — この名前で警告を拾う監視が作られると、改名した瞬間に
   *   その監視が黙って反応しなくなる。
   */
  duplicatedTimetables: "clean_task_report_rows_per_timetable",
  /** 施設マスタに無い `facility_id`（その面積は合計にも内訳にも入らない）の件数 */
  unmatchedFacilities: "clean_area_facility_ids_without_master",
} as const;

export interface CleanAreaAnomalyInput {
  /** 呼び出し元のルート（例: `/api/clean-area`）。★データ層は知らないのでルートが渡す★ */
  route: string;
  method: string;
  /** `DataSource.cleanArea()` の `duplicated_timetable_count` */
  duplicated_timetable_count: number;
  /** `aggregateCleanArea()` の `unmatched_facility_ids` */
  unmatched_facility_ids: readonly number[];
}

/**
 * 異常が1つも無ければ★1行も出さない★（0件を毎回 info で出すとログが埋まる）。
 *
 * @param emit テストから差し替えるための出口（★本番コードは省略して `logEvent` を使う★。
 *   `lib/settings/advice.ts` の `onWarn` と同じ作法）。
 */
export function reportCleanAreaAnomalies(
  input: CleanAreaAnomalyInput,
  emit: (event: DataAnomalyLogEvent) => void = (event) => logEvent("warn", event)
): void {
  if (input.duplicated_timetable_count > 0) {
    emit({
      event: "data.anomaly",
      route: input.route,
      method: input.method,
      metric: CLEAN_AREA_ANOMALY_METRICS.duplicatedTimetables,
      // ★「2行以上ある `clean_timetable_id` は0件であってほしい」という期待値★
      expected: 0,
      actual: input.duplicated_timetable_count,
    });
  }
  if (input.unmatched_facility_ids.length > 0) {
    emit({
      event: "data.anomaly",
      route: input.route,
      method: input.method,
      metric: CLEAN_AREA_ANOMALY_METRICS.unmatchedFacilities,
      expected: 0,
      // ★施設IDそのものは載せない★ — 件数で異常の有無は分かり、
      //   ★IDの列挙は件数が増えるとログを埋める★（必要ならDBを引き直せる）
      actual: input.unmatched_facility_ids.length,
    });
  }
}
