'use client';

import { useCallback, useRef, useState } from 'react';
import { ApiCode } from '@pac/types';
import { env } from '@/lib/env';
import { tryRefresh } from '@/lib/api-client';
import { useAuthStore } from '@/stores/auth-store';
import type { ScriptSection } from './mock-data';

/**
 * 流式重新生成话术 hook。
 *
 * 后端 SSE 帧:
 *   event: start    \ndata: {invocationId, modelId, promptVersion}
 *   event: partial  \ndata: {structured, sections}            ← 多次
 *   event: done     \ndata: {planScriptId, source, sections, costYuan...}
 *   event: error    \ndata: {message}
 *
 * 用 fetch + ReadableStream 而非 EventSource(EventSource 不支持自定义 header,
 * 鉴权 Bearer token 必须走 Authorization header)。
 */

interface ServerSection {
  // 稳健/标准:固定 4 id(开场白/告知潜在治疗/复查建议/结束回访语);深度:段数不定,id = s0/s1/…(string)
  id: string;
  label: string;
  durationHint: string;
  markdown: string;
}

type FixedSectionId = 'opening' | 'informMissed' | 'reviewAdvice' | 'closing';

/** 深度档可见过程的步骤(规划→撰写→自检→[修订])*/
export interface DeepStep {
  step: 'plan' | 'write' | 'verify' | 'repair';
  status: 'running' | 'done';
  detail?: {
    outline?: { title: string; intent: string }[];
    pass?: boolean;
    quality?: { natural: number; warmth: number; focus: number; nonPushy: number; overall: number } | null;
    issuesCount?: number;
    issues?: { section: string; problem: string; fix: string }[];
  };
}

export type ScriptStreamState =
  | { status: 'idle' }
  | { status: 'streaming'; sections: ScriptSection[]; steps?: DeepStep[]; modelId?: string; invocationId?: string }
  | {
      status: 'done';
      sections: ScriptSection[];
      steps?: DeepStep[];
      planScriptId: string | null;
      source: 'agent' | 'template_fallback' | 'failed';
      /** ⚠️ 企微的 done 不带成本/token(只推步骤+正文)→ 可选。⛔ 别在下游直接 .toFixed() */
      costYuan?: number;
      promptTokens?: number;
      completionTokens?: number;
      fallbackReason?: string;
    }
  | { status: 'error'; message: string };

export interface UseScriptStream {
  state: ScriptStreamState;
  /** 触发流式重新生成 */
  regenerate: (
    planId: string,
    options?: { model?: string; tier?: string; channel?: 'phone' | 'wecom' },
  ) => Promise<void>;
  /** 中途停掉(用户点暂停 / 离开页面) */
  abort: () => void;
  reset: () => void;
}

// 2026-06 重构:4 模块 — opening / informMissed / reviewAdvice / closing
const LABEL_FALLBACK: Record<FixedSectionId, string> = {
  opening: '开场白',
  informMissed: '告知潜在治疗',
  reviewAdvice: '复查建议',
  closing: '结束回访语',
};
// 时长无意义,统一空串(UI 不再展示建议时长)
const DURATION_FALLBACK: Record<FixedSectionId, string> = {
  opening: '',
  informMissed: '',
  reviewAdvice: '',
  closing: '',
};

