"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";

import { API_BASE_URL, apiFetch, apiFetchWithStatus } from "@/lib/api";
import type { components } from "@/lib/api-types";

// Reuse generated OpenAPI types (api-types.ts) — НЕ дублировать руками (#2259 PR-D).
export type ReportBuildResponse = components["schemas"]["ReportBuildResponse"];
export type ReportStatusResponse =
  components["schemas"]["ReportStatusResponse"];

/**
 * useFullReport — единый хук кнопки «Полный отчёт (PDF)» (#2259 PR-E).
 *
 * Переиспользуется двумя компонентами (ptica/reports/PticaReports «тёмная»
 * витрина экспортов + analysis/ExportButtons «светлая» страница) — вся логика
 * enqueue → poll → download живёт здесь, компоненты только рендерят состояние.
 *
 * Контракт бэкенда (PR-D #2287):
 *   • POST /api/v1/parcels/{cad}/report
 *       → 202 { status: "building" }  джоба поставлена, PDF собирается в фоне
 *       → 200 { status: "ready", ... } готовый кэш уже на диске, качать сразу
 *   • GET  /api/v1/parcels/{cad}/report/status
 *       → { status: "none" | "building" | "ready", report_generated_at?, ... }
 *   • GET  /api/v1/parcels/{cad}/report/download?format=pdf|docx
 *       → application/pdf | application/vnd.openxmlformats-…docx (attachment)
 *
 * Один report-билд даёт ОБА файла (PDF + DOCX) — status=ready означает, что оба
 * готовы для свежих ранов (#2259 R2 PR-F). Формат влияет только на финальный
 * download: и PDF-, и DOCX-карточка гоняют ОДИН общий прогон enqueue→poll,
 * а какой файл скачать в конце — запоминает pendingFormatRef (кто кликнул — тот
 * формат и качается). Отдельного state под формат НЕТ.
 *
 * Легаси-раны (собранные до PR-F) не несут docx_path → download?format=docx даёт
 * 404. Чтобы <a download> не «молча» скачивал HTML-страницу ошибки, DOCX-скачивание
 * идёт через HEAD-preflight (triggerReportDownload с preflight): 404 → onDownloadError
 * («Пересоберите отчёт»), ok → навигация <a>. PDF-путь preflight не нужен (файл всегда
 * есть при ready), остаётся прямой <a>-навигацией.
 *
 * «failed»-состояния в контракте НЕТ: если building висит дольше POLL_CAP_MS,
 * фронт сам останавливает поллинг и показывает «Сборка затянулась — попробуйте
 * позже» (uiState === "timeout"), кнопка снова активна.
 *
 * Скачивание: страница за Caddy basic_auth. Обычная навигация браузера к
 * /report/download наследует уже авторизованную сессию (cookie/basic-auth),
 * поэтому качаем через скрытый <a href download> (см. triggerReportDownload) —
 * не тянем blob отдельным fetch'ем, который пришлось бы отдельно авторизовывать.
 */

// Поллинг: тик каждые 5с, cap ~3 мин (36 тиков) → стоп + сообщение.
const POLL_INTERVAL_MS = 5_000;
const POLL_CAP_MS = 3 * 60 * 1000;

/** Формат финального download'а. Один прогон enqueue→poll — формат только у скачивания. */
export type ReportFormat = "pdf" | "docx";

export type FullReportUiState =
  | "idle" // ничего не запускалось / ещё не готов
  | "building" // джоба идёт, поллим status
  | "ready" // PDF готов, есть report_generated_at
  | "timeout" // building провисел дольше cap — остановились
  | "error"; // POST enqueue упал

export interface UseFullReportResult {
  uiState: FullReportUiState;
  /** ISO дата генерации готового отчёта (для подписи «Скачать отчёт (дата)»). */
  reportGeneratedAt: string | null;
  /** Прошедшее с начала сборки время, мс (для «Готовится… мм:сс»). */
  elapsedMs: number;
  /**
   * Клик по кнопке: enqueue → сразу download в `format` если ready, иначе старт
   * поллинга (формат запоминается — финальный авто-download будет в нём же).
   * format по умолчанию "pdf" — обратная совместимость со старыми вызовами.
   */
  start: (format?: ReportFormat) => void;
  /** Ручное скачивание готового отчёта в `format` (кнопка «Скачать …»). */
  download: (format?: ReportFormat) => void;
  /**
   * Легаси-раны без docx: download?format=docx вернул 404 → сообщение
   * «Пересоберите отчёт». null пока ошибки нет. Сбрасывается на новом start().
   */
  downloadError: string | null;
}

function buildReportPath(cad: string, suffix: string): string {
  return `/api/v1/parcels/${encodeURIComponent(cad)}/report${suffix}`;
}

/** URL download'а с ?format= (pdf — дефолт бэкенда, не гоним лишний query). */
function downloadUrl(cad: string, format: ReportFormat): string {
  const base = `${API_BASE_URL}${buildReportPath(cad, "/download")}`;
  return format === "docx" ? `${base}?format=docx` : base;
}

