// Late-response polling (t-paliad-235 rewrite).
//
// When the SSE stream closes mid-turn with an error event, the bubble
// can't tell from the wire whether (a) the upstream is still finishing
// the turn and we just lost transport, or (b) the upstream is truly
// dead.
//
// This module hits the dispatching recovery endpoint
// `/api/paliadin/turns/{id}/recover`, which knows the active backend:
//
//   - aichat backend  → asks aichat via its conversation API whether
//                       the turn actually completed upstream
//   - legacy backend  → reads the local row (paliad's filesystem
//                       janitor patches it when claude writes the
//                       response file late)
//
// The endpoint returns:
//
//   recovery_state="recovered" → response is in the payload, render it
//   recovery_state="pending"   → keep polling
//   recovery_state="lost"      → upstream is truly gone, give up

export interface LateTurn {
  turn_id: string;
  response: string | null;
  error_code: string | null;
  finished_at: string | null;
  duration_ms: number | null;
  used_tools: string[];
  rows_seen: number[];
  chip_count: number;
  classifier_tag: string | null;
}

export interface LatePollOptions {
  turnId: string;
  intervalMs?: number;     // default 3000
  maxDurationMs?: number;  // default 600000 (10 min)
  onLateResponse: (turn: LateTurn) => void;
  // onLost — backend confirmed the turn is unrecoverable. Caller should
  // swap the bubble copy to the "verloren" string. Distinct from
  // onGiveUp (which fires only on the local timeout).
  onLost?: () => void;
  onGiveUp?: () => void;
}

export interface LatePollHandle {
  cancel: () => void;
}

interface RecoverResponse {
  turn_id: string;
  started_at: string;
  response: string | null;
  error_code: string | null;
  finished_at: string | null;
  duration_ms: number | null;
  used_tools: string[];
  rows_seen: number[];
  chip_count: number;
  classifier_tag: string | null;
  recovery_state: "recovered" | "pending" | "lost";
}

export function pollForLateResponse(opts: LatePollOptions): LatePollHandle {
  const interval = opts.intervalMs ?? 3000;
  const maxDuration = opts.maxDurationMs ?? 10 * 60 * 1000;
  const startedAt = Date.now();

  let cancelled = false;
  let timer: number | undefined;

  const tick = async () => {
    if (cancelled) return;
    if (Date.now() - startedAt > maxDuration) {
      opts.onGiveUp?.();
      return;
    }
    try {
      const r = await fetch(`/api/paliadin/turns/${opts.turnId}/recover`, {
        credentials: "same-origin",
      });
      if (r.ok) {
        const body = (await r.json()) as RecoverResponse;
        if (body.recovery_state === "recovered" && body.response) {
          opts.onLateResponse(toLateTurn(body));
          return;
        }
        if (body.recovery_state === "lost") {
          opts.onLost?.();
          return;
        }
        // pending — keep polling
      } else if (r.status === 404) {
        // Row gone — give up. Different signal from `lost`: a missing row
        // is a paliad-side bookkeeping problem; aichat may still have the
        // answer but we can't surface it without the row.
        opts.onGiveUp?.();
        return;
      }
    } catch {
      // Transient network error; retry on next tick.
    }
    timer = window.setTimeout(tick, interval);
  };

  // First poll deliberately runs after one interval so we don't race
  // the dispatch endpoint on the very first tick (gives the upstream a
  // moment to actually settle the row after the stream drop).
  timer = window.setTimeout(tick, interval);

  return {
    cancel: () => {
      cancelled = true;
      if (timer != null) window.clearTimeout(timer);
    },
  };
}

function toLateTurn(body: RecoverResponse): LateTurn {
  return {
    turn_id: body.turn_id,
    response: body.response,
    error_code: body.error_code,
    finished_at: body.finished_at,
    duration_ms: body.duration_ms,
    used_tools: body.used_tools ?? [],
    rows_seen: body.rows_seen ?? [],
    chip_count: body.chip_count ?? 0,
    classifier_tag: body.classifier_tag,
  };
}
