/**
 * レポート画面の4ブロックを ★1本ずつ★ 出すための判定（指示書AL ★段階2★ / §3）。
 *
 * ------------------------------------------------------------------------
 * ★★なぜ画面コンポーネントの中に書かずに `lib/` へ出すのか★★
 * ------------------------------------------------------------------------
 *   `vitest.config.ts` の `include` は `lib/**` と `app/**` なので、
 *   ★`components/` に置いたテストは走りません★（D-120 項2 / 指示書AL §3-5）。
 *   ★指示書AJ で `lib/dashboard/rescue-visibility.ts` を切り出したのと同じ形★です。
 *
 * ------------------------------------------------------------------------
 * ★★何を決めているか★★
 * ------------------------------------------------------------------------
 * | # | 状況 | 期待（指示書AL §3-5） |
 * |---|---|---|
 * | 2-1 | 4本すべて成功 | 4ブロックとも出す |
 * | ★2-2★ | ★KPIだけ422★ | ★残り3ブロックを出す★＋KPIは「顧客を選んでください」 |
 * | 2-3 | KPIだけ500 | 残り3ブロックを出す＋KPIのブロックにエラー |
 * | ★2-4★ | ★4本とも失敗★ | ★★画面全体を1行のエラーにする★★ |
 * | 2-5 | 施設別だけ失敗 | 残り3ブロックを出す |
 *
 * ★2-4 だけ「画面全体」なのは、★4つとも空の枠が並ぶより1行のほうが分かる★から
 *   （指示書AL §3-3 の監督決定）。
 *
 * ------------------------------------------------------------------------
 * ★★「顧客を選んでください」に振り分ける条件★★
 * ------------------------------------------------------------------------
 * ★`status === 422` だけでは足りない★ — `/reports/kpi` の 422 には
 * ★理由が2つある★（`app/api/reports/kpi/route.ts` の冒頭）:
 *
 *   1. パラメータが不正（`details` は `{ week: ["..."] }` のような ★文字列の配列★）
 *   2. 対象顧客が1社に定まらない（`details` は `{ customer_ids: [1, 2] }` = ★数値の配列★）
 *
 * ★だから `details.customer_ids` が ★数値の配列として実際に入っているか★ で振り分ける★。
 * ★文言では判定しないこと★ — EN 切替やサーバ側の言い換えで壊れる
 * （`details` を機械可読にした理由そのもの。`lib/types/index.ts` の `ApiErrorDetails`）。
 *
 * ★純粋関数にすること★（`CLAUDE.md` §9.4）— fetch もログも state も混ぜない。
 */
import { ApiFetchError } from "@/lib/api/client";
import type { ApiErrorDetails } from "@/lib/types";

/** 422 が候補顧客を返すときのキー。★ルートと画面で同じ文字列を見るため定数にする★ */
export const CUSTOMER_IDS_DETAIL_KEY = "customer_ids";

/** 1本ぶんの失敗。★`ApiFetchError` をそのまま持ち回らない★（判定に要るのはこの3つだけ） */
export interface ReportFetchFailure {
  /** HTTP ステータス。★ネットワーク到達不能などレスポンスが無い場合は 0★（`ApiFetchError` と同じ） */
  status: number;
  /** サーバが返した文言。`ApiError` として読めなかった場合は null（画面の既定文言に落とす） */
  serverMessage: string | null;
  details: ApiErrorDetails;
}

export type ReportFetchOutcome<T> =
  | { ok: true; value: T }
  | { ok: false; failure: ReportFetchFailure };

/**
 * `Promise.allSettled()` の1件を結果に変換する。
 *
 * ★`ApiFetchError` 以外の例外（想定外のバグ）も失敗として扱う★ —
 *   ここで投げ直すと ★1本の想定外が4本ぶんの表示を巻き添えにする★（＝直したい元の症状）。
 *   ★`status: 0` にするのは `ApiFetchError` のネットワーク失敗と同じ扱い★で、
 *   422 判定に混ざらない値だから（0 は「レスポンスが無い」の意）。
 */
export function toFetchOutcome<T>(
  result: PromiseSettledResult<T>
): ReportFetchOutcome<T> {
  if (result.status === "fulfilled") {
    return { ok: true, value: result.value };
  }
  if (result.reason instanceof ApiFetchError) {
    return {
      ok: false,
      failure: {
        status: result.reason.status,
        serverMessage: result.reason.serverMessage,
        details: result.reason.details,
      },
    };
  }
  return {
    ok: false,
    failure: { status: 0, serverMessage: null, details: null },
  };
}

