import OpenAI from "openai";
import type { AssistantChatRequest, AssistantChatResponse, AssistantIntentRequest, AssistantIntentResponse, AssistantRunRequest, AppConfig } from "../../shared/types";

export interface AssistantToolDescriptor {
  name: string;
  source: "mcp" | "local";
  title?: string;
  description?: string;
  inputSchema?: unknown;
}

export interface AssistantToolPlanCall {
  name: string;
  arguments: Record<string, unknown>;
}

export interface AssistantToolPlan {
  mode: "tool" | "chat";
  toolCalls: AssistantToolPlanCall[];
  replyWhenNoTool?: string;
  reason?: string;
}

export interface AssistantToolObservation {
  name: string;
  source: "mcp" | "local";
  arguments: Record<string, unknown>;
  ok: boolean;
  result?: unknown;
  error?: string;
}

const SYSTEM_PROMPT = [
  "你是“市民请集合智能助手”，运行在市民请集合后台管理员桌面端中，不是通用聊天机器人。",
  "你的主要服务对象是 smqjh 项目的后台管理员、客服、运营和开发维护人员。",
  "工作范围严格限定为两类：第一类是 smqjh 项目相关问题，包括 smqjh-cloud、后台管理、认证、会员、积分、券、订单、商品、系统配置、运行日志、接口联调、管理员操作需求整理；第二类是日常简单问答，包括问候、简短解释、简单翻译、基础计算、简短写作润色、普通办公沟通建议。",
  "超出范围的问题要礼貌拒绝，不要展开回答；拒绝时说明当前助手只支持 smqjh 项目相关事项和日常简单问答，并引导用户改问项目、后台操作或简单日常问题。",
  "不要回答与 smqjh 无关的复杂编程、医疗、法律、金融投资、政治时事、娱乐八卦、考试代答、长篇创作、商业咨询等泛化问题；除非用户明确说明它与 smqjh 项目实施、后台运营或简单日常沟通有关。",
  "回答 smqjh 项目问题时，要把自然语言诉求拆成清晰、可确认、可审计的操作建议；能关联桌面端任务工具时明确说出任务名称。",
  "当用户要查询某个商品在 smqjh 系统内的定价，并对比市面或外部电商价格时，应建议使用“商品价格对比调研”任务。只要用户给出了商品名称或可搜索关键词，就视为可以开始；商品ID、SKU ID、精确规格是可选增强信息，不要反复索要。用户补充“单瓶、单件、550ml”等口径时，直接沿用该口径推进。smqjh 系统价优先走数据库只读查询；当前外部价格源限定为苏宁易购和慢慢买公开价格线索，并在表格里列出请求地址。不要要求用户自己打开电商页面查询；遇到登录、风控或公开页无价格时要如实说明需要官方接口或授权采集账号。",
  "不要为了可选信息阻塞只读查询类任务；先用已有关键词执行查询或给出可执行动作，再说明哪些字段能提高准确度。",
  "外部平台价格必须来自公开页面、搜索结果或授权接口，并附带请求地址或数据来源；不能凭空编造。",
  "你不能声称已经执行真实后台写入操作；涉及发券、扣积分、批量导入、删除、状态变更等高风险动作时，必须先提示需要管理员确认，并建议使用桌面端任务工具或预演模式。",
  "如果用户信息不足，先追问最少必要字段，例如手机号、会员 ID、日期范围、订单号、券类型、导入文件。",
  "回答要简洁、直接、中文优先；不要输出密钥、Token、密码、验证码或其他敏感信息。"
].join("\n");

export class DeepSeekClient {
  constructor(private readonly getConfig: () => AppConfig) {}

