"use client";

import { useCallback, useEffect, useMemo, useState } from "react";
import { FilterButton } from "@/components/ui/FilterButton";
import { Pagination } from "@/components/ui/Pagination";
import { Pill } from "@/components/ui/Pill";
import { StatCard } from "@/components/ui/StatCard";
import { useTheme } from "@/components/theme/ThemeProvider";
import { apiFetch, ApiFetchError } from "@/lib/api/client";
import { DEVICE_NOTE_MAX_LENGTH, PAGINATION } from "@/lib/config";
import { formatHm, formatMd } from "@/lib/format/date";
import type { DsDeviceSummary } from "@/lib/ds-devices/query";
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, type ThemeName } from "@/lib/theme";
import type {
  DsConnectionStatus,
  DsDevice,
  DsDeviceStatusFilter,
  SummaryListResponse,
} from "@/lib/types";
import styles from "./DsLogScreen.module.css";

type ThemeTokens = (typeof THEME_TOKENS)[ThemeName];

/** 通信状態は正常/異常の2状態だけ（D-065。通信断は「異常」そのもの） */
function connectionStatusLabel(status: DsConnectionStatus): string {
  if (status === "ok") return "正常";
  return "異常";
}

function connectionStatusColor(c: ThemeTokens, status: DsConnectionStatus): string {
  if (status === "ok") return c.ok;
  return c.ng;
}

function formatDateTime(iso: string): string {
  return `${formatMd(iso)} ${formatHm(iso)}`;
}

const EMPTY_SUMMARY: DsDeviceSummary = { total: 0, ok: 0, error: 0 };

/**
 * PATCH /ds-devices/{serial_number}/note のレスポンス。
 *
 * `d-watch-api.yaml` ではこのスキーマに名前が付いていない（インライン定義）ため、
 * `lib/types` には置かずここで持つ。名前付きスキーマになった時点で移す。
 * `note` が null になり得るのは、空文字を送ると備考が削除されるため。
 */
interface DsDeviceNoteResponse {
  serial_number: string;
  note: string | null;
}

/** 備考の保存失敗。どのセルの下に出すか判るよう serial_number と対で持つ */
interface NoteError {
  serial: string;
  message: string;
}

interface NoteCellProps {
  device: DsDevice;
  /** 編集モードか（= このセルの textarea が開いているか） */
  editing: boolean;
  /** 保存API呼び出し中か。入力の無効化に使う */
  saving: boolean;
  /** 保存に失敗した場合の文言。無ければ null */
  errorMessage: string | null;
  onStartEdit: () => void;
  /** 確定（onBlur）。textarea の現在値を渡す */
  onCommit: (note: string) => void;
}

/**
 * 「備考」列のセル。表示 ↔ 入力をセルの中だけで切り替える（インライン編集）。
 *
 * モーダルにしない理由: 一覧は1ページあたり `PAGINATION.defaultPerPage` 件並ぶため、
 * 1件ずつ開いて閉じる操作は運用担当の手数が多すぎる。
 * 保存ボタンを常時置かない理由: 「備考」列が広がり他の列が潰れる。
 * 代わりに **フォーカスが外れた時点（onBlur）で確定**する。
 */
