import { Injectable, Logger } from '@nestjs/common';
import { AiGatewayService } from './ai-gateway.service';
import {
  DECOMPOSE_SYSTEM_PROMPT,
  DECOMPOSE_TOOL_NAME,
  DECOMPOSE_TOOL_DESCRIPTION,
  DECOMPOSE_TOOL_INPUT_SCHEMA,
} from './prompts/decompose';
import {
  RISK_INTERPRET_SYSTEM_PROMPT,
  RISK_INTERPRET_TOOL_NAME,
  RISK_INTERPRET_TOOL_DESCRIPTION,
  RISK_INTERPRET_TOOL_INPUT_SCHEMA,
} from './prompts/risk-interpret';
import { buildPrototypeContext } from './prototype-context';
import type {
  AgentDecomposeRequest,
  AgentDecomposeResponse,
  AgentDecomposeError,
  AgentDecomposeResult,
  AgentDecomposeTarget,
  AgentRiskInsight,
  AgentRiskInterpretError,
  AgentRiskInterpretRequest,
  AgentRiskInterpretResponse,
} from '@ftb/shared';

const DECOMPOSE_CONTEXT_CHAR_STEPS = [1800, 1200, 800] as const;
const PROTOTYPE_FETCH_TIMEOUT_MS = 15000;
const PROTOTYPE_FETCH_ATTEMPTS = 3;
const PROTOTYPE_FETCH_RETRY_DELAY_MS = 500;
const PAGE_REFRESH_ADVICE_PATTERNS = [
  /(刷新|重新加载|重载).*(页面|浏览器|小宝|预警)/i,
  /(页面|浏览器|小宝|预警).*(刷新|重新加载|重载)/i,
  /(手动|主动).*(触发|刷新).*(更新|预警|分析)/i,
  /(manual|manually).*(refresh|reload|trigger)/i,
  /(refresh|reload).*(page|browser|xiaobao|warning)/i,
];

@Injectable()
export class AiService {
  private readonly logger = new Logger(AiService.name);

  constructor(private readonly gateway: AiGatewayService) {}

  async decompose(req: AgentDecomposeRequest): Promise<AgentDecomposeResponse | AgentDecomposeError> {
    const startedAt = Date.now();

    if (!req.requirements || req.requirements.length === 0) {
      return {
        ok: false,
        error: '本版本未关联任何需求，无法进行原型拆解',
        code: 'EMPTY_REQUIREMENTS',
      };
    }

    let prototypeContent: string;
    try {
      prototypeContent = await this.fetchPrototype(req.prototypeUrl);
    } catch (e: any) {
      this.logger.error(`抓取原型失败: ${e.message}`);
      return {
        ok: false,
        error: `无法访问原型链接：${e.message}`,
        code: 'PROTOTYPE_FETCH_FAILED',
      };
    }

    let provider;
    let model: string;
    try {
      provider = await this.gateway.getActiveProvider();
      model = await this.gateway.getActiveModel();
    } catch (e: any) {
      return {
        ok: false,
        error: e?.message || '未配置 AI 提供商',
        code: 'NO_PROVIDER',
      };
    }

    let toolResp;
    for (let attemptIndex = 0; attemptIndex < DECOMPOSE_CONTEXT_CHAR_STEPS.length; attemptIndex++) {
      const contextChars = DECOMPOSE_CONTEXT_CHAR_STEPS[attemptIndex];
      const prototypeContext = buildPrototypeContext(prototypeContent, contextChars, { requirements: req.requirements });
      const userPrompt = this.buildUserPrompt(req, prototypeContext.text, prototypeContext, attemptIndex > 0);

      try {
        toolResp = await provider.callTool({
          systemPrompt: DECOMPOSE_SYSTEM_PROMPT,
          userPrompt,
          tool: {
            name: DECOMPOSE_TOOL_NAME,
            description: DECOMPOSE_TOOL_DESCRIPTION,
            inputSchema: DECOMPOSE_TOOL_INPUT_SCHEMA,
          },
          forceTool: true,
          maxTokens: 16000,
          model,
        });
        break;
      } catch (e: any) {
        const canRetry = this.shouldRetryWithShortContext(e) && attemptIndex < DECOMPOSE_CONTEXT_CHAR_STEPS.length - 1;
        if (canRetry) {
          this.logger.warn(
            `AI 第 ${attemptIndex + 1} 次调用未返回 tool_use，使用 ${DECOMPOSE_CONTEXT_CHAR_STEPS[attemptIndex + 1]} 字符原型上下文重试: ${e.message}`,
          );
          continue;
        }

        this.logger.error(`AI 调用失败: ${e.message}`);
        return {
          ok: false,
          error: `AI 服务调用失败：${e.message}`,
          code: 'API_ERROR',
        };
      }
    }

    if (!toolResp) {
      return {
        ok: false,
        error: 'AI 服务调用失败：未获得工具结果',
        code: 'UNKNOWN',
      };
    }

    const result = this.applyTargetToResult(toolResp.toolInput as AgentDecomposeResult, req.target);
    if (!result.report || !Array.isArray(result.devTaskDrafts) || !Array.isArray(result.testCaseDrafts)) {
      return {
        ok: false,
        error: 'AI 返回结构不符合预期',
        code: 'PARSE_ERROR',
      };
    }

    return {
      ok: true,
      result,
      meta: {
        model: toolResp.rawModel || model,
        inputTokens: toolResp.inputTokens,
        outputTokens: toolResp.outputTokens,
        durationMs: Date.now() - startedAt,
      },
    };
  }

