// SSE 客户端 — 按契约§2 + 安全红线（不传 JWT 进 URL，先换 ticket 再建 EventSource）
// 特性：ticket 认证 + event_seq 去重 + 最多5次指数退避重连
// 注意：BE 用 named events（event: text_candidate 等），必须用 addEventListener 而非 onmessage
import { SseEvent } from '@/types/sse';
import { api } from './api';
const MAX_RETRIES = 5;
const BASE_DELAY_MS = 1000;
// 与 backend/app/utils/sse_utils.py SSE_EVENT_TYPES 保持同步
const SSE_EVENT_TYPES = [
  'task_started', 'analyze_done', 'text_progress', 'text_candidate',
  'image_progress', 'image_candidate', 'flywheel_injected',
  'batch_failed', 'task_done', 'error', 'heartbeat',
] as const;

export type SseConnectionStatus = 'connecting' | 'connected' | 'reconnecting' | 'failed' | 'closed';

export interface SseClientOptions {
  onEvent: (event: SseEvent) => void;
  onStatusChange: (status: SseConnectionStatus, attempt?: number) => void;
}

export class SseClient {
  private taskId: number;
  private options: SseClientOptions;
  private es: EventSource | null = null;
  private seenSeqs = new Set<number>();
  private retryCount = 0;
  private closed = false;

  constructor(taskId: number, options: SseClientOptions) {
    this.taskId = taskId;
    this.options = options;
  }

  connect(lastSeq = 0): void {
    if (this.closed) return;
    this.options.onStatusChange(this.retryCount === 0 ? 'connecting' : 'reconnecting', this.retryCount);
    this._fetchTicketAndOpen(lastSeq);
  }

  private async _fetchTicketAndOpen(lastSeq: number): Promise<void> {
    if (this.closed) return;
    try {
      const { ticket } = await api.post<{ ticket: string; expires_in: number }>(
        `/api/v1/tasks/${this.taskId}/sse-ticket`
      );
      if (!this.closed) this._openEventSource(ticket, lastSeq);
    } catch {
      if (!this.closed) this.scheduleRetry();
    }
  }

  private _dispatch(e: MessageEvent): void {
    // BE格式：event:<type>\ndata:<json>\nid:<seq>；e.type=named event type
    try {
      const data = JSON.parse(e.data);
      const seq = e.lastEventId ? Number(e.lastEventId) : undefined;
      if (seq !== undefined && !isNaN(seq)) {
        if (this.seenSeqs.has(seq)) return;
        this.seenSeqs.add(seq);
      }
      this.retryCount = 0;
      // 将 named event type 合入 data，构造 SseEvent
      const sseEvent = { type: e.type, event_seq: seq ?? 0, data } as SseEvent;
      this.options.onEvent(sseEvent);
    } catch { /* 忽略解析错误 */ }
  }

  private _openEventSource(ticket: string, lastSeq: number): void {
    // SSE 必须直连后端，绕过 Next dev rewrite 代理——后者会缓冲/吞掉 EventSource 的
    // 流式事件（实测：经代理 EventSource 只触发 open，零事件；直连后端事件正常）。
    // NEXT_PUBLIC_SSE_BASE_URL 缺省时退回 NEXT_PUBLIC_API_BASE_URL，再退回相对路径。
    const sseBase = (
      process.env.NEXT_PUBLIC_SSE_BASE_URL ||
      process.env.NEXT_PUBLIC_API_BASE_URL ||
      ''
    ).replace(/\/$/, '');
    const url = `${sseBase}/api/v1/tasks/${this.taskId}/stream?ticket=${ticket}&last_seq=${lastSeq}`;
    this.es = new EventSource(url);
    this.es.onopen = () => this.options.onStatusChange('connected');
    // 监听所有 named event types（onmessage 收不到 named events）
    for (const evType of SSE_EVENT_TYPES) {
      this.es.addEventListener(evType, (e) => this._dispatch(e as MessageEvent));
    }
    this.es.onerror = () => {
      this.es?.close(); this.es = null;
      if (!this.closed) this.scheduleRetry();
    };
  }

  private scheduleRetry(): void {
    if (this.retryCount >= MAX_RETRIES) { this.options.onStatusChange('failed'); return; }
    const delay = BASE_DELAY_MS * Math.pow(2, this.retryCount);
    const lastSeq = this.seenSeqs.size > 0 ? Math.max(...this.seenSeqs) : 0;
    this.retryCount++;
    setTimeout(() => this.connect(lastSeq), delay);
  }

  close(): void {
    this.closed = true; this.es?.close(); this.es = null;
    this.options.onStatusChange('closed');
  }
}

export function createSseClient(taskId: number, options: SseClientOptions): SseClient {
  const client = new SseClient(taskId, options);
  client.connect();
  return client;
}