/**
 * `details.customer_ids` を ★数値の配列として読めたときだけ★ 返す。読めなければ null。
 *
 * ★`Array.isArray` だけで済ませないこと★ — `details` は3つの形の union（`ApiErrorDetails`）で、
 *   パラメータ不正の 422 は ★文字列の配列★（`{ week: ["..."] }`）を返す。
 *   キー名が一致しても中身が文字列なら「候補顧客」ではない。
 *
 * ★空配列は null ではなく `[]` を返す★ — ★「候補が0社」も意味のある答え★だから
 *   （`group_id` 指定で傘下0社のとき。`lib/reports/target-customer.ts` の docstring）。
 */
export function customerIdCandidates(details: ApiErrorDetails): number[] | null {
  if (details === null) return null;
  const raw: unknown = (details as Record<string, unknown>)[
    CUSTOMER_IDS_DETAIL_KEY
  ];
  if (!Array.isArray(raw)) return null;
  if (!raw.every((id) => typeof id === "number" && Number.isFinite(id))) {
    return null;
  }
  return raw as number[];
}

/** 1ブロックの見せ方 */
export type ReportBlockView =
  /** 取得できた。中身を出す */
  | { kind: "ready" }
  /** 失敗した。★そのブロックの中に★ メッセージを出す（どれが欠けたか分かるように） */
  | { kind: "error"; message: string | null }
  /**
   * 対象顧客が定まらない（KPIの 422）。★「選んでください」へ誘導する★。
   * ★`candidateCustomerIds` を画面へ渡す★ — ★件数を出す／セレクトを強調する等、
   * 「文言を読ませるだけ」で終わらせないため★（指示書AL §3-4）。
   */
  | {
      kind: "select-customer";
      message: string | null;
      candidateCustomerIds: number[];
    };

export interface ReportBlockOutcomes {
  kpi: ReportFetchOutcome<unknown>;
  facilityPerformance: ReportFetchOutcome<unknown>;
  continuousProblems: ReportFetchOutcome<unknown>;
  errorBreakdown: ReportFetchOutcome<unknown>;
}

export interface ReportBlocksView {
  /** ★4本とも失敗★。true のときは個別ブロックを出さず、画面全体を1行のエラーにする */
  allFailed: boolean;
  /**
   * `allFailed` のときに出す1行の文言。null なら画面側の既定文言を使う。
   * ★最初に見つかった「サーバが返した文言」を採る★ — `Promise.all` だったころの
   * 「最初に失敗した1本の文言を出す」と同じ見え方に寄せている。
   */
  message: string | null;
  kpi: ReportBlockView;
  facilityPerformance: ReportBlockView;
  continuousProblems: ReportBlockView;
  errorBreakdown: ReportBlockView;
}

/** KPI 以外の3本。★422 の特別扱いをしない★（顧客が定まらない話はKPIだけ） */
function plainView(outcome: ReportFetchOutcome<unknown>): ReportBlockView {
  return outcome.ok
    ? { kind: "ready" }
    : { kind: "error", message: outcome.failure.serverMessage };
}

function kpiView(outcome: ReportFetchOutcome<unknown>): ReportBlockView {
  if (outcome.ok) return { kind: "ready" };
  const { status, serverMessage, details } = outcome.failure;
  if (status === 422) {
    const candidates = customerIdCandidates(details);
    // ★`customer_ids` が読めた 422 だけを誘導にする★ —
    //   パラメータ不正の 422（`{ week: ["..."] }`）は普通のエラー表示のまま
    if (candidates !== null) {
      return {
        kind: "select-customer",
        message: serverMessage,
        candidateCustomerIds: candidates,
      };
    }
  }
  return { kind: "error", message: serverMessage };
}

/**
 * 4本の結果から「どのブロックをどう出すか」を決める。
 *
 * @param outcomes 4本ぶんの結果（`toFetchOutcome()` で作る）。★中身の型は見ない★ので `unknown` で足りる
 */
export function resolveReportBlocks(
  outcomes: ReportBlockOutcomes
): ReportBlocksView {
  // ★順序に意味がある★（`message` の採用順 = 画面の並び順）
  const ordered = [
    outcomes.kpi,
    outcomes.facilityPerformance,
    outcomes.continuousProblems,
    outcomes.errorBreakdown,
  ];

  const allFailed = ordered.every((outcome) => !outcome.ok);

  let message: string | null = null;
  for (const outcome of ordered) {
    if (!outcome.ok && outcome.failure.serverMessage !== null) {
      message = outcome.failure.serverMessage;
      break;
    }
  }

  return {
    allFailed,
    message,
    kpi: kpiView(outcomes.kpi),
    facilityPerformance: plainView(outcomes.facilityPerformance),
    continuousProblems: plainView(outcomes.continuousProblems),
    errorBreakdown: plainView(outcomes.errorBreakdown),
  };
}