  async extractIntent(request: AssistantIntentRequest): Promise<AssistantIntentResponse> {
    const config = this.getConfig();
    if (!config.deepSeek.enabled) {
      throw new Error("DeepSeek 未启用");
    }
    if (!config.deepSeek.apiKey) {
      throw new Error("DeepSeek API Key 未配置");
    }

    const openai = new OpenAI({
      baseURL: config.deepSeek.baseURL,
      apiKey: config.deepSeek.apiKey
    });

    const context = [
      `当前环境：${request.environmentName}`,
      `网关地址：${request.baseUrl}`,
      `后台登录状态：${request.authenticated ? `已登录${request.username ? `，账号 ${request.username}` : ""}` : "未登录"}`,
      "可用任务：",
      ...request.availableTasks.map((task) => `- ${task.title}（${task.id}，${task.category}，风险：${task.dangerLevel}）：${task.description}`)
    ].join("\n");

    const completion = await openai.chat.completions.create({
      model: config.deepSeek.model,
      messages: [
        {
          role: "system",
          content: [
            "你是市民请集合智能助手的意图识别器，只输出 JSON，不输出 Markdown。",
            "必须以当前 message 为主判断意图。history 只能在当前 message 明确是“这个/它/同规格/单瓶/继续查”等追问时，用来补全商品名、订单号、手机号或日期范围。",
            "你可以返回的 taskId 只有：product.price.compare、product.lookup.summary、order.count.query、database.readonly.query、user.current、cloud.health，或者空字符串。",
            "当当前 message 明确想查询 smqjh/业务系统/后台里的某个商品价格，并对比市面价格、外部电商平台或苏宁易购/慢慢买价格线索时，返回 taskId=product.price.compare。",
            "当当前 message 只是查询 smqjh 商品资料、商品描述、商品状态、品牌、规格或系统内定价，不需要外部平台比价时，可以返回 taskId=product.lookup.summary。",
            "当当前 message 是订单数量、手机号下了多少单、订单销售额/营业额/实付金额等汇总统计时，优先返回 taskId=order.count.query，并提取 mobile、dateFrom、dateTo；mobile 只能是 1 开头的 11 位中国手机号，不要把订单号、流水号或普通数字当手机号。",
            "当当前 message 包含物流、快递、配送、运单、发货、到货、dvy、物流状态、订单状态、某订单、订单号、外部订单号、支付流水号等明细查询诉求时，必须返回 taskId=database.readonly.query，不要返回 order.count.query。",
            "当当前 message 需要查询订单物流状态、某订单明细、某字段、会员资料、商品资料、销售额明细、退款/支付状态、后台表内数据等，而现有固定任务无法直接覆盖时，返回 taskId=database.readonly.query，并在 params.sql 中给出可执行的安全 SELECT。",
            "productKeyword 必须只保留真实商品名和规格，删除帮我、查询一下、业务系统、系统里面、后台、价格、对比、电商平台、淘宝、京东、拼多多等操作性词语。",
            "必须保留品牌、品类、口味、容量、包装数量和口径，例如：娃哈哈纯净水596ml/瓶、东方树叶红茶500ml*15/箱、农夫山泉矿泉水单瓶。",
            "不要编造商品ID、SKU、价格或外部平台结果。",
            "openBrowser 只有用户明确要求打开浏览器、弹出页面、人工复核、手动查看时才返回 true，否则 false。",
            "SQL 安全规则：只能生成单条 SELECT；禁止 INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE/CREATE/REPLACE/GRANT/REVOKE/CALL/SET；禁止多语句和注释；明细查询必须 LIMIT 100 以内。",
            "SQL 表名必须使用白名单内表，尽量带库名：smqjh_oms.oms_order、smqjh_oms.oms_order_item、smqjh_system.sm_member、smqjh_system.ums_member_account、smqjh_pms.tz_prod、smqjh_pms.tz_sku、smqjh_pms.pms_djk_goods、smqjh_pms.pms_spu、smqjh_pms.pms_sku、smqjh_pms.pms_video_product、smqjh_pms.pms_video_channel_price。",
            "已知订单字段：order_id、order_number、member_id、shop_id、channel_id、channel_name、hb_order_status、hb_logistic_status、arrive_time、complete_time、order_money、total、actual_total、freight_amount、pay_type、remarks、shop_remarks、dvy_type、dvy_id、dvy_flow_id、dvy_no、dvy_name、pay_time、dvy_time、cancel_reason、cancel_time、delete_status、order_type、create_time、update_time、business_type、consignee_name、consignee_mobile、consignee_address、is_payed、out_trade_no、transaction_id、refund_status、third_order_id、expire_time、tenant_id。",
            "订单查询默认过滤 COALESCE(delete_status,0)=0；销售额优先用 COALESCE(actual_total,total,order_money,0)；手机号可匹配 oms_order.consignee_mobile，也可按 member_id 关联 smqjh_system.sm_member.id 后匹配 sm_member.mobile。",
            "订单物流状态示例：SELECT order_number,hb_order_status,hb_logistic_status,dvy_type,dvy_flow_id,dvy_no,dvy_name,dvy_time,arrive_time,complete_time FROM smqjh_oms.oms_order WHERE COALESCE(delete_status,0)=0 AND order_number LIKE '%订单号%' LIMIT 20",
            "销售额示例：SELECT COUNT(DISTINCT order_id) AS order_count, COALESCE(SUM(COALESCE(actual_total,total,order_money,0)),0) AS total_amount, COALESCE(SUM(CASE WHEN COALESCE(is_payed,0)=1 THEN COALESCE(actual_total,total,order_money,0) ELSE 0 END),0) AS paid_amount FROM smqjh_oms.oms_order WHERE COALESCE(delete_status,0)=0",
            "商品资料示例：SELECT p.prod_id,p.prod_name,p.brand_name,COALESCE(s.sku_name,'') AS sku_name,COALESCE(s.price,p.price,p.ori_price) AS price,p.status FROM smqjh_pms.tz_prod p LEFT JOIN smqjh_pms.tz_sku s ON s.prod_id=p.prod_id AND COALESCE(s.is_delete,0)=0 WHERE COALESCE(p.is_delete,0)=0 AND (p.prod_name LIKE '%关键词%' OR s.sku_name LIKE '%关键词%') LIMIT 20",
            "如果只是天气、问候、简单解释、或和 smqjh 无关，返回 taskId 为空字符串，confidence 小于 0.5。",
            "返回 JSON 结构：{\"taskId\":\"任务ID或空字符串\",\"confidence\":0到1,\"params\":{\"productKeyword\":\"\",\"productType\":\"auto\",\"openBrowser\":\"false\",\"notes\":\"\",\"mobile\":\"\",\"dateFrom\":\"\",\"dateTo\":\"\",\"question\":\"原问题\",\"sql\":\"SELECT ...\"},\"reason\":\"简短原因\"}"
          ].join("\n")
        },
        { role: "system", content: context },
        {
          role: "user",
          content: JSON.stringify({
            message: request.message,
            history: request.history.slice(-6)
          })
        }
      ],
      stream: false,
      thinking: { type: "enabled" },
      reasoning_effort: config.deepSeek.reasoningEffort
    } as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming);

    const content = completion.choices[0]?.message?.content?.trim() ?? "{}";
    const parsed = parseJsonObject(content);
    const params = typeof parsed.params === "object" && parsed.params !== null
      ? Object.fromEntries(
          Object.entries(parsed.params as Record<string, unknown>).map(([key, value]) => [key, value === undefined || value === null ? "" : String(value)])
        )
      : undefined;

    return {
      taskId: typeof parsed.taskId === "string" ? parsed.taskId : undefined,
      confidence: Number(parsed.confidence) || 0,
      params,
      reason: typeof parsed.reason === "string" ? parsed.reason : undefined,
      model: config.deepSeek.model
    };
  }

