"use client";

import {
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type RefObject,
} from "react";
import { openDatePicker } from "@/components/ui/date-picker";
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 {
  DS_LOG_DEFAULT_PER_PAGE,
  DS_LOG_PER_PAGE_OPTIONS,
  EXPORT_MAX_BYTES,
  EXPORT_MAX_FILES,
} from "@/lib/config/ds-agent";
import {
  apiErrorMessage,
  detailStrings,
  formatDateTime,
  formatFileSize,
} from "@/lib/ds-logs/modal-text";
import {
  dateRangeError,
  periodFromDates,
  periodFromPreset,
  type DsLogPeriodPreset,
} from "@/lib/ds-logs/presets";
import { deviceLogTypesParam } from "@/lib/ds-logs/query";
import { THEME_TOKENS } from "@/lib/theme";
import type {
  DsAgentTokenIssued,
  DsAgentTokenStatus,
  DsLogExportResponse,
  DsLogFile,
  DsLogListResponse,
  DsLogSort,
  DsLogType,
} from "@/lib/types";
import { shouldCloseOnBackdrop, trapFocusIndex } from "@/lib/ui/modal";
import styles from "./DeviceLogsModal.module.css";

interface DeviceLogsModalProps {
  serialNumber: string;
  facilityName: string;
  /** 閉じたときにフォーカスを戻す「ログ」ボタン */
  restoreFocusRef: RefObject<HTMLButtonElement | null>;
  onClose: () => void;
}

/** Tab の閉じ込めに使う、窓の中のフォーカスできる要素（ErrorContentPicker と同じ） */
const FOCUSABLE_SELECTOR = [
  "button:not([disabled])",
  "input:not([disabled])",
  "select:not([disabled])",
  "textarea:not([disabled])",
  "a[href]",
  '[tabindex]:not([tabindex="-1"])',
].join(", ");

type PeriodMode =
  | { kind: "preset"; preset: DsLogPeriodPreset }
  | { kind: "dates"; start: string; end: string };

/**
 * 制御盤のログのモーダル（指示書AY 段階9-2。D-179）。
 * 見た目は AreaModal、フォーカス・Tab・背景クリックは ErrorContentPicker に揃える。
 */