export function useScriptStream(): UseScriptStream {
  const [state, setState] = useState<ScriptStreamState>({ status: 'idle' });
  const abortRef = useRef<AbortController | null>(null);

  const reset = useCallback(() => setState({ status: 'idle' }), []);

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

  const regenerate = useCallback(async (
    planId: string,
    options?: { model?: string; tier?: string; channel?: 'phone' | 'wecom' },
  ) => {
    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;

    /**
     * ⭐ 企微走另一个端点,但**事件形状完全一致**(step / done),所以整套流程复用:
     *   鉴权 + silent refresh、帧解析、步骤时间线、停止。
     * ⚠️ 企微没有"档"(只有深度档),不带 tier 参数;也没有分段骨架
     *   —— 它是一整块,占位骨架会先闪出几个空段再被替换,比空白更难看。
     */
    const wecom = options?.channel === 'wecom';
    const url = new URL(
      `/pac/v1/plans/${encodeURIComponent(planId)}/${wecom ? 'wecom-script:stream' : 'script:stream'}`,
      env.apiBaseUrl,
    );
    if (options?.model) url.searchParams.set('model', options.model);
    if (!wecom && options?.tier) url.searchParams.set('tier', options.tier);

    // 占位 sections(分档骨架),避免第一帧到达前 UI 闪;企微无段 → 不铺骨架
    setState({ status: 'streaming', sections: wecom ? [] : makeEmptySections(options?.tier) });

    try {
      // SSE 走原生 fetch(api-client 不处理 stream),自己实现 silent refresh:
      // 第一次拿当前 token → 401/auth-错误 → tryRefresh() → 重试一次
      const openStream = async (retried: boolean): Promise<Response> => {
        const token = useAuthStore.getState().accessToken;
        if (!token) throw new Error('未鉴权 — 请从宿主系统重新打开页面');

        const res = await fetch(url, {
          method: 'GET',
          headers: { Authorization: `Bearer ${token}`, Accept: 'text/event-stream' },
          signal: controller.signal,
        });

        // 后端 SSE 端点鉴权失败时会返回 wrap JSON(HTTP 200,code=10105),body 不是 event-stream
        // 简单检测:Content-Type 不是 text/event-stream → 当 auth/error 处理
        if (!res.ok || !res.headers.get('content-type')?.includes('text/event-stream')) {
          if (!retried && useAuthStore.getState().canRefresh()) {
            // 看是不是 auth 错误,尝试 refresh
            const cloned = res.clone();
            let isAuth = !res.ok && (res.status === 401 || res.status === 403);
            try {
              const body = await cloned.json();
              if (
                body?.code === ApiCode.AUTH_TOKEN_EXPIRED ||
                body?.code === ApiCode.AUTH_TOKEN_MISSING ||
                body?.code === ApiCode.AUTH_TOKEN_INVALID
              ) {
                isAuth = true;
              }
            } catch {
              /* 非 JSON,按 HTTP 状态判断 */
            }
            if (isAuth) {
              const refreshed = await tryRefresh();
              if (refreshed) return openStream(true);
            }
          }
          const text = await res.text().catch(() => '');
          throw new Error(`HTTP ${res.status} ${text.slice(0, 200) || res.statusText}`);
        }
        return res;
      };

      const res = await openStream(false);
      if (!res.body) throw new Error('SSE 响应无 body');

      const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
      let buffer = '';

      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        buffer += value;

        // SSE 帧以 \n\n 分隔
        let sepIdx: number;
        while ((sepIdx = buffer.indexOf('\n\n')) !== -1) {
          const rawFrame = buffer.slice(0, sepIdx);
          buffer = buffer.slice(sepIdx + 2);

          const evt = parseSseFrame(rawFrame);
          if (!evt) continue;

          if (evt.type === 'start') {
            setState({
              status: 'streaming',
              sections: makeEmptySections(options?.tier),
              modelId: evt.modelId,
              invocationId: evt.invocationId,
            });
          } else if (evt.type === 'delta') {
            /**
             * ⭐ 企微:正文增量,直接**追加**到那一段上。
             *   2026-08 企微砍掉工作流之后没有步骤可推了,改推 delta —— 体感从
             *   "盯着三个勾等 60 秒"变成"看着它写"。
             * ⚠️ 追加不是替换:服务端推的就是增量,替换会只剩最后一小段。
             */
            setState((prev) => {
              if (prev.status !== 'streaming') return prev;
              const cur = prev.sections?.[0];
              return {
                ...prev,
                sections: [
                  {
                    id: 'wecom',
                    label: '企微话术',
                    durationHint: '',
                    markdown: (cur?.markdown ?? '') + (evt.text ?? ''),
                  },
                ],
              };
            });
          } else if (evt.type === 'step') {
            setState((prev) =>
              prev.status === 'streaming'
                ? { ...prev, steps: upsertStep(prev.steps ?? [], evt) }
                : prev,
            );
          } else if (evt.type === 'partial') {
            setState((prev) =>
              prev.status === 'streaming'
                ? { ...prev, sections: serverToClientSections(evt.sections) }
                : prev,
            );
          } else if (evt.type === 'done') {
            setState((prev) => ({
              status: 'done',
              /**
               * ⚠️ 企微的 done 带的是整块 `content`(它没有段);电话带 `sections`。
               * 这里把企微那块包成**一段**,让下游(渲染 / 复制 / 状态)只认一种形状。
               * ⛔ 别为此在下游到处判渠道 —— 判据散开之后总会漏一处。
               */
              /**
               * 🔴 企微 `source==='failed'` 时 content 是**空串** —— 这里必须让它覆盖掉
               *   流式过程中已经追加进 sections 的正文。
               *   那份稿子没过机器安全闸(多半带着 `【时间段1】` 之类的占位),
               *   企微是**整段复制直发**的:留在框里 = 客服一键复制就发给患者了,
               *   而占位符会原样出现在对方微信里。⛔ 宁可空着让他自己写。
               */
              sections: evt.sections
                ? serverToClientSections(evt.sections)
                : [{ id: 'wecom', label: '企微话术', durationHint: '', markdown: evt.content ?? '' }],
              steps: prev.status === 'streaming' ? prev.steps : undefined, // 保留过程时间线供完成后回看
              planScriptId: evt.planScriptId,
              source: evt.source,
              costYuan: evt.costYuan,
              promptTokens: evt.promptTokens,
              completionTokens: evt.completionTokens,
              fallbackReason: evt.fallbackReason,
            }));
          } else if (evt.type === 'error') {
            setState({ status: 'error', message: evt.message });
          }
        }
      }
    } catch (err) {
      if (controller.signal.aborted) {
        setState({ status: 'idle' });
        return;
      }
      setState({
        status: 'error',
        message: err instanceof Error ? err.message : String(err),
      });
    } finally {
      if (abortRef.current === controller) abortRef.current = null;
    }
  }, []);

  return { state, regenerate, abort, reset };
}

