"use client";

import Link from "next/link";
import { useCallback, useEffect, useMemo, useState } from "react";
import { CleanLogTable } from "@/components/features/detail-log/CleanLogTable";
import { AreaModal } from "@/components/features/detail-log/AreaModal";
import { FilterButton } from "@/components/ui/FilterButton";
import { Pagination } from "@/components/ui/Pagination";
import { useTheme } from "@/components/theme/ThemeProvider";
import { apiFetch, ApiFetchError } from "@/lib/api/client";
import type { AreaFilter } from "@/lib/clean-logs/areas";
import {
  appendDateRangeParams,
  dateRangeError,
} from "@/lib/clean-logs/detail-log-date-range";
import { MAX_CSV_ROWS, MAX_DATE_RANGE_DAYS, PAGINATION } from "@/lib/config";
import { formatMd } from "@/lib/format/date";
import { openDatePicker } from "@/components/ui/date-picker";
import { useClassificationFilter } from "@/lib/hooks/use-classification-filter";
import {
  ALL_OPTION_VALUE,
  appendClassificationParams,
} from "@/lib/masters/options";
import { totalPageCount } from "@/lib/pagination/page-items";
import { THEME_TOKENS } from "@/lib/theme";
import type {
  AreaCompletionsResponse,
  CleanLogAdviceResponse,
  CleanLogItem,
  ListResponse,
} from "@/lib/types";
import styles from "./DetailLogScreen.module.css";

type GradeFilter = "problem" | "all" | "critical";

/**
 * エリアモーダルを開いている対象。
 *
 * ★`logId` は「エリアを特定するキー」ではなく、施設と日付を特定するための入口★
 * （指示書Z 決定1）。施設名・日付はヘッダに出すためだけに持つ。
 */
interface AreaTarget {
  logId: number;
  facilityName: string;
  dateLabel: string;
}