function NoteCell({
  device,
  editing,
  saving,
  errorMessage,
  onStartEdit,
  onCommit,
}: NoteCellProps) {
  const { t } = useTheme();

  return (
    <td className={styles.tdNote}>
      {editing ? (
        <textarea
          className={styles.noteInput}
          /* 非制御。確定が onBlur だけなので入力途中の値を state に持つ必要が無く、
             1文字ごとに一覧全体を再描画せずに済む。編集に入るたびに別のノードが
             マウントされるため、defaultValue はその都度入り直す */
          defaultValue={device.note ?? ""}
          rows={2}
          /* 上限超過（422）を画面側で先に防ぐ。上限値の正は lib/config */
          maxLength={DEVICE_NOTE_MAX_LENGTH}
          placeholder={t("備考を入力")}
          /* クリックで編集に入る方式なので、開いた直後に自分でフォーカスを取らないと
             onBlur が発火せず確定できない。autoFocus はマウント時のみ効くため、
             保存失敗などで再描画されてもフォーカスを奪い返さない */
          autoFocus
          disabled={saving}
          onBlur={(e) => onCommit(e.target.value)}
        />
      ) : (
        <button
          type="button"
          className={styles.noteButton}
          title={t("クリックして備考を編集")}
          onClick={onStartEdit}
        >
          {device.note ?? "-"}
        </button>
      )}
      {saving && <div className={styles.noteSaving}>{t("保存中…")}</div>}
      {/* サーバーが返した文言は辞書に無ければ `translate()` が原文のまま返すため、
          画面側の既定文言（保存に失敗しました）だけが EN に差し替わる */}
      {errorMessage && <div className={styles.noteError}>{t(errorMessage)}</div>}
    </td>
  );
}