export function DeviceLogsModal({
  serialNumber,
  facilityName,
  restoreFocusRef,
  onClose,
}: DeviceLogsModalProps) {
  const { t, theme } = useTheme();
  const c = THEME_TOKENS[theme];
  const titleId = useId();
  const panelRef = useRef<HTMLDivElement>(null);
  const initialFocusRef = useRef<HTMLButtonElement>(null);
  const downOnOverlayRef = useRef(false);
  const exportAbortRef = useRef<AbortController | null>(null);

  const [period, setPeriod] = useState<PeriodMode>({ kind: "preset", preset: "24h" });
  const [sort, setSort] = useState<DsLogSort>("date_desc");
  const [page, setPage] = useState(1);
  const [perPage, setPerPage] = useState(DS_LOG_DEFAULT_PER_PAGE);
  /** null = まだ種類が来ていない。空 Set = 1つも選んでいない */
  const [selectedTypes, setSelectedTypes] = useState<Set<string> | null>(null);
  const [types, setTypes] = useState<DsLogType[]>([]);
  const [items, setItems] = useState<DsLogFile[]>([]);
  const [total, setTotal] = useState(0);
  const [truncated, setTruncated] = useState(false);
  const [connected, setConnected] = useState(false);
  const [lastSeenAt, setLastSeenAt] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [listError, setListError] = useState<string | null>(null);
  /** file_id → size_bytes。ページ送り・並べ替えでは保持、種類・期間変更で消す（D23） */
  const [selection, setSelection] = useState<Map<string, number>>(() => new Map());
  const [exporting, setExporting] = useState(false);
  const [exportMessage, setExportMessage] = useState<string | null>(null);
  const [exportError, setExportError] = useState<string | null>(null);

  const [tokenIssuedAt, setTokenIssuedAt] = useState<string | null>(null);
  const [tokenValue, setTokenValue] = useState<string | null>(null);
  const [tokenBusy, setTokenBusy] = useState(false);
  const [tokenError, setTokenError] = useState<string | null>(null);
  const [tokenLoading, setTokenLoading] = useState(true);

  const typeLabels = useMemo(() => {
    const map = new Map<string, string>();
    for (const type of types) map.set(type.key, type.label);
    return map;
  }, [types]);

  const periodRange = useMemo(() => {
    if (period.kind === "preset") return periodFromPreset(period.preset, new Date());
    return periodFromDates(
      period.start === "" ? null : period.start,
      period.end === "" ? null : period.end
    );
  }, [period]);

  const rangeError = useMemo(() => {
    if (period.kind !== "dates") return null;
    return dateRangeError(
      period.start === "" ? null : period.start,
      period.end === "" ? null : period.end
    );
  }, [period]);

  /** 制御盤に種類があるときだけ「選んでください」（指示書BJ J10） */
  const noTypeSelected =
    selectedTypes !== null && selectedTypes.size === 0 && types.length > 0;

  /** null = 取得しない（種類0件・日付不正）。全種類／未初期化は types パラメータ無し（指示書BJ J10） */
  const queryString = useMemo(() => {
    if (noTypeSelected || periodRange === null) return null;
    const params = new URLSearchParams();
    const typesParam = deviceLogTypesParam(
      types.map((x) => x.key),
      selectedTypes
    );
    if (typesParam !== null) params.set("types", typesParam);
    if (periodRange.from) params.set("from", periodRange.from);
    if (periodRange.to) params.set("to", periodRange.to);
    params.set("sort", sort);
    params.set("page", String(page));
    params.set("per_page", String(perPage));
    return params.toString();
  }, [noTypeSelected, selectedTypes, types, periodRange, sort, page, perPage]);

  // Esc・フォーカス復帰・背景スクロール停止
  useEffect(() => {
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    initialFocusRef.current?.focus();
    const opener = restoreFocusRef.current;
    return () => {
      document.body.style.overflow = prevOverflow;
      opener?.focus();
      exportAbortRef.current?.abort();
    };
  }, [restoreFocusRef]);

  useEffect(() => {
    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        onClose();
        return;
      }
      if (e.key !== "Tab") return;
      const panel = panelRef.current;
      if (!panel) return;
      const focusables = Array.from(panel.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR));
      const active = document.activeElement;
      const activeIndex = active instanceof HTMLElement ? focusables.indexOf(active) : -1;
      const next = trapFocusIndex(focusables.length, activeIndex, e.shiftKey);
      if (focusables.length === 0) {
        e.preventDefault();
        return;
      }
      if (next !== null) {
        e.preventDefault();
        focusables[next]?.focus();
      }
    };
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [onClose]);

  // 制御盤トークンの発行日時（未接続でも取得する）
  useEffect(() => {
    let cancelled = false;
    // eslint-disable-next-line react-hooks/set-state-in-effect -- モーダル表示時のトークン状態の取り直し
    setTokenLoading(true);
    setTokenError(null);
    apiFetch<DsAgentTokenStatus>(
      `/api/ds-devices/${encodeURIComponent(serialNumber)}/agent-token`
    )
      .then(
        (body) => {
          if (cancelled) return;
          setTokenIssuedAt(body.issued_at);
        },
        (e: unknown) => {
          if (cancelled) return;
          setTokenError(apiErrorMessage(e, t) ?? t("データの取得に失敗しました"));
        }
      )
      .finally(() => {
        if (!cancelled) setTokenLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, [serialNumber, t]);

  // 一覧の取得（BC 段階1 の作法。古い応答は cancelled で捨てる）
  useEffect(() => {
    if (queryString === null) {
      // eslint-disable-next-line react-hooks/set-state-in-effect -- 種類0件・日付不正では取得せず文言だけ出す
      setLoading(false);
      setListError(
        noTypeSelected
          ? t("種類を1つ以上選んでください")
          : rangeError === "reversed"
            ? t("開始日は終了日以前にしてください")
            : t("日付の範囲が不正です")
      );
      setItems([]);
      setTotal(0);
      setTruncated(false);
      return;
    }

    let cancelled = false;
    setLoading(true);
    setListError(null);
    apiFetch<DsLogListResponse>(
      `/api/ds-devices/${encodeURIComponent(serialNumber)}/logs?${queryString}`
    )
      .then(
        (body) => {
          if (cancelled) return;
          setTypes(body.types);
          setSelectedTypes((prev) => {
            if (prev === null) return new Set(body.types.map((x) => x.key));
            return prev;
          });
          setItems(body.items);
          setTotal(body.meta.total);
          setTruncated(body.truncated);
          setConnected(body.agent.connected);
          setLastSeenAt(body.agent.last_seen_at);
          const pages = Math.max(1, Math.ceil(body.meta.total / body.meta.per_page) || 1);
          if (body.meta.page > pages) setPage(pages);
        },
        (e: unknown) => {
          if (cancelled) return;
          const message = apiErrorMessage(e, t);
          if (message === null) return;
          setListError(message);
          setItems([]);
          setTotal(0);
          setTruncated(false);
          if (e instanceof ApiFetchError && e.code === "agent_offline") {
            setConnected(false);
            const last = detailStrings(e.details, "last_seen_at")[0] ?? null;
            setLastSeenAt(last);
          } else if (e instanceof ApiFetchError && e.code === "agent_error") {
            // ★制御盤は結果を返しているので接続中（指示書BJ J2）★
            setConnected(true);
          }
        }
      )
      .finally(() => {
        if (!cancelled) setLoading(false);
      });
    return () => {
      cancelled = true;
    };
  }, [serialNumber, queryString, noTypeSelected, rangeError, t]);

  const selectedCount = selection.size;
  const selectedBytes = useMemo(() => {
    let sum = 0;
    for (const size of selection.values()) sum += size;
    return sum;
  }, [selection]);
  const overMaxBytes = selectedBytes > EXPORT_MAX_BYTES;
  const overMaxFiles = selectedCount > EXPORT_MAX_FILES;
  const canExport =
    selectedCount > 0 && !overMaxBytes && !overMaxFiles && !exporting && !loading;

  const pageAllSelected =
    items.length > 0 && items.every((item) => selection.has(item.file_id));

  const clearSelection = () => setSelection(new Map());

  const changePeriodPreset = (preset: DsLogPeriodPreset) => {
    setPeriod({ kind: "preset", preset });
    setPage(1);
    clearSelection();
  };

  const changeDate = (which: "start" | "end", value: string) => {
    setPeriod((prev) => {
      const start = which === "start" ? value : prev.kind === "dates" ? prev.start : "";
      const end = which === "end" ? value : prev.kind === "dates" ? prev.end : "";
      return { kind: "dates", start, end };
    });
    setPage(1);
    clearSelection();
  };

  const toggleType = (key: string) => {
    setSelectedTypes((prev) => {
      const next = new Set(prev ?? []);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });
    setPage(1);
    clearSelection();
  };

  const toggleFile = (file: DsLogFile) => {
    setSelection((prev) => {
      const next = new Map(prev);
      if (next.has(file.file_id)) next.delete(file.file_id);
      else next.set(file.file_id, file.size_bytes);
      return next;
    });
  };

  const togglePageAll = () => {
    setSelection((prev) => {
      const next = new Map(prev);
      if (pageAllSelected) {
        for (const item of items) next.delete(item.file_id);
      } else {
        for (const item of items) next.set(item.file_id, item.size_bytes);
      }
      return next;
    });
  };

  const startExport = async () => {
    if (!canExport) return;
    setExporting(true);
    setExportError(null);
    setExportMessage(null);
    const ac = new AbortController();
    exportAbortRef.current = ac;
    try {
      const body = await apiFetch<DsLogExportResponse>(
        `/api/ds-devices/${encodeURIComponent(serialNumber)}/logs/exports`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ file_ids: [...selection.keys()] }),
          signal: ac.signal,
        }
      );
      const anchor = document.createElement("a");
      anchor.href = body.download_url;
      anchor.download = "";
      document.body.appendChild(anchor);
      anchor.click();
      anchor.remove();
      setExportMessage(
        t("ダウンロードを開始しました（{size}）").replace(
          "{size}",
          formatFileSize(body.total_bytes)
        )
      );
    } catch (e: unknown) {
      const message = apiErrorMessage(e, t);
      if (message !== null) setExportError(message);
    } finally {
      if (exportAbortRef.current === ac) exportAbortRef.current = null;
      setExporting(false);
    }
  };

  const issueToken = async () => {
    if (tokenBusy) return;
    if (
      !window.confirm(
        t("トークンを発行します。発行済みのトークンは使えなくなります。よろしいですか？")
      )
    ) {
      return;
    }
    setTokenBusy(true);
    setTokenError(null);
    try {
      const body = await apiFetch<DsAgentTokenIssued>(
        `/api/ds-devices/${encodeURIComponent(serialNumber)}/agent-token`,
        { method: "POST" }
      );
      setTokenValue(body.token);
      setTokenIssuedAt(body.issued_at);
    } catch (e: unknown) {
      const message = apiErrorMessage(e, t);
      if (message !== null) setTokenError(message);
    } finally {
      setTokenBusy(false);
    }
  };

  const revokeToken = async () => {
    if (tokenBusy) return;
    if (
      !window.confirm(
        t("トークンを失効させます。この制御盤はログを送れなくなります。よろしいですか？")
      )
    ) {
      return;
    }
    setTokenBusy(true);
    setTokenError(null);
    try {
      // ★204 は本文が無いので apiFetch を使わない（4-23・M5）★
      const response = await fetch(
        `/api/ds-devices/${encodeURIComponent(serialNumber)}/agent-token`,
        { method: "DELETE" }
      );
      if (!response.ok) {
        let message = t("データの取得に失敗しました");
        try {
          const body = (await response.json()) as { error?: { message?: string } };
          if (typeof body.error?.message === "string") message = body.error.message;
        } catch {
          /* 本文が読めなければ既定 */
        }
        setTokenError(message);
        return;
      }
      setTokenIssuedAt(null);
      setTokenValue(null);
    } catch {
      setTokenError(t("データの取得に失敗しました"));
    } finally {
      setTokenBusy(false);
    }
  };

  const copyToken = async () => {
    if (!tokenValue) return;
    try {
      await navigator.clipboard.writeText(tokenValue);
    } catch {
      /* コピー失敗は黙る（権限拒否など） */
    }
  };

  const lastSeenLabel = lastSeenAt
    ? t("最終接続 {日時}").replace("{日時}", formatDateTime(lastSeenAt))
    : t("最終接続 -");

  return (
    <div
      className={styles.overlay}
      onMouseDown={(e) => {
        downOnOverlayRef.current = e.target === e.currentTarget;
      }}
      onClick={(e) => {
        const downOnOverlay = downOnOverlayRef.current;
        downOnOverlayRef.current = false;
        if (shouldCloseOnBackdrop(downOnOverlay, e.target === e.currentTarget)) onClose();
      }}
    >
      <div
        ref={panelRef}
        className={styles.panel}
        role="dialog"
        aria-modal="true"
        aria-labelledby={titleId}
        onClick={(e) => e.stopPropagation()}
      >
        <div className={styles.header}>
          <div className={styles.headerMain}>
            <div>
              <h2 id={titleId} className={styles.title}>
                {t("制御盤のログ")}
              </h2>
              <div className={styles.meta}>
                {serialNumber} | {facilityName}
              </div>
            </div>
            <div className={styles.connection}>
              <span
                className={styles.connectionStatus}
                style={{ color: connected ? c.ok : c.ng }}
              >
                {connected ? t("接続中") : t("未接続")}
              </span>
              <span className={styles.connectionSeen}>{lastSeenLabel}</span>
            </div>
          </div>
          <button
            ref={initialFocusRef}
            type="button"
            className={styles.close}
            onClick={onClose}
            aria-label={t("閉じる")}
          >
            ✕
          </button>
        </div>

        <div className={styles.body}>
          <section className={styles.section}>
            <div className={styles.sectionLabel}>{t("種類")}</div>
            <div className={styles.typeList}>
              {types.length === 0 && !loading ? (
                <span className={styles.hint}>{t("種類がまだありません")}</span>
              ) : (
                types.map((type) => (
                  <label key={type.key} className={styles.checkLabel}>
                    <input
                      type="checkbox"
                      checked={selectedTypes?.has(type.key) ?? false}
                      onChange={() => toggleType(type.key)}
                    />
                    {/* ★ラベルは生値。t() に通さない★ */}
                    <span>{type.label}</span>
                  </label>
                ))
              )}
            </div>
          </section>

          <section className={styles.section}>
            <div className={styles.sectionLabel}>{t("期間")}</div>
            <div className={styles.rowWrap}>
              {(
                [
                  ["1h", "直近1時間"],
                  ["3h", "直近3時間"],
                  ["24h", "直近24時間"],
                  ["all", "全て"],
                ] as const
              ).map(([preset, label]) => (
                <span key={preset} className={styles.fixedSlotPeriod}>
                  <FilterButton
                    active={period.kind === "preset" && period.preset === preset}
                    color={c.pr}
                    onClick={() => changePeriodPreset(preset)}
                  >
                    {t(label)}
                  </FilterButton>
                </span>
              ))}
              <input
                type="date"
                className={styles.dateInput}
                aria-label={t("開始日")}
                value={period.kind === "dates" ? period.start : ""}
                onChange={(e) => changeDate("start", e.target.value)}
                onClick={openDatePicker}
              />
              <span className={styles.dateSep}>–</span>
              <input
                type="date"
                className={styles.dateInput}
                aria-label={t("終了日")}
                value={period.kind === "dates" ? period.end : ""}
                onChange={(e) => changeDate("end", e.target.value)}
                onClick={openDatePicker}
              />
            </div>
          </section>

          <section className={styles.section}>
            <div className={styles.sectionLabel}>{t("並べ替え")}</div>
            <div className={styles.rowWrap}>
              <span className={styles.fixedSlotSort}>
                <FilterButton
                  active={sort === "date_desc"}
                  color={c.pr}
                  onClick={() => {
                    setSort("date_desc");
                    setPage(1);
                  }}
                >
                  {t("日付")}
                </FilterButton>
              </span>
              <span className={styles.fixedSlotSort}>
                <FilterButton
                  active={sort === "type_date_desc"}
                  color={c.pr}
                  onClick={() => {
                    setSort("type_date_desc");
                    setPage(1);
                  }}
                >
                  {t("種類＋日付")}
                </FilterButton>
              </span>
            </div>
          </section>

          {truncated && !loading && !listError && (
            <div className={styles.notice}>
              {t("件数が多いため、新しいものから一部だけを表示しています")}
            </div>
          )}

          {loading && <div className={styles.message}>{t("読み込み中…")}</div>}
          {!loading && listError && <div className={styles.error}>{t(listError)}</div>}
          {!loading && !listError && items.length === 0 && (
            <div className={styles.message}>{t("該当するファイルはありません")}</div>
          )}

          {!loading && !listError && items.length > 0 && (
            <div className={styles.tableWrap}>
              <table className={styles.table}>
                <thead>
                  <tr>
                    {/* ★チェックの列はチェックボックスだけの幅★（ユーザー指示 2026-11-06「チェックボックスとファイル名が間があきすぎ」）。
                        「このページをすべて選択」は見出しに文字で出さず、読み上げ（aria-label）とマウスを載せたときの title で出す */}
                    <th className={styles.thCheck}>
                      <input
                        type="checkbox"
                        checked={pageAllSelected}
                        onChange={togglePageAll}
                        aria-label={t("このページをすべて選択")}
                        title={t("このページをすべて選択")}
                      />
                    </th>
                    <th className={styles.th}>{t("ファイル名")}</th>
                    <th className={styles.th}>{t("種類")}</th>
                    <th className={styles.th}>{t("大きさ")}</th>
                    <th className={styles.th}>{t("更新日時")}</th>
                  </tr>
                </thead>
                <tbody>
                  {items.map((item) => (
                    <tr key={item.file_id}>
                      <td className={styles.tdCheck}>
                        <input
                          type="checkbox"
                          checked={selection.has(item.file_id)}
                          onChange={() => toggleFile(item)}
                          aria-label={item.name}
                        />
                      </td>
                      <td className={styles.tdName} title={item.name}>
                        {item.name}
                      </td>
                      <td className={styles.td}>
                        {typeLabels.get(item.type) ?? item.type}
                      </td>
                      <td className={styles.tdSize}>{formatFileSize(item.size_bytes)}</td>
                      <td className={styles.tdDate}>{formatDateTime(item.modified_at)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}

          {!loading && !listError && (
            <div className={styles.pagerRow}>
              <Pagination
                total={total}
                page={page}
                perPage={perPage}
                onPageChange={setPage}
              />
              <label className={styles.perPage}>
                <span>{t("ページあたり")}</span>
                <select
                  value={perPage}
                  onChange={(e) => {
                    setPerPage(Number(e.target.value));
                    setPage(1);
                  }}
                >
                  {DS_LOG_PER_PAGE_OPTIONS.map((n) => (
                    <option key={n} value={n}>
                      {n}
                    </option>
                  ))}
                </select>
              </label>
            </div>
          )}

          <div className={styles.selectionBar}>
            <span>
              {t("選択: {n}件・合計 {size}")
                .replace("{n}", String(selectedCount))
                .replace("{size}", formatFileSize(selectedBytes))}
            </span>
            {overMaxBytes && (
              <span className={styles.warn}>
                {t("1回に取り出せるのは {max} までです").replace(
                  "{max}",
                  formatFileSize(EXPORT_MAX_BYTES)
                )}
              </span>
            )}
            {overMaxFiles && (
              <span className={styles.warn}>
                {t("1回に選べるのは {n} 件までです").replace(
                  "{n}",
                  String(EXPORT_MAX_FILES)
                )}
              </span>
            )}
            <button
              type="button"
              className={styles.exportButton}
              disabled={!canExport}
              onClick={() => void startExport()}
            >
              {exporting ? t("制御盤で準備中…") : t("エクスポート")}
            </button>
          </div>
          {exportMessage && <div className={styles.success}>{exportMessage}</div>}
          {exportError && <div className={styles.error}>{exportError}</div>}

          <section className={styles.tokenSection}>
            <div className={styles.tokenHeader}>
              <span className={styles.sectionLabel}>{t("制御盤トークン")}</span>
              {tokenLoading ? (
                <span className={styles.hint}>{t("読み込み中…")}</span>
              ) : tokenIssuedAt ? (
                <span className={styles.hint}>
                  {t("発行日時 {日時}").replace("{日時}", formatDateTime(tokenIssuedAt))}
                </span>
              ) : (
                <span className={styles.hint}>{t("未発行")}</span>
              )}
            </div>
            <div className={styles.tokenActions}>
              <button
                type="button"
                className={styles.tokenButton}
                disabled={tokenBusy}
                onClick={() => void issueToken()}
              >
                {t("発行（作り直し）")}
              </button>
              <button
                type="button"
                className={styles.tokenButton}
                disabled={tokenBusy || tokenIssuedAt === null}
                onClick={() => void revokeToken()}
              >
                {t("失効")}
              </button>
            </div>
            {tokenValue !== null && (
              <div className={styles.tokenReveal}>
                <input
                  className={styles.tokenInput}
                  type="text"
                  readOnly
                  value={tokenValue}
                  aria-label={t("制御盤トークン")}
                />
                <button
                  type="button"
                  className={styles.tokenButton}
                  onClick={() => void copyToken()}
                >
                  {t("コピー")}
                </button>
                <p className={styles.tokenWarn}>
                  {t("この値は二度と表示されません。制御盤の設定ファイルに入れてください")}
                </p>
              </div>
            )}
            {tokenError && <div className={styles.error}>{tokenError}</div>}
          </section>
        </div>

        <div className={styles.footer}>
          <button type="button" className={styles.closeBtn} onClick={onClose}>
            {t("閉じる")}
          </button>
        </div>
      </div>
    </div>
  );
}
