import type {
  AgentChatResponse,
  AgentChatResultResponse,
  AgentRunProvenance,
  ChatMessage,
  TraceEvent
} from "../types/domain";
import { api } from "../services/api/client";
import type { ActivityRef, ActivityRefSource } from "../services/api/client";
import { WORKBENCH_PROJECT_ID, normalizeWorkbenchProjectId } from "./constants";

export interface TraceSnapshot {
  traceId?: string;
  status?: string;
  sessionId?: string | null;
  threadId?: string | null;
  events?: TraceEvent[];
  eventCount?: number;
  eventsCompacted?: boolean;
  agentRun?: AgentRunProvenance;
  traceStatus?: string;
  retention?: unknown;
  terminalEvidence?: unknown;
  finalResponse?: unknown;
  traceSummary?: unknown;
  lastEventLabel?: string;
  waitingFor?: string;
  updatedAt?: string;
}

const TRACE_POLL_INTERVAL_MS = 1500;
const TRACE_REPLAY_RETRY_INTERVAL_MS = 500;
const TRACE_SNAPSHOT_RETRY_INTERVAL_MS = 500;
const TRACE_SNAPSHOT_MAX_TIMEOUT_MS = 30000;

export function isTerminalStatus(status: string | undefined): boolean {
  if (!status) return false;
  return ["completed", "failed", "blocked", "timeout", "cancelled"].includes(String(status));
}

export function isResultUrlStatus(response: AgentChatResponse): boolean {
  return Boolean(response.resultUrl) && (response.status === "running" || response.status === "accepted" || isTerminalStatus(response.status));
}

