"use client";

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

import { HTTPError, apiFetch, apiFetchWithStatus } from "@/lib/api";
import { abortableSleep } from "@/lib/abortableSleep";
import type { ParcelAnalysis } from "@/types/site-finder";

// #93 — on-demand cadastre fetch flow.
// Backend returns 202 Accepted when geometry needs fetching; frontend polls
// /fetch-status every 2s, re-triggers analyze когда status=ready.

export type FetchStatus =
  | "ready"
  | "fetching"
  | "failed"
  | "not_in_nspd"
  | "invalid_format";

export interface FetchStatusResponse {
  status: FetchStatus;
  job_id: number | null;
  error_msg: string | null;
  eta_seconds: number | null;
}

export interface AnalyzeAcceptedResponse {
  status: "fetching";
  cad_num: string;
  job_id: number;
  eta_seconds: number;
  message: string;
}

export type AnalyzeResult =
  | { kind: "ready"; data: ParcelAnalysis }
  | { kind: "fetching"; jobId: number; etaSeconds: number };

const POLL_INTERVAL_MS = 2000;
const POLL_MAX_ITERATIONS = 60; // 60 × 2s = 2 min hard cap

export interface AnalyzeOptions {
  /** If set, backend uses this saved profile for weights. */
  profileId?: number;
  /** If set together with no profileId, backend uses user's default profile. */
  profileUserId?: string;
  /**
   * Inline POI weights override — sent as request body.
   * Priority: inline → profileId → profileUserId default → system.
   */
  weights?: Record<string, number> | null;
}

/**
 * Custom hook для analyze flow с graceful on-demand fetch fallback.
 *
 * Состояния:
 * - idle: ничего не делали
 * - pending (mutation): первичный POST /analyze в полёте
 * - fetching: получили 202, идёт polling /fetch-status
 * - data set: ParcelAnalysis отрисовываем
 * - error: not_in_nspd / invalid_format / failed
 *
 * Отмена (issue #1242): per-call AbortController. cancel() абортит ТОЛЬКО
 * текущий цикл — новая мутация создаёт свой controller, поэтому
 * "воскресший" poll-цикл предыдущего вызова не может затронуть состояние
 * актуального. setFetchingState вызывается только если signal не aborted —
 * иначе баннер ETA/cancel активного запроса был бы перезаписан зомби-циклом.
 */