// ─────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────

interface SseStartEvent {
  type: 'start';
  invocationId: string;
  modelId: string;
  promptVersion: string;
}
interface SsePartialEvent {
  type: 'partial';
  structured: Record<string, unknown>;
  sections: ServerSection[];
}
interface SseDoneEvent {
  type: 'done';
  planScriptId: string | null;
  agentInvocationId: string;
  source: 'agent' | 'template_fallback';
  /** ⚠️ 企微没有段 → 缺省;此时正文在 content 里(见上面 done 分支的包段逻辑) */
  sections?: ServerSection[];
  content: string;
  costYuan: number;
  promptTokens: number;
  completionTokens: number;
  fallbackReason?: string;
}
interface SseErrorEvent {
  type: 'error';
  message: string;
}
interface SseStepEvent {
  type: 'step';
  step: DeepStep['step'];
  status: DeepStep['status'];
  detail?: DeepStep['detail'];
}
/** 企微:正文增量(2026-08 砍掉工作流后取代 step 事件) */
interface SseDeltaEvent {
  type: 'delta';
  text: string;
}

type SseEvent =
  | SseStartEvent
  | SsePartialEvent
  | SseDoneEvent
  | SseErrorEvent
  | SseStepEvent
  | SseDeltaEvent;

/** 合并 step 事件到时间线:同名步更新状态/详情,否则追加(保持 plan→write→verify→repair 顺序)*/
function upsertStep(steps: DeepStep[], evt: SseStepEvent): DeepStep[] {
  const next: DeepStep = { step: evt.step, status: evt.status, detail: evt.detail };
  const i = steps.findIndex((s) => s.step === evt.step);
  if (i === -1) return [...steps, next];
  const copy = steps.slice();
  const prev = copy[i]!;
  copy[i] = { ...prev, ...next, detail: evt.detail ?? prev.detail };
  return copy;
}

function parseSseFrame(frame: string): SseEvent | null {
  // SSE 帧的每一行可能是 "event: <name>" / "data: <json>" / ": comment"
  const dataLines: string[] = [];
  for (const line of frame.split('\n')) {
    if (line.startsWith('data:')) dataLines.push(line.slice(5).trimStart());
    // event / id / retry / 注释行都忽略 — data 内本身已带 type
  }
  if (dataLines.length === 0) return null;
  try {
    return JSON.parse(dataLines.join('\n')) as SseEvent;
  } catch {
    return null;
  }
}

// loading 占位段(分档:骨架要贴各档的输出形态,别都套稳健的"4段固定标题")
//   稳健 = 4 段固定标题;标准 = 4 段(标题模型自起,占位不假装标题);深度 = 段数不定,只占一段。
function makeEmptySections(tier?: string): ScriptSection[] {
  if (tier === 'deep') {
    return [{ id: 's0', label: '生成中…', durationHint: '', markdown: '' }];
  }
  if (tier === 'standard') {
    return Array.from({ length: 4 }, (_, i) => ({
      id: `s${i}`,
      label: '生成中…',
      durationHint: '',
      markdown: '',
    }));
  }
  return (['opening', 'informMissed', 'reviewAdvice', 'closing'] as const).map((id) => ({
    id,
    label: LABEL_FALLBACK[id],
    durationHint: DURATION_FALLBACK[id],
    markdown: '',
  }));
}

function serverToClientSections(srv: ServerSection[]): ScriptSection[] {
  return srv.map((s) => ({
    id: s.id,
    label: s.label,
    durationHint: s.durationHint,
    markdown: s.markdown,
  }));
}
