import { Body, Controller, Logger, Post, Req, Res, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Request, Response } from 'express';
import { ApiBearerAuth, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger';
import type { ModelMessage } from 'ai';
import { AssistantService, MAX_TOOL_STEPS } from './assistant.service';
import { TranscribeService } from './transcribe.service';
import { CurrentUser, type AuthenticatedUser } from '../../common/decorators/current-user.decorator';
import { TenantScope } from '../../common/decorators/tenant-scope.decorator';
import type { TenantScopeContext } from '../../common/decorators/tenant-scope.decorator';
import { buildSystemExtra } from './assistant-prompts';

/// multer 内存模式的最小文件形状(不引 @types/multer)
interface UploadedAudio {
  buffer: Buffer;
  originalname: string;
  mimetype: string;
}

interface ChatBody {
  messages: ModelMessage[];
  model?: string;
}

/**
 * 从「正在流式生成的工具入参 JSON」里增量提取 html 字段值(用于 artifact 实时渲染)。
 * 入参形如 {"title":"...","html":"<div>...   ← 还没结束。容忍末尾不完整的转义,返回已解出的部分。
 */
function extractHtmlField(jsonText: string): string | null {
  const m = jsonText.match(/"html"\s*:\s*"/);
  if (!m || m.index === undefined) return null;
  const s = jsonText.slice(m.index + m[0].length);
  const esc: Record<string, string> = {
    n: '\n', t: '\t', r: '\r', '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f',
  };
  let out = '';
  for (let i = 0; i < s.length; i++) {
    const c = s[i];
    if (c === '\\') {
      const n = s[i + 1];
      if (n === undefined) break; // 末尾半截转义 → 停
      if (n === 'u') {
        if (i + 6 > s.length) break;
        out += String.fromCharCode(parseInt(s.slice(i + 2, i + 6), 16));
        i += 5;
        continue;
      }
      out += esc[n] ?? n;
      i += 1;
      continue;
    }
    if (c === '"') break; // html 值结束
    out += c;
  }
  return out;
}

/**
 * AssistantController — "外部 agent" 模拟器聊天端点(SSE 流式)。
 *
 * POST /pac/v1/assistant/chat  —— 普通 JWT 鉴权(全局 guard);转发用户 token 给 MCP,
 * 工具在该用户 tenant scope 下执行。把 streamText 的 fullStream 部件转成 SSE 事件给前端:
 *   { type: 'text', text }            文本增量
 *   { type: 'tool_call', tool, args } 模型发起工具调用(前端可视化"调了哪个 PAC 工具")
 *   { type: 'tool_result', tool, result }
 *   { type: 'error', error } / { type: 'done' }
 */
@ApiTags('assistant')
@ApiBearerAuth('accessToken')
@Controller('assistant')
export class AssistantController {
  private readonly logger = new Logger(AssistantController.name);

  constructor(
    private readonly assistant: AssistantService,
    private readonly transcriber: TranscribeService,
  ) {}

  /** 语音听写 → 文字(SenseVoice 自部署,PII 不出内网)。multipart 字段名 file。 */
  @Post('transcribe')
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: '语音转文字(听写)— 浏览器录音上传,返回 { text }' })
  @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 15 * 1024 * 1024 } }))
  async transcribe(@UploadedFile() file: UploadedAudio | undefined): Promise<{ text: string }> {
    const r = await this.transcriber.transcribe(
      file?.buffer ?? Buffer.alloc(0),
      file?.originalname ?? 'audio.webm',
      file?.mimetype ?? 'audio/webm',
    );
    return { text: r.text };
  }

  /** 桌宠发言:输入一段环境观察,流式吐一句 ≤30 字的台词(无工具,极轻)。 */
  @Post('pet-say')
  @ApiOperation({ summary: '桌宠环境观察发言(SSE,简短一句)' })
  async petSay(
    @Req() req: Request,
    @Res() res: Response,
    @Body() body: { observation?: string },
  ): Promise<void> {
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache, no-transform');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders?.();
    const send = (event: Record<string, unknown>): void => {
      res.write(`data: ${JSON.stringify(event)}\n\n`);
    };
    const ac = new AbortController();
    req.on('close', () => ac.abort());
    try {
      const r = this.assistant.petSay({
        observation: String(body.observation ?? '').slice(0, 400), // 观察必须简短,后端兜底截断
        abortSignal: ac.signal,
      });
      for await (const text of r.textStream) {
        if (text) send({ type: 'text', text });
      }
      send({ type: 'done' });
    } catch (err) {
      send({ type: 'error', error: err instanceof Error ? err.message : String(err) });
    } finally {
      res.end();
    }
  }

  @Post('chat')
  @ApiOperation({ summary: '助手对话(SSE)— 模型自主调 PAC MCP 工具' })
  async chat(
    @Req() req: Request,
    @Res() res: Response,
    @Body() body: ChatBody,
    @CurrentUser() user: AuthenticatedUser,
    @TenantScope() scope: TenantScopeContext,
  ): Promise<void> {
    const token = (req.headers['authorization'] ?? '').replace(/^Bearer\s+/i, '');

    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache, no-transform');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders?.();

    const send = (event: Record<string, unknown>): void => {
      res.write(`data: ${JSON.stringify(event)}\n\n`);
    };

    const ac = new AbortController();
    req.on('close', () => ac.abort());

    // 流式 artifact:render_artifact 的入参(HTML)逐字生成,边生成边推 → 前端实时"长出"卡片,
    // 不必等整段(~30s)生成完。按 toolCallId 累积 JSON 入参,增量提取 html 字段,250ms 节流发送。
    const artToolIds = new Set<string>();
    const artBuf = new Map<string, string>();
    const artLastSent = new Map<string, number>();

    try {
      const result = await this.assistant.chat({
        userToken: token,
        modelId: body.model,
        messages: body.messages ?? [],
        // 只用于 MCP 工具清单的缓存分桶(见 mcpCapabilityKey)——
        // 不传的话主管和客服会共用一份缓存,谁先进来谁的清单被全员复用。
        permissions: user.permissions,
        // ⭐ 侧信道:本地工具把确认单肥载荷直接推前端,不经模型上下文(见 AssistantChatInput)
        onSideEvent: send,
        scope,
        // ⭐ 按能力切工作流约束(主管 / 客服两套)。
        //   ⚠️ 在此之前这个参数**从来没被 /assistant/chat 传过** ——
        //   「一个助手,按角色切换工作流约束」这条既定取舍此前是零实现。
        systemExtra: buildSystemExtra({
          permissions: user.permissions,
          userName: user.dictionary?.users?.[user.sub] ?? null,
        }),
        abortSignal: ac.signal,
      });

      for await (const part of result.fullStream) {
        const p = part as Record<string, unknown> & { type: string };
        switch (p.type) {
          case 'text-delta':
          case 'text':
            send({ type: 'text', text: (p.text as string) ?? (p.delta as string) ?? '' });
            break;
          case 'tool-input-start':
            if (p.toolName === 'render_artifact') artToolIds.add(String(p.id));
            break;
          case 'tool-input-delta': {
            const id = String(p.id);
            if (!artToolIds.has(id)) break;
            const acc = (artBuf.get(id) ?? '') + String(p.delta ?? '');
            artBuf.set(id, acc);
            const now = Date.now();
            if (now - (artLastSent.get(id) ?? 0) > 250) {
              artLastSent.set(id, now);
              const html = extractHtmlField(acc);
              if (html) send({ type: 'artifact_html', id, html });
            }
            break;
          }
          case 'tool-call':
            send({ type: 'tool_call', id: p.toolCallId, tool: p.toolName, args: p.input });
            break;
          case 'tool-result':
            send({ type: 'tool_result', id: p.toolCallId, tool: p.toolName, result: p.output });
            break;
          case 'tool-error':
            // 工具 execute 抛错(如越权 patientId)→ SDK 发 tool-error 分片(流不中断,模型会自纠)。
            // 必须转发,否则前端该步骤永远停在 running(右侧转圈不停)。
            send({
              type: 'tool_error',
              id: p.toolCallId,
              tool: p.toolName,
              error: p.error instanceof Error ? p.error.message : String(p.error),
            });
            break;
          case 'finish':
            /**
             * 🔴 **no silent cap** —— `stopWhen(MAX_TOOL_STEPS)` 到顶时，SDK 以
             * `finishReason='tool-calls'` 收尾：模型还想继续调工具，被截断了。
             *
             * 不报出来的后果很隐蔽：它可能停在「我先查一下」之后就没了下文，
             * 而主管以为查完了。⚠️ 这里只**陈述事实**（本轮到上限了），
             * ⛔ 不替模型补话、⛔ 不自动续跑 —— 续不续是用户的决定。
             */
            if (p.finishReason === 'tool-calls') {
              this.logger.warn(
                `助手单轮工具步数到顶(${MAX_TOOL_STEPS})，模型仍想继续调用 —— 本轮被截断`,
              );
              send({ type: 'step_limit', limit: MAX_TOOL_STEPS });
            }
            break;
          case 'error':
            send({ type: 'error', error: String(p.error) });
            break;
          default:
            break;
        }
      }
      send({ type: 'done' });
    } catch (err) {
      send({ type: 'error', error: err instanceof Error ? err.message : String(err) });
    } finally {
      res.end();
    }
  }
}