export function useSiteAnalysis() {
  const [fetchingState, setFetchingState] = useState<{
    cadNum: string;
    jobId: number;
    etaSeconds: number;
  } | null>(null);
  // Per-call AbortController. cancel() абортит только текущий цикл;
  // конкурентные мутации не делят токен отмены (фикс #1242).
  const abortRef = useRef<AbortController | null>(null);

  const mutation = useMutation({
    mutationFn: async ({
      cad,
      options,
    }: {
      cad: string;
      options?: AnalyzeOptions;
    }): Promise<ParcelAnalysis> => {
      // Абортим предыдущий in-flight цикл (если ещё спит / поллит)
      // и создаём новый controller для текущего вызова.
      abortRef.current?.abort();
      const controller = new AbortController();
      abortRef.current = controller;
      const { signal } = controller;

      setFetchingState(null);

      // Build optional query string for weight profile params.
      const qs = new URLSearchParams();
      if (options?.profileId != null) {
        qs.set("profile_id", String(options.profileId));
      }
      if (options?.profileUserId) {
        qs.set("profile_user_id", options.profileUserId);
      }
      const qsStr = qs.toString();
      const analyzeUrl = (cadNum: string) => {
        const base = `/api/v1/parcels/${encodeURIComponent(cadNum)}/analyze`;
        return qsStr ? `${base}?${qsStr}` : base;
      };

      // Build optional JSON body for inline weights (#201).
      const bodyPayload =
        options?.weights != null
          ? JSON.stringify({ weights: options.weights })
          : undefined;

      // First request — POST /analyze
      const first = await apiFetchWithStatus<
        ParcelAnalysis | AnalyzeAcceptedResponse
      >(analyzeUrl(cad), {
        method: "POST",
        signal,
        ...(bodyPayload
          ? {
              body: bodyPayload,
              headers: { "Content-Type": "application/json" },
            }
          : {}),
      });

      if (first.status === 200) {
        return first.body as ParcelAnalysis;
      }

      // 202 Accepted — entering polling mode
      if (first.status === 202) {
        const accepted = first.body as AnalyzeAcceptedResponse;
        // Guard: не перезаписываем баннер если этот вызов уже отменили
        // (succeeded race: cancel пришёл во время первого POST).
        if (signal.aborted) {
          throw new Error("Загрузка отменена пользователем");
        }
        setFetchingState({
          cadNum: cad,
          jobId: accepted.job_id,
          etaSeconds: accepted.eta_seconds,
        });

        // Poll loop
        for (let i = 0; i < POLL_MAX_ITERATIONS; i++) {
          await abortableSleep(POLL_INTERVAL_MS, signal);
          if (signal.aborted) {
            throw new Error("Загрузка отменена пользователем");
          }
          const status = await apiFetch<FetchStatusResponse>(
            `/api/v1/parcels/${encodeURIComponent(cad)}/fetch-status`,
            { signal },
          );
          if (status.status === "ready") {
            // Re-trigger analyze — should be 200 now. Используем
            // apiFetchWithStatus, чтобы НЕ принять 202-stub за готовый
            // ParcelAnalysis (#1339): re-POST может вернуть 202 Accepted
            // (геометрия ready, но analyze-воркер не закончил —
            // задокументированная гонка). 202-stub не имеет score/
            // score_breakdown → крашит рендер Секций 3/4. На 202 продолжаем
            // polling (symmetry с первым запросом), как в useParcelAnalyzeQuery.
            // setFetchingState(null) ПОСЛЕ await чтобы не было flicker:
            // если очистить до await, mutation isPending=true но
            // fetchingState=null → пустой экран ~1 RTT. После await
            // mutation сразу резолвится с data — render skipped.
            const second = await apiFetchWithStatus<
              ParcelAnalysis | AnalyzeAcceptedResponse
            >(analyzeUrl(cad), {
              method: "POST",
              signal,
              ...(bodyPayload
                ? {
                    body: bodyPayload,
                    headers: { "Content-Type": "application/json" },
                  }
                : {}),
            });
            if (second.status === 200) {
              // Только если этот вызов всё ещё актуален — иначе зомби-цикл
              // стирает баннер свежей мутации (#1242).
              if (!signal.aborted) {
                setFetchingState(null);
              }
              return second.body as ParcelAnalysis;
            }
            // 202 race → fall through and poll /fetch-status again.
            continue;
          }
          if (status.status === "not_in_nspd") {
            if (!signal.aborted) setFetchingState(null);
            throw new HTTPError(
              404,
              status,
              status.error_msg ?? "Кадастровый номер не найден в НСПД",
            );
          }
          if (status.status === "failed") {
            if (!signal.aborted) setFetchingState(null);
            throw new HTTPError(
              503,
              status,
              status.error_msg ?? "НСПД временно недоступен",
            );
          }
          if (status.status === "invalid_format") {
            if (!signal.aborted) setFetchingState(null);
            throw new HTTPError(
              400,
              status,
              status.error_msg ?? "Неверный формат кадастрового номера",
            );
          }
          if (status.status === "fetching") {
            // continue polling
            continue;
          }
          // Неизвестный status (#1485): рассинхрон фронт/бэк — опечатка или
          // новый/переименованный статус вне закрытого union FetchStatus.
          // Бросаем явную ошибку вместо молчаливого polling до 2-мин таймаута
          // (что вводило бы в заблуждение «загрузка слишком долгая»).
          if (!signal.aborted) setFetchingState(null);
          throw new Error(`Неизвестный статус загрузки: ${status.status}`);
        }
        // Polling exhausted (2 min)
        if (!signal.aborted) setFetchingState(null);
        throw new Error(
          "Загрузка длится слишком долго (>2 мин). Попробуйте позже.",
        );
      }

      throw new Error(`Unexpected response status: ${first.status}`);
    },
  });

  const cancel = useCallback(() => {
    abortRef.current?.abort();
    abortRef.current = null;
    setFetchingState(null);
    mutation.reset();
  }, [mutation]);

  return {
    ...mutation,
    fetchingState,
    cancel,
  };
}