  async planToolUse(request: AssistantRunRequest, tools: AssistantToolDescriptor[]): Promise<AssistantToolPlan> {
    const config = this.getConfig();
    if (!config.deepSeek.enabled) {
      throw new Error("DeepSeek 未启用");
    }
    if (!config.deepSeek.apiKey) {
      throw new Error("DeepSeek API Key 未配置");
    }

    const openai = new OpenAI({
      baseURL: config.deepSeek.baseURL,
      apiKey: config.deepSeek.apiKey
    });

    const toolContext = tools.map((tool) => ({
      name: tool.name,
      source: tool.source,
      title: tool.title,
      description: tool.description,
      inputSchema: tool.inputSchema
    }));

    const completion = await openai.chat.completions.create({
      model: config.deepSeek.model,
      messages: [
        {
          role: "system",
          content: [
            "你是“市民请集合智能助手”的工具调度器，只输出 JSON，不输出 Markdown。",
            "目标：根据管理员自然语言问题，自主判断是否需要调用工具；只要能通过工具查询业务系统事实，就不要让用户自己执行 SQL、自己去后台查、或自己补充非必要字段。",
            "范围：只处理 smqjh 项目业务、后台运营、商品、订单、会员、积分、券、月结、物流、系统配置、日志，以及日常简单问答。超出范围且没有工具时走 chat。",
            "工具优先级：业务系统事实查询优先选 MCP 工具；外部价格对比选 product.price.compare；简单寒暄、常识解释、天气闲聊等无业务数据问题选 chat。",
            "不要把普通问答错判成商品比价。只有用户明确提到价格对比、市面价、电商、慢慢买、苏宁、淘宝、京东、拼多多、同平台价格等，才选 product.price.compare。",
            "商品资料、商品描述、系统内定价、商品状态、品牌、规格，选 smqjh.product.lookup.summary，并把 productKeyword 提炼为真实商品名和规格。",
            "订单数量、销售额、实付金额、营业额、按手机号统计，优先选 smqjh.order.count.query；如果需要明细字段或该工具无法表达，再选 smqjh.database.readonly.query。",
            "订单物流状态、配送状态、快递单号、运单号、订单明细、会员资料、任意后台表内字段查询，选 smqjh.database.readonly.query 并生成安全 SELECT。",
            "企业月结、铜仁移动/招商银行贵阳分行/中数未来结算、结算流程、结算月份，选 smqjh.settlement.monthly.plan。",
            "SQL 安全要求：只能单条 SELECT；禁止写入、DDL、多语句、注释；明细查询 LIMIT 100 以内；默认排除逻辑删除；只访问 smqjh 白名单业务表。",
            "常用表：smqjh_oms.oms_order、smqjh_oms.oms_order_item、smqjh_system.sm_member、smqjh_system.ums_member_account、smqjh_system.sm_channel、smqjh_pms.tz_prod、smqjh_pms.tz_sku。",
            "常用订单字段：order_id、order_number、member_id、channel_id、channel_name、hb_order_status、hb_logistic_status、order_money、total、actual_total、freight_amount、dvy_type、dvy_id、dvy_flow_id、dvy_no、dvy_name、pay_time、dvy_time、create_time、consignee_mobile、is_payed、delete_status。",
            "常用商品 SQL 示例：SELECT p.prod_id,p.prod_name,p.brand_name,p.brief,COALESCE(s.sku_name,'') AS sku_name,COALESCE(s.price,p.price,p.ori_price) AS price FROM smqjh_pms.tz_prod p LEFT JOIN smqjh_pms.tz_sku s ON s.prod_id=p.prod_id AND COALESCE(s.is_delete,0)=0 WHERE COALESCE(p.is_delete,0)=0 AND (p.prod_name LIKE '%关键词%' OR s.sku_name LIKE '%关键词%') LIMIT 20",
            "常用物流 SQL 示例：SELECT order_number,hb_order_status,hb_logistic_status,dvy_type,dvy_flow_id,dvy_no,dvy_name,dvy_time,arrive_time,complete_time FROM smqjh_oms.oms_order WHERE COALESCE(delete_status,0)=0 AND order_number LIKE '%订单号%' LIMIT 20",
            "返回 JSON：{\"mode\":\"tool\"或\"chat\",\"toolCalls\":[{\"name\":\"工具名\",\"arguments\":{}}],\"replyWhenNoTool\":\"chat 模式时的简短答复\",\"reason\":\"简短判断依据\"}。",
            "最多返回 3 个工具调用。arguments 必须符合工具 schema。不要编造不存在的工具名。"
          ].join("\n")
        },
        {
          role: "system",
          content: JSON.stringify({
            environmentName: request.environmentName,
            baseUrl: request.baseUrl,
            authenticated: request.authenticated,
            username: request.username ?? "",
            tools: toolContext
          })
        },
        {
          role: "user",
          content: JSON.stringify({
            message: request.message,
            history: request.history.slice(-8)
          })
        }
      ],
      stream: false,
      thinking: { type: "enabled" },
      reasoning_effort: config.deepSeek.reasoningEffort
    } as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming);

    const parsed = parseJsonObject(completion.choices[0]?.message?.content?.trim() ?? "{}");
    const rawCalls = Array.isArray(parsed.toolCalls) ? parsed.toolCalls : [];
    const toolCalls = rawCalls
      .map((item): AssistantToolPlanCall | undefined => {
        if (!item || typeof item !== "object") {
          return undefined;
        }
        const record = item as Record<string, unknown>;
        if (typeof record.name !== "string" || !record.name.trim()) {
          return undefined;
        }
        const args = record.arguments && typeof record.arguments === "object" && !Array.isArray(record.arguments)
          ? (record.arguments as Record<string, unknown>)
          : {};
        return {
          name: record.name.trim(),
          arguments: args
        };
      })
      .filter((item): item is AssistantToolPlanCall => Boolean(item))
      .slice(0, 3);

    const mode = parsed.mode === "tool" && toolCalls.length > 0 ? "tool" : "chat";
    return {
      mode,
      toolCalls: mode === "tool" ? toolCalls : [],
      replyWhenNoTool: typeof parsed.replyWhenNoTool === "string" ? parsed.replyWhenNoTool : undefined,
      reason: typeof parsed.reason === "string" ? parsed.reason : undefined
    };
  }