export function DetailLogScreen() {
  const { t, theme } = useTheme();
  const c = THEME_TOKENS[theme];

  /**
   * 分類（ホールディングス → 屋号）の絞り込み（指示書AK 段階7。D-053 を解除）。
   *
   * ★この画面に「検索」ボタンは無い★（モック:553-570 は CSV ボタンだけ）。
   *   セレクトの変更は △×のみ / 全件 / ×のみ や日付入力と同じ「1操作＝1確定」なので、
   *   ★フックの `draft` をそのまま確定値として使う★（レポート画面のような
   *   draft/確定の二重持ちはしない）。
   *   ★1文字ごとに叩かないための debounce が要るのは文字入力だけ★で、
   *   セレクトは1回の操作で1回しか変わらない。
   */
  const classification = useClassificationFilter(t);

  /**
   * 期間（`YYYY-MM-DD`。空文字＝指定なし）。
   *
   * ★既定は「期間を指定しない（全期間）」★（ユーザー指示 2026-10-06「C」。D-159 追記 / 指示書AV 段階5b）—
   *   ダッシュボードは「各清掃エリアの直近の清掃（7日以内）」なので、既定を当日にすると
   *   前日 19:00 の出発失敗がダッシュボードには出るのに、カードや「全項目 →」で移った先に出ない。
   *   旧: 開いたときは `date_from` = `date_to` = 当日。
   * ★空の側は URL に付けない★（`appendDateRangeParams()`）。片方だけならその側だけの範囲になる（5b 以前と同じ）。
   * ★性能の注意（本書では対策しない）★: Prisma 版は期間なしだと保持期間（2年）の予定を全部読んでから
   *   ページ分けする（`lib/db/index.ts` の `cleanLogs()`）。リリース前に見直す（指示書AV 段階5b-7）
   */
  const [dateFrom, setDateFrom] = useState("");
  const [dateTo, setDateTo] = useState("");
  const [gradeFilter, setGradeFilter] = useState<GradeFilter>("problem");
  const [facilityName, setFacilityName] = useState("");
  const [model, setModel] = useState("");
  const [serial, setSerial] = useState("");
  const [debouncedFacility, setDebouncedFacility] = useState("");
  const [debouncedModel, setDebouncedModel] = useState("");
  const [debouncedSerial, setDebouncedSerial] = useState("");
  /**
   * 現在ページ（1始まり。指示書AA 段階3）
   *
   * ★フィルタを変えたら必ず 1 に戻すこと★ — 件数が減ったときにページ3へ
   *   取り残されると、ユーザーが名指しで禁じた「押したら有りません」の状態になる。
   * ★`useEffect` で後追いしないこと★ — 古いページのままフェッチが1回余計に飛ぶ。
   *   state を変えるその場で一緒に戻す（debounce のものはタイマーの中で戻す）。
   */
  const [page, setPage] = useState(1);

  const [items, setItems] = useState<CleanLogItem[]>([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  const [expandedId, setExpandedId] = useState<number | null>(null);
  const [adviceById, setAdviceById] = useState<
    Record<number, CleanLogAdviceResponse | undefined>
  >({});
  const [adviceLoadingId, setAdviceLoadingId] = useState<number | null>(null);
  const [showGradeInfo, setShowGradeInfo] = useState(false);
  const [areaTarget, setAreaTarget] = useState<AreaTarget | null>(null);
  // ★既定は "problem"（YAML の default と同じ）★
  const [areaFilter, setAreaFilter] = useState<AreaFilter>("problem");
  const [areaData, setAreaData] = useState<AreaCompletionsResponse | null>(null);
  const [areaLoading, setAreaLoading] = useState(false);
  const [areaError, setAreaError] = useState<string | null>(null);

  // ★debounce の反映と同時にページを1へ戻す★（React が1回の再描画にまとめる）
  useEffect(() => {
    const id = window.setTimeout(() => {
      setDebouncedFacility(facilityName);
      setPage(1);
    }, 300);
    return () => window.clearTimeout(id);
  }, [facilityName]);
  useEffect(() => {
    const id = window.setTimeout(() => {
      setDebouncedModel(model);
      setPage(1);
    }, 300);
    return () => window.clearTimeout(id);
  }, [model]);
  useEffect(() => {
    const id = window.setTimeout(() => {
      setDebouncedSerial(serial);
      setPage(1);
    }, 300);
    return () => window.clearTimeout(id);
  }, [serial]);

  const queryString = useMemo(() => {
    const params = new URLSearchParams();
    // ★空の側は付けない★（期間なし＝全期間。API の既存の挙動。CSV も同じクエリ）
    appendDateRangeParams(params, dateFrom, dateTo);
    params.set("result", gradeFilter);
    if (debouncedFacility) params.set("facility_name", debouncedFacility);
    if (debouncedModel) params.set("model", debouncedModel);
    if (debouncedSerial) params.set("serial", debouncedSerial);
    // ★分類は手書きしない★（`group_id=0` を漏らさない・空のキーを並べないための
    //   判断が `appendClassificationParams()` に入っている。指示書AK 落とし穴3）
    appendClassificationParams(params, classification.draft);
    params.set("per_page", String(PAGINATION.defaultPerPage));
    params.set("page", String(page));
    return params.toString();
    // ★`page` を依存に入れること★ — 入れ忘れると `queryString` が変わらず、
    //   `fetchLogs` も変わらないので、ボタンを押しても取り直さない
  }, [
    dateFrom,
    dateTo,
    gradeFilter,
    debouncedFacility,
    debouncedModel,
    debouncedSerial,
    classification.draft,
    page,
  ]);

  // ★期間の検証は両方入っているときだけ★（指示書AV 段階5b-2）。文言・境界はサーバと同じ
  const rangeError = dateRangeError(dateFrom, dateTo, MAX_DATE_RANGE_DAYS);
  const dateRangeUnset = !dateFrom && !dateTo;

  /**
   * ★CSVを出せない絞り込みか★（ユーザー指示 2026-09-15 / 指示書AI 段階8）
   *
   * ★★一覧を取ったときの `meta.total` をそのまま使う★★ —
   *   ★CSVのために問い合わせを増やさないこと★（件数は既に手元にある）。
   *
   * ★上限は「含む」★ — ちょうど `MAX_CSV_ROWS` 件は出せる（`>` であって `>=` ではない）。
   *   サーバ側（`app/api/clean-logs/export/route.ts`）と★同じ向き★にすること。
   *   ★片方だけ `>=` にすると、押せるのに422が返る（またはその逆）★。
   *
   * ★★サーバ側の422を消さないこと★★ — APIを直接叩く経路が残る。
   *   ★画面は「親切」、サーバは「歯止め」で役割が違う★。
   */
  const csvBlocked = total > MAX_CSV_ROWS;

  const fetchLogs = useCallback(async () => {
    if (rangeError !== null) {
      setError(rangeError);
      setItems([]);
      setTotal(0);
      setLoading(false);
      return;
    }

    setLoading(true);
    setError(null);
    try {
      const body = await apiFetch<ListResponse<CleanLogItem>>(
        `/api/clean-logs?${queryString}`
      );
      setItems(body.items);
      setTotal(body.meta.total);
      // ★フィルタ外の要因（データ更新など）で総ページ数が減り、存在しないページに
      //   取り残された場合の保険。最終ページに寄せて取り直す★
      //   `pages > 0` の判定を落とさないこと（0件のとき setPage(0) で無限ループになる）
      const pages = totalPageCount(body.meta.total, body.meta.per_page);
      if (pages > 0 && body.meta.page > pages) setPage(pages);
    } catch (e) {
      // サーバーが ApiError を返した場合はその文言、それ以外は画面側の既定文言
      setError(
        (e instanceof ApiFetchError ? e.serverMessage : null) ??
          "データの取得に失敗しました"
      );
      setItems([]);
      setTotal(0);
    } finally {
      setLoading(false);
    }
  }, [queryString, rangeError]);

  useEffect(() => {
    // フィルタ変更に合わせて一覧を取り直す（外部APIとの同期）
    // eslint-disable-next-line react-hooks/set-state-in-effect -- fetch 起点の一覧更新
    void fetchLogs();
  }, [fetchLogs]);

  async function toggleRow(row: CleanLogItem) {
    if (expandedId === row.id) {
      setExpandedId(null);
      return;
    }
    setExpandedId(row.id);
    if (adviceById[row.id]) return;
    setAdviceLoadingId(row.id);
    try {
      const body = await apiFetch<CleanLogAdviceResponse>(
        `/api/clean-logs/${row.id}/advice`
      );
      setAdviceById((prev) => ({ ...prev, [row.id]: body }));
    } catch {
      // 取得できなくても行は開く。一覧が持っている値で最小限の中身を埋める
      setAdviceById((prev) => ({
        ...prev,
        [row.id]: {
          escalation_level: row.escalation_level,
          consecutive_problem_days: row.consecutive_problem_days,
          // ★一覧の行も同じ上限を通ってきている★（指示書AF 段階6。/clean-logs と
          //   /clean-logs/{id}/advice で連続日数が食い違わないようにするため）。
          //   一覧が古くてフラグを持っていなければ「頭打ちではない」として扱う
          consecutive_problem_days_capped:
            row.consecutive_problem_days_capped ?? false,
          max_level: 0,
          advices: [],
        },
      }));
    } finally {
      setAdviceLoadingId(null);
    }
  }

  /**
   * エリアモーダルを開く。
   *
   * ★ここでは取得しない★ — 取得は下の `useEffect` に寄せてある。
   *   ★`filter` を切り替えたら**取り直す**必要がある★（絞り込みはサーバ側。指示書Z 決定8）ので、
   *   「開いたとき」と「切り替えたとき」で取得経路が2本に分かれないようにした。
   * ★開くたびに `filter` を既定（問題エリアのみ）に戻す★ — 前に見た行の選択が残ると、
   *   別の行を開いたときに何を見ているのか分からなくなる。
   */
  function openAreas(row: CleanLogItem) {
    setAreaFilter("problem");
    setAreaTarget({
      logId: row.id,
      facilityName: row.facility_name,
      dateLabel: formatMd(row.scheduled_at),
    });
  }

  const areaLogId = areaTarget?.logId ?? null;
  useEffect(() => {
    if (areaLogId === null) return;
    // 取得中に別の行を開く / filter を切り替えると、古い応答が後から届いて
    // 新しい表示を上書きしうるので破棄する
    let cancelled = false;
    // eslint-disable-next-line react-hooks/set-state-in-effect -- fetch 起点のモーダル更新
    setAreaLoading(true);
    setAreaError(null);
    setAreaData(null);
    void (async () => {
      try {
        const body = await apiFetch<AreaCompletionsResponse>(
          `/api/clean-logs/${areaLogId}/areas?filter=${areaFilter}`
        );
        if (!cancelled) setAreaData(body);
      } catch (e) {
        // ★取得できなくてもモーダルは開いたままにする★ —
        //   ボタンを押して何も起きないのがいちばん困るため、失敗はモーダルの中に出す
        if (!cancelled) {
          setAreaError(
            (e instanceof ApiFetchError ? e.serverMessage : null) ??
              "データの取得に失敗しました"
          );
        }
      } finally {
        if (!cancelled) setAreaLoading(false);
      }
    })();
    return () => {
      cancelled = true;
    };
  }, [areaLogId, areaFilter]);

  return (
    <div className={styles.page}>
      <div className={styles.header}>
        <div>
          <h1 className={styles.title}>{t("清掃詳細ログ")}</h1>
          <div className={styles.subtitle}>1行＝1ロボット×1スケジュール実行</div>
        </div>
        <div className={styles.headerRight}>
          {/* ★期間が空のときは「全期間」と分かるようにする★（指示書AV 段階5b-5）—
              ★`<input type="date">` の placeholder は多くのブラウザで表示されない★ので、
              両方空のあいだだけ同じ文言を入力欄の前にも出す（placeholder にも同じ文言） */}
          {dateRangeUnset && (
            <span className={styles.dateSep}>{t("指定なし（全期間）")}</span>
          )}
          <input
            type="date"
            /* ★枠のどこを押してもカレンダーを開く★（ユーザー指示 2026-10-06。D-162） */
            onClick={openDatePicker}
            className={styles.dateInput}
            placeholder={t("指定なし（全期間）")}
            value={dateFrom}
            onChange={(e) => {
              setDateFrom(e.target.value);
              setPage(1);
            }}
          />
          <span className={styles.dateSep}>～</span>
          <input
            type="date"
            onClick={openDatePicker}
            className={styles.dateInput}
            placeholder={t("指定なし（全期間）")}
            value={dateTo}
            onChange={(e) => {
              setDateTo(e.target.value);
              setPage(1);
            }}
          />
          <Link href="/dashboard" className={styles.backBtn}>
            ← 戻る
          </Link>
        </div>
      </div>

      <div className={styles.card}>
        <div className={styles.toolbar}>
          <div className={styles.toolbarLeft}>
            {/* ★分類の絞り込み（モック:555-560。指示書AK 段階7 で D-053 を解除）★
                ・選択肢は `/groups` と `/customers` から取る（★固定の選択肢を書かない★。
                  モックの "PPIH" / "ドン・キホーテ" を写し取ったのが D-053 で消されたUI）
                ・★取得に失敗した／屋号が0件のときは出さない★（`available` が false）—
                  ★空の `<select>` は「押しても何も起きないUI」への逆戻り★（落とし穴11）
                ・★option のラベルを `t()` に通さないこと★ — 「全ホールディングス」等は
                  フック側で `t()` 済みで、屋号名まで通すと辞書の対訳に誤って当たる */}
            {classification.available && (
              <>
                {classification.hasGroups && (
                  <select
                    /* ★幅を固定する★（選んだ名前の長さで幅が変わらないように）。★切れた名前はホバーで読めるよう、
                       選択中の表示名を title に出す★（ユーザー指示 2026-10-06。D-162） */
                    className={`${styles.select} ${styles.masterSelect}`}
                    aria-label={t("ホールディングス")}
                    title={
                      classification.groupOptions.find(
                        (o) =>
                          o.value ===
                          String(classification.draft.groupId ?? ALL_OPTION_VALUE),
                      )?.label
                    }
                    value={
                      classification.draft.groupId === null
                        ? ALL_OPTION_VALUE
                        : String(classification.draft.groupId)
                    }
                    onChange={(e) => {
                      classification.selectGroup(e.target.value);
                      // ★絞り込みを変えたらその場で1ページ目に戻す★
                      //   （件数が減ったときにページ3へ取り残されないため）
                      setPage(1);
                    }}
                  >
                    {classification.groupOptions.map((o) => (
                      <option key={o.value} value={o.value}>
                        {o.label}
                      </option>
                    ))}
                  </select>
                )}
                <select
                  /* 幅の固定と title は上のホールディングスと同じ（D-162） */
                  className={`${styles.select} ${styles.masterSelect}`}
                  aria-label={t("顧客")}
                  title={
                    classification.customerOptions.find(
                      (o) =>
                        o.value ===
                        String(classification.draft.customerId ?? ALL_OPTION_VALUE),
                    )?.label
                  }
                  value={
                    classification.draft.customerId === null
                      ? ALL_OPTION_VALUE
                      : String(classification.draft.customerId)
                  }
                  onChange={(e) => {
                    classification.selectCustomer(e.target.value);
                    setPage(1);
                  }}
                >
                  {classification.customerOptions.map((o) => (
                    <option key={o.value} value={o.value}>
                      {o.label}
                    </option>
                  ))}
                </select>
              </>
            )}
            <FilterButton
              active={gradeFilter === "problem"}
              color={c.wn}
              onClick={() => {
                setGradeFilter("problem");
                setPage(1);
              }}
            >
              {t("△×のみ")}
            </FilterButton>
            <FilterButton
              active={gradeFilter === "all"}
              color={c.txD}
              onClick={() => {
                setGradeFilter("all");
                setPage(1);
              }}
            >
              {t("全件")}
            </FilterButton>
            <FilterButton
              active={gradeFilter === "critical"}
              color={c.ng}
              onClick={() => {
                setGradeFilter("critical");
                setPage(1);
              }}
            >
              {t("×のみ")}
            </FilterButton>
          </div>
          <div className={styles.toolbarRight}>
            <input
              className={styles.searchInput}
              style={{ width: 130 }}
              value={facilityName}
              onChange={(e) => setFacilityName(e.target.value)}
              placeholder="施設名"
            />
            <input
              className={styles.searchInput}
              style={{ width: 80 }}
              value={model}
              onChange={(e) => setModel(e.target.value)}
              placeholder="機種"
            />
            <input
              className={styles.searchInput}
              style={{ width: 100 }}
              value={serial}
              onChange={(e) => setSerial(e.target.value)}
              placeholder="シリアル"
            />
            {/* /api/clean-logs/export はページではなくファイルダウンロード用のAPIなので、
                Next のルーター（router.push）ではなく通常の <a download> でブラウザの
                ネイティブなダウンロード遷移に任せる。router.push を使うと RSC の
                プリフェッチ/プローブが追加で走り、クリック1回でエクスポートが2回実行されてしまう。 */}
            {/* ★上限超過のときは <a> ではなく <button disabled> に差し替える★（指示書AI 段階8）—
                ★`<a>` に `disabled` 属性は無く、付けても押せてしまう★。
                `pointer-events: none` で隠すのも不可（キーボード操作では辿れてしまう）。
                ★理由は下の `.csvNote` に必ず出すこと★ — 押せないボタンだけを置くと
                不具合と区別がつかない（D-053 がUIを削除した理由と同じ）。 */}
            {csvBlocked ? (
              <button
                type="button"
                className={styles.csvBtnDisabled}
                disabled
                aria-describedby="csv-note"
              >
                {t("CSV")}
              </button>
            ) : (
              <a
                className={styles.csvBtn}
                href={`/api/clean-logs/export?${queryString}`}
                download
              >
                {t("CSV")}
              </a>
            )}
          </div>
        </div>
        {/* ★CSVは表示中のページではなく全件★ —
            app/api/clean-logs/export/route.ts は page / per_page を無視する契約なので、
            href に `page` が混ざっても動作は変わらない（リンクを分岐させないこと）。
            ★上限を超えているときは、同じ場所を「出せない理由」に差し替える★
            （注記と理由を上下に並べると、どちらが今の状態なのか分からなくなる） */}
        <div
          id="csv-note"
          className={csvBlocked ? styles.csvNoteBlocked : styles.csvNote}
        >
          {csvBlocked
            ? t(
                "この期間だとCSV出力できません（{total}件／上限{max}件）。期間を短くするか、店舗で絞り込んでください。"
              )
                .replace("{total}", String(total))
                .replace("{max}", String(MAX_CSV_ROWS))
            : t(
                "CSVは絞り込み条件に一致する全件を出力します（表示中のページだけではありません）"
              )}
        </div>

        <div className={styles.tableWrap}>
          {loading && <div className={styles.loading}>読み込み中…</div>}
          {!loading && error && <div className={styles.error}>{error}</div>}
          {!loading && !error && (
            <CleanLogTable
              rows={items}
              expandedId={expandedId}
              adviceById={adviceById}
              adviceLoadingId={adviceLoadingId}
              showGradeInfo={showGradeInfo}
              onToggleGradeInfo={() => setShowGradeInfo((v) => !v)}
              onToggleRow={toggleRow}
              onOpenAreas={openAreas}
            />
          )}
        </div>

        {/* ★件数表示に `items.length` を使わないこと★ —
            ローディング中や取得失敗で items が空になり、件数だけ0に見える */}
        <Pagination
          total={total}
          page={page}
          perPage={PAGINATION.defaultPerPage}
          onPageChange={setPage}
        />
      </div>

      {areaTarget && (
        <AreaModal
          facilityName={areaTarget.facilityName}
          dateLabel={areaTarget.dateLabel}
          filter={areaFilter}
          onChangeFilter={setAreaFilter}
          data={areaData}
          loading={areaLoading}
          error={areaError}
          onClose={() => setAreaTarget(null)}
        />
      )}
    </div>
  );
}