function traceUrlFromResultUrl(resultUrl: string, traceId: string, projectId = WORKBENCH_PROJECT_ID): string {
  const url = resultUrl.replace(/\/v1\/agent\/chat\/result\/[^/?#]+/u, `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`);
  if (/[?&]projectId=/u.test(url)) return url;
  return `${url}${url.includes("?") ? "&" : "?"}projectId=${encodeURIComponent(normalizeWorkbenchProjectId(projectId) ?? WORKBENCH_PROJECT_ID)}`;
}

export function mergeRunnerTrace(previous: ChatMessage["runnerTrace"], next: NonNullable<ChatMessage["runnerTrace"]>): NonNullable<ChatMessage["runnerTrace"]> {
  if (!previous) return next;
  const previousEvents = Array.isArray(previous.events) ? previous.events : [];
  const nextEvents = Array.isArray(next.events) ? next.events : [];
  const previousCount = previousEvents.length;
  const nextCount = nextEvents.length;
  const merged = nextCount >= previousCount ? nextEvents : [...previousEvents, ...nextEvents];
  return {
    ...previous,
    ...next,
    events: merged,
    eventCount: next.eventCount ?? previous.eventCount ?? merged.length,
    lastEventLabel: next.lastEventLabel ?? previous.lastEventLabel ?? (merged[merged.length - 1]?.label ?? merged[merged.length - 1]?.type),
    updatedAt: next.updatedAt ?? previous.updatedAt ?? new Date().toISOString()
  };
}

export function snapshotToRunnerTrace(snapshot: TraceSnapshot): NonNullable<ChatMessage["runnerTrace"]> {
  const events = Array.isArray(snapshot.events) ? snapshot.events : [];
  return {
    traceId: snapshot.traceId,
    status: snapshot.status,
    sessionId: snapshot.sessionId ?? undefined,
    threadId: snapshot.threadId ?? undefined,
    events,
    eventCount: snapshot.eventCount ?? events.length,
    eventsCompacted: snapshot.eventsCompacted,
    traceStatus: snapshot.traceStatus,
    retention: snapshot.retention,
    terminalEvidence: snapshot.terminalEvidence,
    finalResponse: snapshot.finalResponse,
    traceSummary: snapshot.traceSummary,
    agentRun: snapshot.agentRun,
    runnerKind: snapshot.agentRun?.adapter,
    sessionMode: snapshot.agentRun?.backendProfile,
    lastEventLabel: snapshot.lastEventLabel ?? events.at(-1)?.label ?? events.at(-1)?.type,
    waitingFor: snapshot.waitingFor,
    updatedAt: snapshot.updatedAt ?? new Date().toISOString()
  };
}

export function mergeTraceResults(terminal: AgentChatResultResponse, trace: TraceSnapshot | null): AgentChatResultResponse {
  if (!trace) return terminal;
  const traceEvents = Array.isArray(trace.events) ? trace.events : [];
  const terminalEvents = Array.isArray((terminal as { events?: TraceEvent[] }).events) ? ((terminal as { events?: TraceEvent[] }).events ?? []) : [];
  const terminalRunnerTrace = terminal.runnerTrace && typeof terminal.runnerTrace === "object" ? terminal.runnerTrace : null;
  const terminalRunnerEvents = Array.isArray(terminalRunnerTrace?.events) ? terminalRunnerTrace.events : [];
  const events = longestTraceEvents(traceEvents, terminalEvents, terminalRunnerEvents);
  const mergedTrace = { ...trace, events, eventCount: trace.eventCount ?? events.length };
  const assistantText = bestAssistantTextCandidate([
    { text: finalResponseText(trace.finalResponse), priority: 40 },
    { text: terminalAssistantTextFromTraceEvents(events), priority: 30 },
    { text: firstNonEmptyResultText(terminal), priority: 20 },
    { text: assistantTextFromTraceEvents(events), priority: 10 }
  ]);
  return {
    ...terminal,
    traceId: trace.traceId ?? terminal.traceId,
    sessionId: trace.sessionId ?? terminal.sessionId,
    threadId: trace.threadId ?? terminal.threadId,
    agentRun: trace.agentRun ?? terminal.agentRun,
    assistantText: assistantText ?? terminal.assistantText,
    runnerTrace: mergeTerminalRunnerTrace(terminalRunnerTrace, mergedTrace),
    traceEvents: terminal.traceEvents ?? events,
    events,
    eventCount: trace.eventCount ?? events.length,
    traceStatus: trace.traceStatus ?? terminal.traceStatus,
    retention: trace.retention ?? terminal.retention,
    terminalEvidence: trace.terminalEvidence ?? terminal.terminalEvidence,
    finalResponse: trace.finalResponse ?? terminal.finalResponse,
    traceSummary: trace.traceSummary ?? terminal.traceSummary,
    lastEventLabel: trace.lastEventLabel ?? terminal.lastEventLabel ?? events.at(-1)?.label
  } as AgentChatResultResponse;
}

function longestTraceEvents(...candidates: TraceEvent[][]): TraceEvent[] {
  return candidates.reduce((best, item) => item.length > best.length ? item : best, [] as TraceEvent[]);
}

function mergeTerminalRunnerTrace(terminalTrace: AgentChatResultResponse["runnerTrace"] | null, trace: TraceSnapshot): AgentChatResultResponse["runnerTrace"] {
  const restored = snapshotToRunnerTrace(trace);
  if (!terminalTrace) return restored;
  const terminalEvents = Array.isArray(terminalTrace.events) ? terminalTrace.events : [];
  const restoredEvents = Array.isArray(restored.events) ? restored.events : [];
  const events = restoredEvents.length >= terminalEvents.length ? restoredEvents : terminalEvents;
  return {
    ...terminalTrace,
    ...restored,
    events,
    eventCount: Math.max(Number(terminalTrace.eventCount ?? 0) || 0, Number(restored.eventCount ?? 0) || 0, events.length),
    eventsCompacted: false,
    fullTraceLoaded: events.length > 0 || restored.fullTraceLoaded === true || terminalTrace.fullTraceLoaded === true
  };
}

function firstNonEmptyResultText(result: AgentChatResultResponse): string | null {
  const reply = result.reply;
  const replyText = typeof reply === "string" ? reply : typeof reply?.content === "string" ? reply.content : null;
  return bestAssistantTextCandidate([
    { text: result.assistantText, priority: 20 },
    { text: replyText, priority: 10 }
  ]);
}

interface AssistantTextCandidate {
  text: string | null | undefined;
  priority: number;
}

function bestAssistantTextCandidate(candidates: AssistantTextCandidate[]): string | null {
  const scored: Array<{ text: string; fidelity: number; priority: number; index: number }> = [];
  candidates.forEach((candidate, index) => {
    const text = firstNonEmptyString(candidate.text);
    if (!text) return;
    scored.push({ text, fidelity: markdownBlockFidelityScore(text), priority: candidate.priority, index });
  });
  const best = scored.reduce<typeof scored[number] | null>((currentBest, current) => {
    if (!currentBest || compareAssistantTextCandidate(current, currentBest) > 0) return current;
    return currentBest;
  }, null);
  return best?.text ?? null;
}

function compareAssistantTextCandidate(
  left: { fidelity: number; priority: number; index: number },
  right: { fidelity: number; priority: number; index: number }
): number {
  if (left.fidelity !== right.fidelity) return left.fidelity - right.fidelity;
  if (left.priority !== right.priority) return left.priority - right.priority;
  return right.index - left.index;
}

function markdownBlockFidelityScore(text: string): number {
  const newlineCount = (text.match(/\n/gu) ?? []).length;
  const headingCount = (text.match(/^#{1,6}\s+/gmu) ?? []).length;
  const listItemCount = (text.match(/^\s*(?:[-*+] |\d+\.\s+)/gmu) ?? []).length;
  const codeFenceCount = (text.match(/^```/gmu) ?? []).length;
  const tableRowCount = (text.match(/^\s*\|.*\|\s*$/gmu) ?? []).length;
  const paragraphBreakCount = (text.match(/\n\s*\n/gu) ?? []).length;
  return Math.min(newlineCount, 24)
    + headingCount * 8
    + listItemCount * 4
    + codeFenceCount * 8
    + tableRowCount * 4
    + paragraphBreakCount * 3;
}

function assistantTextFromTraceEvents(events: TraceEvent[]): string | null {
  const terminal = terminalAssistantTextFromTraceEvents(events);
  if (terminal) return terminal;
  for (let index = events.length - 1; index >= 0; index -= 1) {
    const event = events[index];
    if (!event || !isAssistantTraceEvent(event)) continue;
    const text = firstNonEmptyString(event.message, event.text, event.outputSummary);
    if (text) return text;
  }
  return null;
}

function terminalAssistantTextFromTraceEvents(events: TraceEvent[]): string | null {
  for (let index = events.length - 1; index >= 0; index -= 1) {
    const event = events[index];
    if (!event || !isTerminalAssistantTraceEvent(event)) continue;
    const text = firstNonEmptyString(event.message, event.text, event.outputSummary);
    if (text) return text;
  }
  return null;
}

function isAssistantTraceEvent(event: TraceEvent): boolean {
  const label = String(event.label ?? "");
  return label === "agentrun:assistant:message" || label === "assistant:message" || label === "assistant:completed" || event.type === "assistant" || event.type === "assistant_message";
}

function isTerminalAssistantTraceEvent(event: TraceEvent): boolean {
  const label = String(event.label ?? "");
  if (label === "assistant:completed") return true;
  if (label === "agentrun:assistant:message") return event.replyAuthority === true || event.final === true;
  return event.type === "assistant_message" && (event.status === "completed" || event.final === true || event.terminal === true);
}

function firstNonEmptyString(...values: unknown[]): string | null {
  for (const value of values) {
    if (typeof value !== "string") continue;
    const text = value.trim();
    if (text) return text;
  }
  return null;
}

/**
 * Unified trace subscription (HWLAB #802). One entry point used by both
 * `submitMessage` (new submit) and `hydrate` (re-attach to a running trace
 * after page refresh). The previous `waitForAgentResult` + `pollRunnerTrace`
 * pair was a normal-path-only dual function set; refresh path was passive
 * (no pollRunnerTrace / waitForAgentResult subscription) which is exactly
 * the path divergence that #802 closes.
 *
 * Design contract (path unification + no-false-timeout):
 * - `for(;;)` loop, no total-timeout, no attempt cap.
 * - `onActivity` is called on EVERY successful poll (not just on new
 *   events) so the per-poll inactivity window in `fetchJson` only fires
 *   when the polling itself is broken (network down or backend
 *   unresponsive for `inactivityTimeoutMs`). In normal operation the
 *   1.5s polling cadence keeps `lastActivityAt` fresh and the inactivity
 *   abort never triggers.
 * - Terminal status -> poll trace once before `onComplete` so refresh keeps
 *   a replayable trace panel even when result text arrives first.
 * - 5xx -> keep polling, do NOT call `onActivity` (the inactivity
 *   window should detect the real backend outage).
 * - 4xx (non-5xx) -> `onInfrastructureError` and return.
 * - `signal.aborted` -> silent return, no callbacks.
 */
export interface TraceSubscriptionConfig {
  traceId: string;
  projectId?: string;
  initial: AgentChatResponse;
  onActivity: () => void;
  onSnapshot: (snapshot: TraceSnapshot) => void;
  onComplete: (result: AgentChatResultResponse) => void;
  onInfrastructureError: (error: string) => void;
  signal: AbortSignal;
  inactivityTimeoutMs: number;
}

export async function subscribeToTrace(config: TraceSubscriptionConfig): Promise<void> {
  const { traceId, initial, onActivity, onSnapshot, onComplete, onInfrastructureError, signal, inactivityTimeoutMs } = config;
  const projectId = normalizeWorkbenchProjectId(config.projectId) ?? WORKBENCH_PROJECT_ID;

  // If the initial response is already terminal, still fetch the trace once.
  // Provider fast-fail responses can include final text immediately while the
  // trace endpoint already has the event list needed for refresh persistence.
  if (isTerminalStatus(initial.status)) {
    const traceSnapshot = await fetchTraceSnapshot(traceId, traceId, inactivityTimeoutMs, signal, projectId);
    if (signal.aborted) return;
    if (traceSnapshot) onSnapshot(traceSnapshot);
    onComplete(mergeTraceResults(initial as AgentChatResultResponse, traceSnapshot));
    return;
  }
  if (!initial.resultUrl) {
    onInfrastructureError("Code Agent initial response is missing resultUrl");
    return;
  }

  const resultUrl = initial.resultUrl;
  const traceUrl = traceUrlFromResultUrl(resultUrl, traceId, projectId);
  let lastEventCount = -1;
  let lastStatus: string | null = null;
  let lastUpdatedAt: string | null = null;
  let terminalResultWithText: AgentChatResultResponse | null = null;
  let terminalResultWithoutText: AgentChatResultResponse | null = null;

  for (;;) {
    if (signal.aborted) return;
    await new Promise<void>((resolve) => window.setTimeout(resolve, TRACE_POLL_INTERVAL_MS));
    if (signal.aborted) return;

    const [resultPolled, tracePolled] = await Promise.all([
      api.getAgentChatResult(resultUrl, inactivityTimeoutMs, null),
      api.getAgentChatResult(traceUrl, inactivityTimeoutMs, null)
    ]);
    if (signal.aborted) return;

    if (resultPolled.ok && resultPolled.data) {
      // Every successful poll keeps the activity clock alive so the
      // per-poll inactivity window does not fire on a healthy polling
      // loop (HWLAB #802: this is what makes the path "never trigger
      // a timeout" in normal operation).
      onActivity();
      if (isTerminalStatus(resultPolled.data.status)) {
        if (firstNonEmptyResultText(resultPolled.data)) {
          terminalResultWithText = resultPolled.data;
        } else {
          terminalResultWithoutText = resultPolled.data;
        }
      }
    } else if (!resultPolled.ok && resultPolled.status >= 500) {
      // server hiccup: keep polling, do NOT call onActivity — the
      // inactivity window should detect a real backend outage.
    } else if (!resultPolled.ok) {
      onInfrastructureError(resultPolled.error ?? "Code Agent result poll failed (non-5xx)");
      return;
    }

    if (tracePolled.ok && tracePolled.data) {
      onActivity();
      const events = Array.isArray(tracePolled.data.events) ? tracePolled.data.events : [];
      const status = String(tracePolled.data.status ?? "");
      const eventCount = Number(tracePolled.data.eventCount ?? events.length) || 0;
      const updatedAt: string | null = typeof tracePolled.data.updatedAt === "string" ? tracePolled.data.updatedAt : null;
      if (eventCount !== lastEventCount || status !== lastStatus || updatedAt !== lastUpdatedAt) {
        lastEventCount = eventCount;
        lastStatus = status;
        lastUpdatedAt = updatedAt;
        const lastEvent = events[events.length - 1];
        const snapshot: TraceSnapshot = {
          traceId,
          status: status || undefined,
          sessionId: tracePolled.data.sessionId ?? null,
          threadId: tracePolled.data.threadId ?? null,
          events,
          eventCount,
          eventsCompacted: false,
          agentRun: tracePolled.data.agentRun,
          traceStatus: typeof tracePolled.data.traceStatus === "string" ? tracePolled.data.traceStatus : undefined,
          retention: tracePolled.data.retention,
          terminalEvidence: tracePolled.data.terminalEvidence,
          finalResponse: tracePolled.data.finalResponse,
          traceSummary: tracePolled.data.traceSummary,
          lastEventLabel: String(lastEvent?.label ?? lastEvent?.type ?? tracePolled.data.lastEventLabel ?? ""),
          waitingFor: String(tracePolled.data.waitingFor ?? ""),
          updatedAt: updatedAt ?? new Date().toISOString()
        };
        onSnapshot(snapshot);
      }
      if (terminalResultWithText) {
        const last = events.at(-1);
        const traceSnapshot = snapshotToTraceSnapshot({
          ...tracePolled.data,
          traceId,
          events,
          eventCount,
          lastEventLabel: String(last?.label ?? last?.type ?? "")
        } as AgentChatResultResponse);
        onComplete(mergeTraceResults(terminalResultWithText, traceSnapshot));
        return;
      }
      if (isTerminalStatus(status)) {
        const last = events.at(-1);
        const traceTerminal = {
          ...tracePolled.data,
          traceId,
          events,
          eventCount,
          lastEventLabel: String(last?.label ?? last?.type ?? "")
        } as AgentChatResultResponse;
        const terminal = terminalResultWithoutText ?? traceTerminal;
        const merged = mergeTraceResults(terminal, snapshotToTraceSnapshot(traceTerminal));
        if (firstNonEmptyResultText(merged)) {
          onComplete(merged);
          return;
        }
      }
    } else if (!tracePolled.ok && tracePolled.status >= 500) {
      if (terminalResultWithText) {
        onComplete(terminalResultWithText);
        return;
      }
      // server hiccup: keep polling, no activity update.
    } else if (!tracePolled.ok) {
      if (terminalResultWithText) {
        onComplete(terminalResultWithText);
        return;
      }
      onInfrastructureError(tracePolled.error ?? "Code Agent trace poll failed (non-5xx)");
      return;
    }
  }
}

export async function fetchTraceSnapshot(traceId: string, traceUrlOrId: string, inactivityTimeoutMs: number, signal?: AbortSignal, projectId = WORKBENCH_PROJECT_ID): Promise<TraceSnapshot | null> {
  if (signal?.aborted) return null;
  const url = traceUrlOrId.startsWith("/") ? traceUrlOrId : traceUrl(traceUrlOrId, projectId);
  const totalTimeoutMs = Math.max(1000, Math.min(inactivityTimeoutMs, TRACE_SNAPSHOT_MAX_TIMEOUT_MS));
  const startedAt = Date.now();
  const maxAttempts = Math.max(1, Math.ceil(totalTimeoutMs / TRACE_SNAPSHOT_RETRY_INTERVAL_MS));
  let attempts = 0;

  while (attempts < maxAttempts && Date.now() - startedAt <= totalTimeoutMs) {
    attempts += 1;
    const remainingMs = Math.max(1000, totalTimeoutMs - (Date.now() - startedAt));
    const tracePolled = await api.getAgentChatResult(url, remainingMs, null);
    if (signal?.aborted) return null;
    if (tracePolled.ok && tracePolled.data) {
      const snapshot = snapshotFromTracePoll(traceId, tracePolled.data);
      if (hasTraceEvents(snapshot) || hasTerminalTraceEvidence(snapshot)) return snapshot;
    }
    if (attempts >= maxAttempts || Date.now() - startedAt >= totalTimeoutMs) break;
    await new Promise<void>((resolve) => window.setTimeout(resolve, Math.min(TRACE_SNAPSHOT_RETRY_INTERVAL_MS, totalTimeoutMs - (Date.now() - startedAt))));
  }
  return null;
}

function snapshotFromTracePoll(traceId: string, result: AgentChatResultResponse): TraceSnapshot {
  const events = Array.isArray(result.events) ? result.events : [];
  const last = events.at(-1);
  return snapshotToTraceSnapshot({
    ...result,
    traceId,
    events,
    eventCount: traceSnapshotEventCount(result, events),
    lastEventLabel: String(last?.label ?? last?.type ?? result.lastEventLabel ?? "")
  } as AgentChatResultResponse);
}

function hasTraceEvents(snapshot: TraceSnapshot): boolean {
  return Array.isArray(snapshot.events) && snapshot.events.length > 0;
}

function hasTerminalTraceEvidence(snapshot: TraceSnapshot): boolean {
  const evidence = objectOrNull(snapshot.terminalEvidence);
  const summary = objectOrNull(snapshot.traceSummary);
  return Boolean(
    evidence?.available === true ||
    finalResponseText(snapshot.finalResponse) ||
    positiveNumber(summary?.sourceEventCount ?? summary?.eventCount) > 0
  );
}

function snapshotToTraceSnapshot(result: AgentChatResultResponse): TraceSnapshot {
  const events = Array.isArray((result as { events?: TraceEvent[] }).events) ? ((result as { events?: TraceEvent[] }).events ?? []) : [];
  const traceStatus = firstNonEmptyString(result.traceStatus, result.status);
  return {
    traceId: result.traceId,
    status: traceStatus ?? result.status,
    sessionId: result.sessionId ?? null,
    threadId: result.threadId ?? null,
    events,
    eventCount: traceSnapshotEventCount(result, events),
    traceStatus: typeof result.traceStatus === "string" ? result.traceStatus : undefined,
    retention: result.retention,
    terminalEvidence: result.terminalEvidence,
    finalResponse: result.finalResponse,
    traceSummary: result.traceSummary,
    agentRun: result.agentRun,
    lastEventLabel: typeof result.lastEventLabel === "string" ? result.lastEventLabel : String(events.at(-1)?.label ?? events.at(-1)?.type ?? ""),
    updatedAt: typeof result.updatedAt === "string" ? result.updatedAt : new Date().toISOString()
  };
}

export async function replayFullTrace(traceId: string, totalTimeoutMs = 15000, projectId = WORKBENCH_PROJECT_ID): Promise<TraceSnapshot | null> {
  const url = traceUrl(traceId, projectId);
  const startedAt = Date.now();
  let polled = await api.getAgentChatResult(url, totalTimeoutMs);
  while ((!polled.ok || !polled.data) && Date.now() - startedAt < totalTimeoutMs) {
    await new Promise<void>((resolve) => window.setTimeout(resolve, TRACE_REPLAY_RETRY_INTERVAL_MS));
    polled = await api.getAgentChatResult(url, Math.max(1000, totalTimeoutMs - (Date.now() - startedAt)));
  }
  if (!polled.ok || !polled.data) return null;
  return { ...snapshotFromTracePoll(traceId, polled.data), eventsCompacted: true };
}

function traceUrl(traceId: string, projectId: string): string {
  return `/v1/agent/chat/trace/${encodeURIComponent(traceId)}?projectId=${encodeURIComponent(normalizeWorkbenchProjectId(projectId) ?? WORKBENCH_PROJECT_ID)}`;
}

function traceSnapshotEventCount(result: AgentChatResultResponse, events: TraceEvent[]): number {
  const explicit = positiveNumber(result.eventCount);
  if (explicit > 0 || events.length > 0) return explicit || events.length;
  const summary = objectOrNull(result.traceSummary);
  return positiveNumber(summary?.sourceEventCount ?? summary?.eventCount) || 0;
}

function positiveNumber(value: unknown): number {
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}

function objectOrNull(value: unknown): Record<string, unknown> | null {
  return value && typeof value === "object" ? value as Record<string, unknown> : null;
}

function finalResponseText(value: unknown): string | null {
  const item = objectOrNull(value);
  return firstNonEmptyString(item?.text, item?.content, item?.message);
}
