import { ESCALATION_THRESHOLDS, GRADE_THRESHOLDS } from "@/lib/config";
import type { AdviceRuleId } from "@/lib/config/advice-rules";
import {
  B2_HANDLE_UP_STATUSES,
  B3_OFFLINE_STATUSES,
} from "@/lib/config/robot-status-match";
import { judgeGrade } from "./grade";

/**
 * 対応アドバイスの発火判定（Phase A: R1〜R4 ＋ Phase B: B1〜B3）
 *
 * ★ここは「どのルールが該当するか」だけを決めます★
 *   表示する文言は `lib/config/advice-rules.ts` の既定値と、
 *   運用設定での上書き（`lib/settings/advice.ts`）から取得します。
 *   文言を変えてもこの関数とテストは影響を受けません。
 *
 * | # | 条件 |
 * |---|---|
 * | R1 | 出発失敗（初回） |
 * | R2 | 完了率×（10%未満） |
 * | R3 | 同一施設で2日以上連続△以下 |
 * | R4 | 同一施設で3日以上連続（出発失敗 or △以下） |
 * | B1 | 完了率10%以上70%未満（マップ更新の確認） |
 * | B2 | ハンドルアップ検知 |
 * | B3 | オフライン検知 |
 *
 * ★B2/B3 の発火条件は `lib/config/robot-status-match.ts` の設定値。値は暫定★（D-078 項2）
 *
 * ★「連続」はスケジュール存在日の連続★（D-026）
 *   `consecutiveProblemDays` には、データ取得側がスケジュール存在日ベースで
 *   算出済みの連続日数を渡してください（どの日がスケジュール存在日かの判断は
 *   データ取得側の責任です）。
 *
 * 複数該当した場合はすべて返します（モックの挙動を踏襲）。
 * 行バッジの色は `maxAdviceLevel()` で決めます。
 */
export interface AdviceInput {
  /** スケジュール通り出発したか */
  departed: boolean;
  /** 清掃完了率(%)。未実施は null。★丸める前の生値★ */
  completionRate: number | null;
  /** スケジュール存在日ベースの連続問題日数 */
  consecutiveProblemDays: number;
  /**
   * ★事前チェック1回目（早い方）★のステータス（API の `status_1h` そのまま）。
   * ★名前は旧来の `status1h` のままだが、60分前とは限らない★（列は分数ではなく順番で割り当てる。D-178）。
   * B2 / B3 は1回目・2回目のどちらかが一致すれば発火するので、何分前かは判定に関係しない。
   * ★`null` ＝ 1回目の事前チェックの予定が無い★（ユーザー指示 2026-10-07。D-171）。★null はどのルールにも当たらない（例外にしない）★
   */
  status1h?: string | null;
  /** ★事前チェック2回目（遅い方）★のステータス（API の `status_30m`。30分前とは限らない。D-178）。★`null` の扱いは `status1h` と同じ★（D-171） */
  status30m?: string | null;
}

/**
 * @param thresholds 連続日数の閾値（既定は `ESCALATION_THRESHOLDS`）。★R1・R3・R4 はこれを使い、直書きしない★
 *   （指示書BB A13・D21。`level2Days = 3` のテストを `vi.mock` なしで書くため。`isResultPending()` の `waitMinutes` と同じ作法）
 */
export function matchAdviceRules(
  input: AdviceInput,
  thresholds: { level2Days: number; level3Days: number } = ESCALATION_THRESHOLDS
): AdviceRuleId[] {
  const matched: AdviceRuleId[] = [];
  const {
    departed,
    completionRate,
    consecutiveProblemDays,
    status1h,
    status30m,
  } = input;

  // R1: 出発失敗（初回）。★2日目（level2Days）以降は R3 / R4 が担当（閾値は設定から。直書きしない — 指示書BB A13）★
  if (!departed && consecutiveProblemDays < thresholds.level2Days) {
    matched.push("R1");
  }

  // R2: 完了率×（10%未満）。出発しなかった場合は completionRate が null なので対象外
  if (completionRate !== null && completionRate < GRADE_THRESHOLDS.poor) {
    matched.push("R2");
  }

  // R3: 2日以上連続
  if (consecutiveProblemDays >= thresholds.level2Days) {
    matched.push("R3");
  }

  // R4: 3日以上連続。R3と同時に出ることがある（段階的な対応を促すため意図的）
  if (consecutiveProblemDays >= thresholds.level3Days) {
    matched.push("R4");
  }

  // B1: △判定（完了率10%以上70%未満）のときにマップ更新を疑う
  if (completionRate !== null && judgeGrade(completionRate) === "△") {
    matched.push("B1");
  }

  // B2 / B3: 事前チェックのステータス由来
  if (matchesStatus(B2_HANDLE_UP_STATUSES, status1h, status30m)) {
    matched.push("B2");
  }
  if (matchesStatus(B3_OFFLINE_STATUSES, status1h, status30m)) {
    matched.push("B3");
  }

  return matched;
}

/**
 * ロボット状態が発火条件にマッチするか。
 * ★前後の空白を trim し、大小文字を無視して比較する★（表記ゆれ対策。D-078 項2）。
 * ★マッチ配列が空なら常に false★（＝値が未判明のルールは発火しない）。
 * ★`null`（事前チェックの予定が無い。D-171）と `undefined` はマッチしない★。
 */
function matchesStatus(
  statuses: readonly string[],
  ...values: Array<string | null | undefined>
): boolean {
  if (statuses.length === 0) return false;
  const normalized = statuses.map((s) => s.trim().toLowerCase());
  return values.some(
    (v) =>
      v !== undefined && v !== null && normalized.includes(v.trim().toLowerCase())
  );
}

/** 該当ルールの最大レベル。行バッジの色分けに使う。該当なしは0 */
export function maxAdviceLevel(
  rules: Array<{ level: number }>
): number {
  return rules.reduce((max, r) => Math.max(max, r.level), 0);
}
