'use client';

import { useCallback, useRef, useState } from 'react';
import type { AssignmentProposal } from '@pac/types';
import { env } from '@/lib/env';
import { emitPetEvent } from '@/lib/pet-events';
import { useAuthStore } from '@/stores/auth-store';

/** 一步工具调用(Claude 式透明步骤:看到调了哪个工具、入参、返回数据)。 */
export interface ToolStep {
  id: string;
  /** 后端 toolCallId,用于把 tool_result/tool_error 精确匹配回对应步骤(支持并行/多调用)。 */
  callId?: string;
  tool: string;
  args: unknown;
  result?: unknown;
  status: 'running' | 'done' | 'error';
  error?: string;
}

/** 模型产出的 HTML 卡片(在输出区沙箱 iframe 渲染)。 */
export interface Artifact {
  id: string;
  title?: string;
  html: string;
  /** true = 还在流式生成中(只注入内容,图表脚本不执行);false/缺省 = 最终(执行图表脚本)。 */
  streaming?: boolean;
}

export type Block =
  | { kind: 'text'; text: string }
  | { kind: 'tool'; step: ToolStep }
  | { kind: 'artifact'; artifact: Artifact }
  /**
   * 全景确认单 —— **原生 React 组件**,不是 artifact。
   *
   * ⚠️ 为什么不能用 artifact:artifact 跑在 `sandbox="allow-scripts"` 的 iframe 里,
   * CSP 是 `connect-src 'none'` —— **卡片内不可能发出写请求**。确认单要能点确认落库,
   * 只能是原生组件。
   * 分岔记牢:**只读展示用 artifact,可交互用原生组件。**
   *
   * ⚠️ sheet 由**服务端经侧信道推来**,不是模型生成的 —— 它含几百个 planId,
   * 让模型吐等于让它编。
   */
  | {
      kind: 'assignment_sheet';
      requestId: string;
      sheet: AssignmentProposal;
      state: 'pending' | 'confirmed' | 'cancelled';
      /** 确认后的批次 id(终态展示用) */
      assignmentId?: string;
    };

/** render_artifact 是本地"渲染"工具,不当普通工具步骤展示,而是渲成卡片。 */
const ARTIFACT_TOOL = 'render_artifact';

export interface ChatMessage {
  id: string;
  role: 'user' | 'assistant';
  blocks: Block[];
}

export type ChatStatus = 'idle' | 'streaming' | 'error';

let _id = 0;
const nextId = () => `m${Date.now()}_${_id++}`;

/** 把 tool_result/tool_error 匹配回对应工具步骤:优先按 callId,退化到"最后一个 running"。 */
function findToolIdx(blocks: Block[], callId: string | undefined): number {
  if (callId) {
    const i = blocks.findIndex((b) => b.kind === 'tool' && b.step.callId === callId);
    if (i !== -1) return i;
  }
  for (let i = blocks.length - 1; i >= 0; i--) {
    const b = blocks[i];
    if (b && b.kind === 'tool' && b.step.status === 'running') return i;
  }
  return -1;
}

/** 按 callId upsert artifact 块:流式 html 增量更新,最终 tool_call 覆盖(title+完整 html)。 */
function upsertArtifact(
  blocks: Block[],
  id: string,
  patch: { title?: string; html?: string; streaming?: boolean },
): Block[] {
  const i = blocks.findIndex((b) => b.kind === 'artifact' && b.artifact.id === id);
  if (i === -1) {
    return [
      ...blocks,
      { kind: 'artifact', artifact: { id, title: patch.title, html: patch.html ?? '', streaming: patch.streaming } },
    ];
  }
  return blocks.map((b, j) => {
    if (j !== i || b.kind !== 'artifact') return b;
    return {
      kind: 'artifact',
      artifact: {
        id,
        title: patch.title ?? b.artifact.title,
        html: patch.html ?? b.artifact.html,
        streaming: patch.streaming ?? b.artifact.streaming,
      },
    };
  });
}

/** 把一条消息压平成后端要的 {role, content} 文本(工具块不回传,模型自行重新决策)。 */
function toApiMessage(m: ChatMessage): { role: 'user' | 'assistant'; content: string } | null {
  const text = m.blocks
    .filter((b): b is Extract<Block, { kind: 'text' }> => b.kind === 'text')
    .map((b) => b.text)
    .join('')
    .trim();
  if (!text) return null;
  return { role: m.role, content: text };
}