  async summarizeToolResults(request: AssistantRunRequest, observations: AssistantToolObservation[]): Promise<AssistantChatResponse> {
    const config = this.getConfig();
    if (!config.deepSeek.enabled) {
      throw new Error("DeepSeek 未启用");
    }
    if (!config.deepSeek.apiKey) {
      throw new Error("DeepSeek API Key 未配置");
    }

    const openai = new OpenAI({
      baseURL: config.deepSeek.baseURL,
      apiKey: config.deepSeek.apiKey
    });

    const completion = await openai.chat.completions.create({
      model: config.deepSeek.model,
      messages: [
        {
          role: "system",
          content: [
            "你是“市民请集合智能助手”，需要根据工具执行结果给管理员一个连贯、可信、可审计的中文答复。",
            "不要让用户自己再执行 SQL；工具已经执行过了。回答要直接给结论、口径和依据。",
            "如果工具结果包含 columns/rows 或 comparisonRows，说明“表格如下”即可，不要重复输出很宽的 Markdown 表格；前端会单独渲染表格。",
            "必须说明依据来源，例如执行的 MCP 工具名、SQL executedSql、公开请求地址、只读查询口径。",
            "如果结果为空，要说明已查的条件和可能原因，并给出下一步可补充的最少字段。",
            "外部价格线索要提醒以平台实时页面为准；涉及业务写入、修改、发券、扣减、结算确认时只给方案和需人工确认，不声称已修改系统。",
            "保持简洁，优先输出 2-5 个短段落。"
          ].join("\n")
        },
        {
          role: "user",
          content: JSON.stringify({
            message: request.message,
            environmentName: request.environmentName,
            baseUrl: request.baseUrl,
            toolObservations: observations
          })
        }
      ],
      stream: false,
      thinking: { type: "enabled" },
      reasoning_effort: config.deepSeek.reasoningEffort
    } as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming);

    return {
      content: completion.choices[0]?.message?.content?.trim() || "工具已执行，但 DeepSeek 没有生成有效总结。",
      model: config.deepSeek.model
    };
  }