/**
 * Навигация браузера к /report/download через скрытый <a download>. Наследует
 * Caddy basic_auth сессию текущей вкладки — отдельный авторизованный fetch не
 * нужен. Browser-only (использует document) — вызывать только из обработчика.
 */
function triggerAnchorDownload(cad: string, format: ReportFormat): void {
  if (typeof document === "undefined") return;
  const a = document.createElement("a");
  a.href = downloadUrl(cad, format);
  // Content-Disposition: attachment на бэкенде задаёт имя файла; download-атрибут
  // без значения лишь подтверждает намерение скачать, не переопределяя имя.
  a.download = "";
  a.rel = "noopener";
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
}

/**
 * Скачать отчёт в `format`. PDF-путь — прямая <a>-навигация (файл всегда есть при
 * ready). DOCX-путь — HEAD-preflight: легаси-раны без docx_path отдают 404, а
 * <a download> на 404 «молча» скачал бы HTML-страницу ошибки; ловим здесь и зовём
 * onError («Пересоберите отчёт»). HEAD дешевле GET (тело не тянется) и наследует
 * ту же basic_auth сессию, что и <a>. Сеть упала — не блокируем: пробуем <a>.
 */
async function triggerReportDownload(
  cad: string,
  format: ReportFormat,
  onError: (message: string) => void,
): Promise<void> {
  if (format === "pdf") {
    triggerAnchorDownload(cad, "pdf");
    return;
  }
  try {
    let res = await fetch(downloadUrl(cad, "docx"), { method: "HEAD" });
    // 5xx сразу после status=ready — транзиентная proxy-гонка (стресс-свип
    // 2026-07-04: первый HEAD после ready ловил 502, ретрей через ~1.5с — 200).
    // Один ретрай, прежде чем показывать пользователю ошибку.
    if (!res.ok && res.status !== 404) {
      await new Promise((r) => setTimeout(r, 1500));
      res = await fetch(downloadUrl(cad, "docx"), { method: "HEAD" });
    }
    if (res.status === 404) {
      // Легаси-ран без docx_path — честная просьба пересобрать.
      onError("Пересоберите отчёт — DOCX для старого прогона отсутствует");
      return;
    }
    if (!res.ok) {
      // Устойчивый не-OK (5xx и после ретрая) — не даём <a> молча скачать
      // страницу ошибки, но и НЕ просим пересобрать (файл скорее всего цел).
      onError("Не удалось скачать отчёт — повторите через минуту");
      return;
    }
  } catch {
    // Сетевая ошибка preflight'а (offline / CORS) — не проглатываем download:
    // отдаём <a>-навигации, пусть браузер сам разрулит.
  }
  triggerAnchorDownload(cad, "docx");
}