export function DsLogScreen() {
  const { t, theme } = useTheme();
  const c = THEME_TOKENS[theme];

  /**
   * 分類（ホールディングス → 屋号）の絞り込み（指示書AK 段階7。D-053 を解除）。
   *
   * ★この画面に「検索」ボタンは無い★ — モック:834-847 のツールバーは
   *   セレクト2つ・検索入力・フィルタボタン3つだけで、★モック全体で「検索」ボタンが
   *   あるのはレポート画面（:679）1箇所だけ★（実測）。
   *   セレクトの変更はフィルタボタンと同じ「1操作＝1確定」なので、
   *   ★フックの `draft` をそのまま確定値として使う★（`DetailLogScreen.tsx` と同じ判断）。
   *   ★debounce が要るのは文字入力（`q`）だけ★で、セレクトは1操作で1回しか変わらない。
   *
   * ★★分類は summary（数値カード3枚）にも効く★★ — サーバ側が
   *   `q` / `status` より前に分類で絞っている（`app/api/ds-devices/route.ts`）ので、
   *   屋号を選ぶと「合計 / 正常 / 異常」もその屋号の値になる。
   */
  const classification = useClassificationFilter(t);

  const [q, setQ] = useState("");
  const [debouncedQ, setDebouncedQ] = useState("");
  const [statusFilter, setStatusFilter] = useState<DsDeviceStatusFilter>("all");

  /**
   * 現在ページ（1始まり。指示書AA 段階4）
   *
   * ★フィルタ・検索を変えたら必ず 1 に戻すこと★（debounce の反映時も）。
   * ★備考の保存では触らないこと★ — 保存のたびに1ページ目へ飛ばされると使い物にならない。
   */
  const [page, setPage] = useState(1);

  const [items, setItems] = useState<DsDevice[]>([]);
  const [summary, setSummary] = useState<DsDeviceSummary>(EMPTY_SUMMARY);
  /**
   * 一覧（ステータスフィルタ後）の総件数。★`meta.total`★ を入れる。
   *
   * ★`summary.total` を流用しないこと★（D-054 / 指示書AA 禁止事項4）—
   *   `summary` はステータスフィルタの**前**に計算されているので、`status=error` で絞ると
   *   `summary.total > meta.total` になり、**存在しないページのボタンが出る**。
   *   `StatCard` に出している `summary.total` とは別物。
   */
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  /**
   * 「備考」列のインライン編集の状態。
   *
   * 同時に開くセルは1つだけなので、セル単位の Map ではなく
   * 「今どの serial_number を編集/保存しているか」を1つずつ持つ形にしている。
   * 入力途中の文字列は textarea 自身（非制御）が持つため state に無い。
   */
  const [editingSerial, setEditingSerial] = useState<string | null>(null);
  const [savingSerial, setSavingSerial] = useState<string | null>(null);
  const [noteError, setNoteError] = useState<NoteError | null>(null);

  // ★debounce の反映と同時にページを1へ戻す★（React が1回の再描画にまとめる）
  useEffect(() => {
    const id = window.setTimeout(() => {
      setDebouncedQ(q);
      setPage(1);
    }, 300);
    return () => window.clearTimeout(id);
  }, [q]);

  const queryString = useMemo(() => {
    const params = new URLSearchParams();
    if (debouncedQ) params.set("q", debouncedQ);
    params.set("status", statusFilter);
    // ★分類は手書きしない★（`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` を依存に入れること★ — 入れ忘れるとボタンを押しても取り直さない
  }, [debouncedQ, statusFilter, classification.draft, page]);

  const fetchDevices = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const body = await apiFetch<SummaryListResponse<DsDevice, DsDeviceSummary>>(
        `/api/ds-devices?${queryString}`
      );
      setItems(body.items);
      setSummary(body.summary);
      // ★総ページ数は `meta.total` から出す（`summary.total` ではない）★
      setTotal(body.meta.total);
      // ★存在しないページに取り残された場合の保険★（`pages > 0` を落とさないこと）
      const pages = totalPageCount(body.meta.total, body.meta.per_page);
      if (pages > 0 && body.meta.page > pages) setPage(pages);
    } catch (e) {
      setError(
        (e instanceof ApiFetchError ? e.serverMessage : null) ??
          "データの取得に失敗しました"
      );
      setItems([]);
      setSummary(EMPTY_SUMMARY);
      setTotal(0);
    } finally {
      setLoading(false);
    }
  }, [queryString]);

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect -- フィルタ変更に合わせた一覧の取り直し
    void fetchDevices();
  }, [fetchDevices]);

  const startEditNote = useCallback((serial: string) => {
    setEditingSerial(serial);
    // 別のセルを開いた時点で、前のセルの失敗表示は用済み
    setNoteError(null);
  }, []);

  /**
   * 備考を保存する。確定の契機は textarea の onBlur（フォーカスが外れた時）。
   *
   * ★保存後に `fetchDevices()` を呼ばないこと★
   *   全件再取得は `setLoading(true)` を伴うため表がローディング表示に入れ替わり、
   *   編集していた行の位置を見失う。レスポンスの値で該当行だけ差し替える。
   */
  const commitNote = useCallback(async (device: DsDevice, note: string) => {
    const serial = device.serial_number;

    // 変えていないなら叩かない。無駄な PATCH になるうえ、
    // 「クリックして外しただけ」で保存失敗の赤字が出るのを避ける
    if (note === (device.note ?? "")) {
      setEditingSerial((prev) => (prev === serial ? null : prev));
      return;
    }

    setSavingSerial(serial);
    setNoteError(null);
    try {
      const saved = await apiFetch<DsDeviceNoteResponse>(
        `/api/ds-devices/${encodeURIComponent(serial)}/note`,
        {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ note }),
        }
      );
      // 保存側がトリムと「空文字 → null」の変換を行うので、送った文字列ではなく
      // 返ってきた値を正として1行だけ差し替える
      setItems((prev) =>
        prev.map((d) => (d.serial_number === serial ? { ...d, note: saved.note } : d))
      );
      // 保存が終わる前に別のセルを開いていた場合は、そちらの編集を閉じない
      setEditingSerial((prev) => (prev === serial ? null : prev));
    } catch (e) {
      // 失敗時は編集状態のまま残す。閉じると入力した文字が消えて打ち直しになる
      setNoteError({
        serial,
        message:
          (e instanceof ApiFetchError ? e.serverMessage : null) ?? "保存に失敗しました",
      });
    } finally {
      setSavingSerial((prev) => (prev === serial ? null : prev));
    }
  }, []);

  return (
    <div className={styles.page}>
      <div className={styles.header}>
        <h1 className={styles.title}>{t("D-Secログ")}</h1>
        <div className={styles.subtitle}>{t("D-S制御盤の稼働状況")}</div>
      </div>

      <div className={styles.statGrid}>
        <StatCard value={String(summary.total)} label={t("合計")} tone="ac" />
        <StatCard value={String(summary.ok)} label={t("正常")} tone="ok" />
        <StatCard value={String(summary.error)} label={t("異常")} sub={t("要対応")} tone="ng" />
      </div>

      <div className={styles.toolbar}>
        {/* ★分類の絞り込み（モック:835-840。指示書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>
          </>
        )}
        <input
          className={styles.searchInput}
          placeholder={t("施設名・シリアルで検索")}
          value={q}
          onChange={(e) => setQ(e.target.value)}
        />
        <div className={styles.statusButtons}>
          <FilterButton
            active={statusFilter === "all"}
            color={c.txD}
            onClick={() => {
              setStatusFilter("all");
              setPage(1);
            }}
          >
            {t("全て")}
          </FilterButton>
          <FilterButton
            active={statusFilter === "error"}
            color={c.ng}
            onClick={() => {
              setStatusFilter("error");
              setPage(1);
            }}
          >
            {t("異常")}
          </FilterButton>
          <FilterButton
            active={statusFilter === "ok"}
            color={c.ok}
            onClick={() => {
              setStatusFilter("ok");
              setPage(1);
            }}
          >
            {t("正常")}
          </FilterButton>
        </div>
      </div>

      {loading && <div className={styles.loading}>読み込み中…</div>}
      {!loading && error && <div className={styles.error}>{error}</div>}

      {!loading && !error && (
        <div className={styles.card}>
          <div className={styles.tableWrap}>
            <table className={styles.table}>
              {/*
                ★列幅は言語によらず固定する（table-layout: fixed）★
                見出しの文字で列幅が決まる作りだと、JP/EN を切り替えるたびに列ごとに幅が変わって
                表が横に動く（2026-10-01 ユーザー指摘）。備考の編集（表示 ↔ textarea）でも幅が動かない。
                幅の値と理由は CSS 側の .col* に書く。
                ★施設名列にも幅を付け、表の余った幅は全列に幅の比で配る★（ユーザー指示 2026-10-06
                「3つとも同じように調整して」。D-167 追記。旧: 施設名列だけ幅を指定せず余りを全部受け持っていた）。
                列を足す・並べ替えるときはここの <col> も同じ順に直すこと（ずれると別の列に幅が付く）。
              */}
              <colgroup>
                <col className={styles.colSerial} />
                <col className={styles.colFacility} />
                <col className={styles.colStatus} />
                <col className={styles.colLastReported} />
                <col className={styles.colNote} />
              </colgroup>
              <thead>
                <tr>
                  <th className={styles.th}>{t("シリアル")}</th>
                  <th className={styles.th}>{t("施設名")}</th>
                  <th className={styles.thCenter}>{t("接続状態")}</th>
                  <th className={styles.th}>{t("最終通信")}</th>
                  <th className={styles.th}>{t("備考")}</th>
                </tr>
              </thead>
              <tbody>
                {items.map((item) => (
                  <tr key={item.serial_number} className={styles.row}>
                    <td className={styles.tdSerial}>{item.serial_number}</td>
                    <td className={styles.td}>{item.facility_name}</td>
                    <td className={styles.tdCenter}>
                      <Pill
                        text={t(connectionStatusLabel(item.connection_status))}
                        color={connectionStatusColor(c, item.connection_status)}
                      />
                    </td>
                    <td className={styles.tdDate}>{formatDateTime(item.last_reported_at)}</td>
                    <NoteCell
                      device={item}
                      editing={editingSerial === item.serial_number}
                      saving={savingSerial === item.serial_number}
                      errorMessage={
                        noteError?.serial === item.serial_number ? noteError.message : null
                      }
                      onStartEdit={() => startEditNote(item.serial_number)}
                      onCommit={(note) => void commitNote(item, note)}
                    />
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
          {items.length === 0 && (
            <div className={styles.empty}>{t("該当するデバイスはありません")}</div>
          )}
          {/* ★件数表示に `items.length` を使わないこと★。0件なら部品側が null を返す */}
          <Pagination
            total={total}
            page={page}
            perPage={PAGINATION.defaultPerPage}
            onPageChange={setPage}
          />
        </div>
      )}
    </div>
  );
}