  async interpretRisk(req: AgentRiskInterpretRequest): Promise<AgentRiskInterpretResponse | AgentRiskInterpretError> {
    const startedAt = Date.now();
    let provider;
    let model: string;
    try {
      provider = await this.gateway.getActiveProvider();
      model = await this.gateway.getActiveModel();
    } catch (e: any) {
      return {
        ok: false,
        error: e?.message || '未配置 AI 提供商',
        code: 'NO_PROVIDER',
      };
    }

    try {
      const toolResp = await provider.callTool({
        systemPrompt: RISK_INTERPRET_SYSTEM_PROMPT,
        userPrompt: `请基于以下小宝预警规则结果生成解释，禁止编造新事实：\n${JSON.stringify(req, null, 2)}`,
        tool: {
          name: RISK_INTERPRET_TOOL_NAME,
          description: RISK_INTERPRET_TOOL_DESCRIPTION,
          inputSchema: RISK_INTERPRET_TOOL_INPUT_SCHEMA,
        },
        forceTool: true,
        maxTokens: 3000,
        model,
      });
      const result = this.parseRiskInsightToolResponse(toolResp);
      if (!result) {
        return {
          ok: false,
          error: 'AI 返回结构不符合预期',
          code: 'PARSE_ERROR',
        };
      }
      return {
        ok: true,
        result,
        meta: {
          model: toolResp.rawModel || model,
          inputTokens: toolResp.inputTokens,
          outputTokens: toolResp.outputTokens,
          durationMs: Date.now() - startedAt,
        },
      };
    } catch (e: any) {
      if (this.isProviderParseError(e)) {
        return {
          ok: false,
          error: e?.message || 'AI 返回结构不符合预期',
          code: 'PARSE_ERROR',
        };
      }
      this.logger.error(`AI 风险解读调用失败: ${e.message}`);
      return {
        ok: false,
        error: `AI 服务调用失败：${e.message}`,
        code: 'API_ERROR',
      };
    }
  }

  private parseRiskInsightToolResponse(toolResp: any): AgentRiskInsight | null {
    if (!toolResp || toolResp.toolName !== RISK_INTERPRET_TOOL_NAME || !this.isRecord(toolResp.toolInput)) {
      return null;
    }

    const input = toolResp.toolInput;
    const summary = this.readString(input.summary);
    const forecast = this.readString(input.forecast);
    const generatedAt = this.readString(input.generatedAt);
    const why = this.readStringArray(input.why);
    const suggestedActions = this.readStringArray(input.suggestedActions);
    const ownerHints = this.readStringArray(input.ownerHints);
    if (!summary || !forecast || !generatedAt || !why || !suggestedActions || !ownerHints) {
      return null;
    }

    const insight: AgentRiskInsight = {
      summary,
      why,
      forecast,
      suggestedActions: this.filterPageRefreshAdvice(suggestedActions),
      ownerHints: this.filterPageRefreshAdvice(ownerHints),
      generatedAt,
    };
    if (input.recommendedReleaseWindow !== undefined) {
      const recommendedReleaseWindow = this.readString(input.recommendedReleaseWindow);
      if (!recommendedReleaseWindow) return null;
      insight.recommendedReleaseWindow = recommendedReleaseWindow;
    }
    return insight;
  }

  private filterPageRefreshAdvice(items: string[]): string[] {
    return items.filter((item) => !this.isPageRefreshAdvice(item));
  }

  private isPageRefreshAdvice(text: string): boolean {
    return PAGE_REFRESH_ADVICE_PATTERNS.some((pattern) => pattern.test(text));
  }

  private readString(value: unknown): string | null {
    return typeof value === 'string' ? value : null;
  }