export function useFullReport(cad: string): UseFullReportResult {
  // Флаг активного поллинга — заводится на building, гасится на ready/timeout.
  const [polling, setPolling] = useState(false);
  const [timedOut, setTimedOut] = useState(false);
  const [elapsedMs, setElapsedMs] = useState(0);
  // Легаси-404 при download?format=docx — сообщение «Пересоберите отчёт».
  const [downloadError, setDownloadError] = useState<string | null>(null);
  // Момент начала сборки — источник истины для elapsed и cap.
  const startedAtRef = useRef<number | null>(null);
  // На ready один раз авто-скачиваем — иначе повторные тики status дёргали бы
  // загрузку снова.
  const autoDownloadedRef = useRef(false);
  // Формат финального авто-download'а — «кто кликнул, тот формат и качается».
  // Ref (не state): читается только внутри обработчиков/эффекта, ре-рендер под
  // него не нужен, и это держит один общий прогон без дубля состояния формата.
  const pendingFormatRef = useRef<ReportFormat>("pdf");

  // Стабильный колбэк ошибки docx-download'а для triggerReportDownload.
  const handleDownloadError = useCallback((message: string) => {
    setDownloadError(message);
  }, []);

  // ── POST enqueue ────────────────────────────────────────────────────────────
  const enqueue = useMutation({
    mutationFn: async (): Promise<ReportBuildResponse> => {
      // apiFetchWithStatus терпит 202 Accepted без throw'а (как async-джобы §22).
      const { body } = await apiFetchWithStatus<ReportBuildResponse>(
        buildReportPath(cad, ""),
        { method: "POST" },
      );
      return body;
    },
  });

  // ── GET status polling ────────────────────────────────────────────────────────
  const statusQuery = useQuery({
    queryKey: ["full-report-status", cad],
    queryFn: (): Promise<ReportStatusResponse> =>
      apiFetch<ReportStatusResponse>(buildReportPath(cad, "/status")),
    // enabled=false вне активной сборки → нет фонового поллинга и утечки после
    // ухода со страницы (unmount снимает подписку, а enabled сам гасит тик).
    enabled: !!cad && polling,
    // Тик каждые POLL_INTERVAL_MS пока building; на ready — стоп (false).
    refetchInterval: (query) =>
      query.state.data?.status === "ready" ? false : POLL_INTERVAL_MS,
    refetchIntervalInBackground: false,
    gcTime: 0,
  });

  const statusData = statusQuery.data;

  // ── Тикающий elapsed + cap-стоп (только пока идёт поллинг) ────────────────────
  useEffect(() => {
    if (!polling) return;
    if (startedAtRef.current === null) startedAtRef.current = Date.now();

    const tick = () => {
      const started = startedAtRef.current;
      if (started === null) return;
      const el = Date.now() - started;
      setElapsedMs(el);
      if (el >= POLL_CAP_MS) {
        setPolling(false);
        setTimedOut(true);
      }
    };
    tick();
    const id = window.setInterval(tick, 1000);
    // Cleanup на unmount / смене polling → интервал не течёт после ухода.
    return () => window.clearInterval(id);
  }, [polling]);

  // ── На ready: авто-download один раз (в запомненном формате) + стоп поллинга ───
  useEffect(() => {
    if (statusData?.status === "ready" && polling) {
      setPolling(false);
      if (!autoDownloadedRef.current) {
        autoDownloadedRef.current = true;
        void triggerReportDownload(
          cad,
          pendingFormatRef.current,
          handleDownloadError,
        );
      }
    }
  }, [statusData?.status, polling, cad, handleDownloadError]);

  // ── Смена участка: полный сброс прогона ──────────────────────────────────────
  // Оба потребителя живут под route-сегментом [cad] (смена участка = remount),
  // этот эффект — страховка на случай будущего in-place реюза хука с другим cad.
  useEffect(() => {
    setPolling(false);
    setTimedOut(false);
    setElapsedMs(0);
    setDownloadError(null);
    autoDownloadedRef.current = false;
    startedAtRef.current = null;
    pendingFormatRef.current = "pdf";
  }, [cad]);

  // ── Действие: клик по кнопке ──────────────────────────────────────────────────
  const start = useCallback(
    (format: ReportFormat = "pdf") => {
      // Сброс прошлого прогона (retry после timeout/error) + запомнить формат
      // финального download'а: кто кликнул PDF/DOCX — тот файл и скачается.
      pendingFormatRef.current = format;
      setTimedOut(false);
      setElapsedMs(0);
      setDownloadError(null);
      startedAtRef.current = null;
      autoDownloadedRef.current = false;

      enqueue.mutate(undefined, {
        onSuccess: (data) => {
          if (data.status === "ready") {
            // Кэш готов — качаем сразу, поллинг не нужен.
            autoDownloadedRef.current = true;
            void triggerReportDownload(cad, format, handleDownloadError);
          } else {
            // building → стартуем поллинг status.
            startedAtRef.current = Date.now();
            setPolling(true);
          }
        },
      });
    },
    [enqueue, cad, handleDownloadError],
  );

  const download = useCallback(
    (format: ReportFormat = "pdf") => {
      setDownloadError(null);
      void triggerReportDownload(cad, format, handleDownloadError);
    },
    [cad, handleDownloadError],
  );

  // ── Производное UI-состояние ──────────────────────────────────────────────────
  let uiState: FullReportUiState = "idle";
  if (statusData?.status === "ready" || enqueue.data?.status === "ready") {
    uiState = "ready";
  } else if (timedOut) {
    uiState = "timeout";
  } else if (polling || enqueue.isPending) {
    uiState = "building";
  } else if (enqueue.isError) {
    uiState = "error";
  }

  const reportGeneratedAt =
    (uiState === "ready"
      ? (statusData?.report_generated_at ?? enqueue.data?.report_generated_at)
      : null) ?? null;

  return {
    uiState,
    reportGeneratedAt,
    elapsedMs,
    start,
    download,
    downloadError,
  };
}

// ── Хелперы микрокопии кнопки (общие для обоих компонентов) ────────────────────

/** «мм:сс» из миллисекунд — для «Готовится… 1:05». */
export function formatElapsed(ms: number): string {
  const totalSec = Math.floor(ms / 1000);
  const min = Math.floor(totalSec / 60);
  const sec = totalSec % 60;
  return `${min}:${String(sec).padStart(2, "0")}`;
}

/** «Скачать отчёт (дата)»; на fast-path из кэша даты ещё нет — без пустых скобок. */
export function readyReportLabel(iso: string | null): string {
  const d = formatReportDate(iso);
  return d ? `Скачать отчёт (${d})` : "Скачать отчёт";
}

/** «Скачать DOCX (дата)» — параллель readyReportLabel для DOCX-карточки. */
export function readyDocxLabel(iso: string | null): string {
  const d = formatReportDate(iso);
  return d ? `Скачать DOCX (${d})` : "Скачать DOCX";
}

/** ISO → «ru-RU» дата генерации отчёта (репо-конвенция, NaN-fallback). */
export function formatReportDate(iso: string | null): string {
  if (!iso) return "";
  const d = new Date(iso);
  // new Date(мусор) не бросает, а даёт Invalid Date — try/catch тут мёртв.
  if (Number.isNaN(d.getTime())) return "";
  return d.toLocaleDateString("ru-RU", {
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
  });
}