/** 听写音频上传 → 文字(后端 /assistant/transcribe → 自部署 SenseVoice,PII 不出内网)。 */
export async function transcribeAudio(blob: Blob): Promise<string> {
  const token = useAuthStore.getState().accessToken;
  const form = new FormData();
  const ext = blob.type.includes('mp4') ? 'mp4' : blob.type.includes('ogg') ? 'ogg' : 'webm';
  form.append('file', blob, `dictation.${ext}`);
  const res = await fetch(new URL('/pac/v1/assistant/transcribe', env.apiBaseUrl), {
    method: 'POST',
    headers: token ? { Authorization: `Bearer ${token}` } : undefined,
    body: form,
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const json = (await res.json()) as { code?: number; msg?: string; data?: { text?: string } };
  if (json.code !== 0) throw new Error(json.msg || '识别失败');
  return json.data?.text ?? '';
}

export function useAssistantChat() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [status, setStatus] = useState<ChatStatus>('idle');
  const [model, setModel] = useState('qwen');
  const abortRef = useRef<AbortController | null>(null);

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

  const send = useCallback(
    async (text: string) => {
      const trimmed = text.trim();
      if (!trimmed || status === 'streaming') return;

      const userMsg: ChatMessage = {
        id: nextId(),
        role: 'user',
        blocks: [{ kind: 'text', text: trimmed }],
      };
      const assistantId = nextId();
      const assistantMsg: ChatMessage = { id: assistantId, role: 'assistant', blocks: [] };

      // 历史(含新 user)→ apiMessages
      const apiMessages = [...messages, userMsg]
        .map(toApiMessage)
        .filter((x): x is { role: 'user' | 'assistant'; content: string } => x !== null);

      setMessages((prev) => [...prev, userMsg, assistantMsg]);
      setStatus('streaming');
      emitPetEvent({ type: 'ai_thinking_start' }); // 宠物演"思考"(详情页助手宠物)

      // 更新 assistant 消息 blocks 的小工具
      const patch = (fn: (blocks: Block[]) => Block[]) =>
        setMessages((prev) =>
          prev.map((m) => (m.id === assistantId ? { ...m, blocks: fn(m.blocks) } : m)),
        );

      const appendText = (t: string) =>
        patch((blocks) => {
          const last = blocks[blocks.length - 1];
          if (last && last.kind === 'text') {
            return [...blocks.slice(0, -1), { kind: 'text', text: last.text + t }];
          }
          return [...blocks, { kind: 'text', text: t }];
        });

      const onEvent = (evt: Record<string, unknown>) => {
        switch (evt.type) {
          case 'text':
            appendText((evt.text as string) ?? '');
            break;
          case 'assignment_sheet':
            // ⭐ 侧信道推来的确认单。upsert 按 requestId —— 模型若因某种原因重复调用,
            //   不该在对话里堆出两张卡片。
            patch((blocks) => {
              const rid = String(evt.requestId ?? '');
              const i = blocks.findIndex((b) => b.kind === 'assignment_sheet' && b.requestId === rid);
              const next: Block = {
                kind: 'assignment_sheet',
                requestId: rid,
                sheet: evt.sheet as AssignmentProposal,
                state: 'pending',
              };
              if (i >= 0) return [...blocks.slice(0, i), next, ...blocks.slice(i + 1)];
              return [...blocks, next];
            });
            break;
          case 'artifact_html':
            // 流式:render_artifact 的 html 边生成边到(按 callId upsert,实时"长出"卡片)。
            patch((blocks) =>
              upsertArtifact(blocks, String(evt.id ?? ''), { html: String(evt.html ?? ''), streaming: true }),
            );
            break;
          case 'tool_call':
            if (evt.tool === ARTIFACT_TOOL) {
              // 渲染工具最终入参:覆盖为完整 html + title,标记 streaming=false(触发图表脚本执行)。
              const a = (evt.args ?? {}) as { title?: string; html?: string };
              patch((blocks) =>
                upsertArtifact(blocks, String(evt.id ?? nextId()), {
                  title: a.title,
                  html: a.html,
                  streaming: false,
                }),
              );
              break;
            }
            patch((blocks) => [
              ...blocks,
              {
                kind: 'tool',
                step: {
                  id: nextId(),
                  callId: evt.id ? String(evt.id) : undefined,
                  tool: String(evt.tool ?? '工具'),
                  args: evt.args,
                  status: 'running',
                },
              },
            ]);
            break;
          case 'tool_result':
            patch((blocks) => {
              const realIdx = findToolIdx(blocks, evt.id ? String(evt.id) : undefined);
              if (realIdx === -1) return blocks;
              const b = blocks[realIdx] as Extract<Block, { kind: 'tool' }>;
              const updated: Block = {
                kind: 'tool',
                step: { ...b.step, result: evt.result, status: 'done' },
              };
              return blocks.map((x, i) => (i === realIdx ? updated : x));
            });
            break;
          case 'tool_error':
            patch((blocks) => {
              const realIdx = findToolIdx(blocks, evt.id ? String(evt.id) : undefined);
              if (realIdx === -1) return blocks;
              const b = blocks[realIdx] as Extract<Block, { kind: 'tool' }>;
              const updated: Block = {
                kind: 'tool',
                step: { ...b.step, status: 'error', error: String(evt.error ?? '工具调用出错') },
              };
              return blocks.map((x, i) => (i === realIdx ? updated : x));
            });
            break;
          case 'error':
            appendText(`\n\n⚠️ 出错:${String(evt.error)}`);
            break;
          default:
            break;
        }
      };

      const controller = new AbortController();
      abortRef.current = controller;
      try {
        const token = useAuthStore.getState().accessToken;
        const res = await fetch(new URL('/pac/v1/assistant/chat', env.apiBaseUrl), {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Accept: 'text/event-stream',
            ...(token ? { Authorization: `Bearer ${token}` } : {}),
          },
          body: JSON.stringify({ messages: apiMessages, model }),
          signal: controller.signal,
        });
        if (!res.ok || !res.body) {
          throw new Error(`HTTP ${res.status}`);
        }

        const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
        let buffer = '';
        while (true) {
          const { value, done } = await reader.read();
          if (done) break;
          buffer += value;
          let sep: number;
          while ((sep = buffer.indexOf('\n\n')) !== -1) {
            const frame = buffer.slice(0, sep);
            buffer = buffer.slice(sep + 2);
            const line = frame.split('\n').find((l) => l.startsWith('data:'));
            if (!line) continue;
            const raw = line.slice('data:'.length).trim();
            if (!raw) continue;
            try {
              onEvent(JSON.parse(raw) as Record<string, unknown>);
            } catch {
              /* 忽略半截帧 */
            }
          }
        }
        setStatus('idle');
      } catch (err) {
        if ((err as Error).name !== 'AbortError') {
          appendText(`\n\n⚠️ 连接失败:${(err as Error).message}`);
          setStatus('error');
        } else {
          setStatus('idle');
        }
      } finally {
        abortRef.current = null;
        emitPetEvent({ type: 'ai_thinking_end' });
      }
    },
    [messages, model, status],
  );

  /**
   * ⭐ 确认单点了确认之后,**必须往消息流注入一条文本块**。
   *
   * 因为 `toApiMessage` **只回传文本块**(工具块不回传,模型自行重新决策)——
   * 不注入的话,下一轮主管说「刚才那批改成 5 天」,模型手里完全没有"那批"的痕迹,
   * 会当成新需求重新提议一次。改动很小但极易漏,漏了会被当成模型能力问题。
   */
  const appendAssistantNote = useCallback((text: string) => {
    setMessages((prev) => {
      if (prev.length === 0) return prev;
      const last = prev[prev.length - 1]!;
      return [...prev.slice(0, -1), { ...last, blocks: [...last.blocks, { kind: 'text', text }] }];
    });
  }, []);

  /** 确认单落库后把卡片切到终态(按钮禁用,显示批次号) */
  const settleSheet = useCallback((requestId: string, assignmentId: string) => {
    setMessages((prev) =>
      prev.map((m) => ({
        ...m,
        blocks: m.blocks.map((b) =>
          b.kind === 'assignment_sheet' && b.requestId === requestId
            ? { ...b, state: 'confirmed' as const, assignmentId }
            : b,
        ),
      })),
    );
  }, []);

  return { messages, status, model, setModel, send, stop, appendAssistantNote, settleSheet };
}