  private readStringArray(value: unknown): string[] | null {
    if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) return null;
    return value;
  }

  private isRecord(value: unknown): value is Record<string, unknown> {
    return typeof value === 'object' && value !== null && !Array.isArray(value);
  }

  private isProviderParseError(error: any): boolean {
    const message = String(error?.message || '');
    return message.includes('tool_call arguments') && message.includes('JSON');
  }

  private async fetchPrototype(url: string): Promise<string> {
    if (!url || !url.startsWith('http')) {
      throw new Error('原型链接无效');
    }

    let lastError: unknown;
    for (let attempt = 1; attempt <= PROTOTYPE_FETCH_ATTEMPTS; attempt++) {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), PROTOTYPE_FETCH_TIMEOUT_MS);
      try {
        const res = await fetch(url, { signal: controller.signal });
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        const html = await res.text();
        return this.htmlToText(html);
      } catch (e) {
        lastError = e;
        if (attempt < PROTOTYPE_FETCH_ATTEMPTS) {
          this.logger.warn(`抓取原型失败，准备重试(${attempt}/${PROTOTYPE_FETCH_ATTEMPTS}): ${(e as Error).message}`);
          await this.sleep(PROTOTYPE_FETCH_RETRY_DELAY_MS * attempt);
        }
      } finally {
        clearTimeout(timeoutId);
      }
    }
    throw lastError instanceof Error ? lastError : new Error(String(lastError));
  }

  private htmlToText(html: string): string {
    return html
      .replace(/<script[\s\S]*?<\/script>/gi, '')
      .replace(/<style[\s\S]*?<\/style>/gi, '')
      .replace(/<[^>]+>/g, ' ')
      .replace(/&nbsp;/g, ' ')
      .replace(/&amp;/g, '&')
      .replace(/&lt;/g, '<')
      .replace(/&gt;/g, '>')
      .replace(/&quot;/g, '"')
      .replace(/\s+/g, ' ')
      .trim();
  }

  private shouldRetryWithShortContext(error: any): boolean {
    const msg = error?.message || '';
    return (
      msg.includes('Anthropic 未通过 tool_use 返回结果') &&
      (msg.includes('上游模型未返回任何内容') || msg.includes('stop_reason=tool_use'))
    );
  }

  private sleep(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  private buildUserPrompt(
    req: AgentDecomposeRequest,
    prototypeContent: string,
    context?: { noteCount: number; truncated: boolean },
    isRetry = false,
  ): string {
    const reqList = req.requirements
      .map(
        (r) =>
          `- id: ${r.id}, code: ${r.code}, title: ${r.title}${r.description ? `, desc: ${r.description}` : ''}`,
      )
      .join('\n');

    const memberList = req.members.map((m) => `- ${m.name} (${m.role})`).join('\n');
    const targetInstruction = this.buildTargetInstruction(req.target ?? 'all');

    return `${isRetry ? '## 重试说明\n上一次 AI 服务未返回工具结果，本次已缩短原型上下文。仍然必须只通过 submit_decompose 工具返回结果。\n\n' : ''}## 产品方案原型

URL: ${req.prototypeUrl}

### 原型内容（已提取 QY 批注和需求命中相关片段）

- 识别到 QY 批注数量：${context?.noteCount ?? 0}
- 内容已截断：${context?.truncated ? '是' : '否'}

${prototypeContent}

## 本版本关联需求

${reqList || '（无）'}

## 原型与需求匹配优先级

1. 优先按需求编号匹配：原型批注或文本中出现 requirement.id 或 requirement.code 时，必须优先判定为该需求命中。
2. 如果没有出现需求编号，再按需求概述语义匹配：用需求 title 和 description 与原型批注/文本表达做对应。
3. 有 QY 编号时，prototype_note 引用只能使用真实存在的 QY 编号；没有 QY 编号但文本已命中需求编号或需求概述时，不要因为缺少 QY 就丢弃该需求，可只使用 requirement 引用并在 matched.noteIds 返回空数组。
4. 既匹配不到需求编号，也匹配不到需求概述语义，但能形成明确功能名称和任务/用例范围的原型批注，必须进入 prototypeOnly 无需求ID分组并继续拆解。
5. prototypeOnly 草案不带 requirement 引用，但必须带 requirementName 和至少 1 条真实 prototype_note 引用；只有无法形成稳定任务/用例的批注才进入 ambiguous。

## 版本成员

${memberList || '（无）'}

负责人推荐只能使用上方版本成员的 members[].name；没有明确匹配时不要输出 recommendedAssigneeName。

## 本次拆解目标

${targetInstruction}

## 你的任务

按系统提示词的规则，拆解出开发任务草案、测试用例草案、对账报告。
通过 tool 调用 submit_decompose 返回结果。`;
  }

  private buildTargetInstruction(target: AgentDecomposeTarget): string {
    if (target === 'dev_tasks') {
      return '本次只重新拆解开发任务；devTaskDrafts 正常返回，testCaseDrafts 必须返回空数组。';
    }
    if (target === 'test_cases') {
      return '本次只重新拆解测试用例；testCaseDrafts 正常返回，devTaskDrafts 必须返回空数组。';
    }
    return '本次同时拆解开发任务和测试用例；devTaskDrafts 与 testCaseDrafts 都按实际需要返回。';
  }

  private applyTargetToResult(
    result: AgentDecomposeResult,
    target: AgentDecomposeTarget = 'all',
  ): AgentDecomposeResult {
    if (target === 'dev_tasks') {
      return { ...result, testCaseDrafts: [] };
    }
    if (target === 'test_cases') {
      return { ...result, devTaskDrafts: [] };
    }
    return result;
  }
}