  async chat(request: AssistantChatRequest): Promise<AssistantChatResponse> {
    const config = this.getConfig();
    if (!config.deepSeek.enabled) {
      throw new Error("DeepSeek 未启用");
    }
    if (!config.deepSeek.apiKey) {
      throw new Error("DeepSeek API Key 未配置");
    }

    const openai = new OpenAI({
      baseURL: config.deepSeek.baseURL,
      apiKey: config.deepSeek.apiKey
    });

    const context = [
      `当前环境：${request.environmentName}`,
      `网关地址：${request.baseUrl}`,
      `当前时间：${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false })}`,
      `后台登录状态：${request.authenticated ? `已登录${request.username ? `，账号 ${request.username}` : ""}` : "未登录"}`,
      "可用任务：",
      ...request.availableTasks.map((task) => `- ${task.title}（${task.id}，${task.category}，风险：${task.dangerLevel}）：${task.description}`)
    ].join("\n");

    const completion = await openai.chat.completions.create({
      model: config.deepSeek.model,
      messages: [
        { role: "system", content: SYSTEM_PROMPT },
        { role: "system", content: context },
        ...request.messages.map((message) => ({
          role: message.role,
          content: message.content
        }))
      ],
      stream: false,
      thinking: { type: "enabled" },
      reasoning_effort: config.deepSeek.reasoningEffort
    } as OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming);

    return {
      content: completion.choices[0]?.message?.content?.trim() || "我没有拿到有效回复，请稍后再试。",
      model: config.deepSeek.model
    };
  }
}

function parseJsonObject(content: string): Record<string, unknown> {
  const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
  const raw = fenced || content;
  try {
    return JSON.parse(raw) as Record<string, unknown>;
  } catch {
    const start = raw.indexOf("{");
    const end = raw.lastIndexOf("}");
    if (start >= 0 && end > start) {
      return JSON.parse(raw.slice(start, end + 1)) as Record<string, unknown>;
    }
    return {};
  }
}
