import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import type { ModelMessage } from 'ai';
import AiBot, { generateReqId } from '@wecom/aibot-node-sdk';
import type { WsFrame, TemplateCard } from '@wecom/aibot-node-sdk';
import type { UserRole } from '@pac/types';
import { calcAge, maskName } from '@pac/utils';
import { AuthService } from '../auth/auth.service';
import { AssistantService } from '../assistant/assistant.service';
import { ExecutionService } from '../plan/execution.service';
import { PrismaService } from '../../prisma/prisma.service';
import type { TenantScopeContext } from '../../common/decorators/tenant-scope.decorator';

/** 卡片按钮 outcome → 中文(更新卡片展示用)。 */
const OUTCOME_ZH: Record<string, string> = {
  success_appointed: '已约到诊',
  scheduled_next: '已跟进 / 约下次',
  no_answer: '未接通',
};

/**
 * WeixinAibotService — 把 PAC 助手接到企业微信「智能机器人 · API 模式 · 长连接」。
 *
 * 链路:企微(客服在聊天里发问)→ WSClient 长连接收到 message.text
 *      → 映射 企微 userid 成 PAC 客服 token(tenant scope)
 *      → 复用 AssistantService.chat(同一个助手大脑,自主调 MCP 工具)
 *      → 把 streamText 的文本增量用 replyStream 回推(全量替换语义,finish=true 收尾)。
 *
 * 开关:仅当配置了 PAC_WX_AIBOT_BOT_ID + PAC_WX_AIBOT_SECRET 才启动(其它环境静默跳过)。
 *
 * ⚠️ 本版为本地 PoC:
 *   - 身份映射是「所有企微用户 → 同一个默认 tenant scope」(jvs-dw / 默认租户 / staff)。
 *     生产需替换为「企微 userid ↔ 真实客服(tenant/clinic/role)」的对照(配置或库)。
 *   - 企微聊天是文字/Markdown,渲染不了 render_artifact 的 HTML 卡片;PoC 直接吐文字。
 *     产品化时给企微通道一个不带 render_artifact 的精简工具集 + 文字优先提示词。
 */
@Injectable()
export class WeixinAibotService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(WeixinAibotService.name);
  private client?: InstanceType<typeof AiBot.WSClient>;
  private cachedHostId?: string;
  /** 最近活跃的群 chatid(主动推送缺省目标;群里 @一次即记住)。 */
  private lastChatId?: string;
  /** 召回卡深链前缀(本地默认 localhost:3100;线上配真实域名)。 */
  private readonly webBase = process.env.PAC_WEB_BASE_URL?.trim() || 'http://localhost:3100';
  /** 多轮会话记忆:key = `${chatid}:${userid}` → 最近消息 + 时间戳(内存,带 TTL)。 */
  private readonly convos = new Map<string, { msgs: ModelMessage[]; at: number }>();
  private readonly CONVO_TTL_MS = 30 * 60 * 1000; // 30 分钟无活动即过期
  private readonly CONVO_MAX = 12; // 每会话保留最近 12 条(user/assistant)

  /** 企微渠道指令(追加到助手 system):纯文字、链接格式、多轮指代。 */
  private get wxSystemExtra(): string {
    return [
      '你现在在企业微信群聊里(纯文字 Markdown 环境):',
      '- 不要调用 render_artifact 画 HTML 卡片(企微渲染不了),直接用简洁文字 / Markdown 回答。',
      `- 需要给"患者 / 召回页面链接"时:用工具返回的召回计划 id,拼成 ${this.webBase}/plans/<planId>,以 Markdown 链接给出(如 [查看召回卡](${this.webBase}/plans/xxx))。没有 id 就先用 list_recall_queue / find_patient 拿到。`,
      '- 多轮对话:记住上文指代,"该患者 / 他 / 这个人"指本轮对话里刚提到的患者,别再反问是谁。',
    ].join('\n');
  }

  // ── 配置 ──
  private readonly botId = process.env.PAC_WX_AIBOT_BOT_ID?.trim();
  private readonly secret = process.env.PAC_WX_AIBOT_SECRET?.trim();
  private readonly hostName = process.env.PAC_WX_AIBOT_HOST?.trim() || 'jvs-dw';
  // PoC 默认租户 = 瑞尔(ruier);可用 env 覆盖。生产从 企微userid→客服 映射来。
  private readonly tenantId =
    process.env.PAC_WX_AIBOT_TENANT_ID?.trim() || 'ruier-grp';

  constructor(
    private readonly auth: AuthService,
    private readonly assistant: AssistantService,
    private readonly executions: ExecutionService,
    private readonly prisma: PrismaService,
  ) {}

  onModuleInit(): void {
    if (!this.botId || !this.secret) {
      this.logger.log('未配置 PAC_WX_AIBOT_BOT_ID / SECRET,企微机器人长连接跳过。');
      return;
    }
    const client = new AiBot.WSClient({ botId: this.botId, secret: this.secret });
    this.client = client;

    client.on('authenticated', () => this.logger.log('企微智能机器人长连接认证成功 🔐'));
    client.on('error', (e: unknown) =>
      this.logger.error(`企微长连接错误: ${e instanceof Error ? e.message : String(e)}`),
    );

    client.on('event.enter_chat', (frame: WsFrame) => {
      void client.replyWelcome(frame, {
        msgtype: 'text',
        text: {
          content:
            '你好,我是 FRIDAY 助手。可以问我:"今日推荐先打谁""某患者为什么被召回""帮我圈重要价值且有种植机会的患者"。',
        },
      });
    });

    client.on('message.text', (frame: WsFrame) => {
      void this.handleText(frame);
    });

    // 模板卡片按钮点击 → 回写 plan_executions(召回闭环在企微里完成)
    client.on('event.template_card_event', (frame: WsFrame) => {
      void this.handleCardEvent(frame);
    });

    client.connect();
    this.logger.log(`企微智能机器人长连接已启动(host=${this.hostName} tenant=${this.tenantId})`);
  }

  onModuleDestroy(): void {
    this.client?.disconnect();
  }

  /** 收到文本 → 跑助手 → 流式回推企微。 */
  private async handleText(frame: WsFrame): Promise<void> {
    const client = this.client;
    if (!client) return;
    const content = frame.body.text?.content?.trim();
    const wxUserid = frame.body.from?.userid ?? 'unknown';
    const streamId = generateReqId('stream');
    if (frame.body.chatid) this.lastChatId = frame.body.chatid; // 记住群,供主动推送
    if (!content) return;

    const ac = new AbortController();
    const timeout = setTimeout(() => ac.abort(), 90_000); // 安全兜底:90s 未完成则中断

    try {
      await client.replyStream(frame, streamId, '🔍 正在查…', false);
      const token = await this.mintToken(wxUserid);

      // 多轮记忆:按 群+人 取历史,追加本轮提问(每人在群里有独立对话线)
      const convoKey = `${frame.body.chatid ?? 'p'}:${wxUserid}`;
      const history = this.loadConvo(convoKey);
      history.push({ role: 'user', content });

      const result = await this.assistant.chat({
        userToken: token,
        messages: history,
        systemExtra: this.wxSystemExtra,
        // ⚠️ 刻意不传 permissions → 走 'basic' 桶。企微这条路用的是 mintToken 造的**合成身份**
        //   (见 mintToken),不是真人登录态;给它主管工具清单等于把分配能力挂到一个
        //   谁都能在群里 @ 出来的机器人上。真身份映射做完之前,这里就该是最小权限。
        abortSignal: ac.signal,
      });

      let full = '';
      let lastPush = 0;
      let lastPushed = '';
      const flush = async (finish: boolean): Promise<void> => {
        const text = full.trim() || (finish ? '（无内容）' : '🔍 正在查…');
        if (!finish && text === lastPushed) return;
        lastPushed = text;
        // 企微 replyStream 为「全量替换」语义:每次发当前累计全文,finish=true 收尾。
        await client.replyStream(frame, streamId, text, finish);
      };

      for await (const part of result.fullStream) {
        const p = part as Record<string, unknown> & { type: string };
        if (p.type === 'text-delta' || p.type === 'text') {
          full += (p.text as string) ?? (p.delta as string) ?? '';
          const now = Date.now();
          if (now - lastPush > 800) {
            lastPush = now;
            await flush(false);
          }
        }
        // 工具调用/结果在企微通道不单独展示(文字答案已含结论);render_artifact 的 HTML 忽略。
      }
      await flush(true);
      // 落历史:本轮助手回复进会话记忆,供下轮指代("该患者"=刚推荐的人)
      if (full.trim()) {
        history.push({ role: 'assistant', content: full.trim() });
        this.saveConvo(convoKey, history);
      }
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      this.logger.error(`企微消息处理失败(user=${wxUserid}): ${msg}`);
      try {
        await client.replyStream(frame, streamId, `抱歉,出错了:${msg}`, true);
      } catch {
        /* 回推失败忽略 */
      }
    } finally {
      clearTimeout(timeout);
    }
  }

  /**
   * 主动推一条「紧急召回」提醒到群(带召回卡深链)。
   * PoC:取召回池当前优先级最高的一条作示例;生产由事件/定时扫描"新出现的紧急缺口"触发。
   * chatid 缺省用最近活跃群(群里 @过一次即可)。
   */
  async pushUrgentAlert(chatid?: string): Promise<{ sent: boolean; reason?: string }> {
    const client = this.client;
    if (!client) return { sent: false, reason: '长连接未启动' };
    const target = chatid || this.lastChatId;
    if (!target) return { sent: false, reason: '无可用 chatid(先在群里 @一次机器人)' };

    const hostId = await this.resolveHostId();
    const plan = await this.prisma.followupPlan.findFirst({
      where: {
        hostId,
        tenantId: this.tenantId,
        status: 'active',
        assigneeUserId: null,
        supersededAt: null,
      },
      orderBy: { priorityScore: 'desc' },
      include: { reasons: { orderBy: { priorityScore: 'desc' }, take: 1 } },
    });
    if (!plan) return { sent: false, reason: '召回池为空' };

    const patient = await this.prisma.patient.findUnique({
      where: { id: plan.patientId },
      select: { name: true, gender: true, birthDate: true },
    });
    const name = maskName(patient?.name ?? null) ?? '患者';
    const age = patient?.birthDate ? calcAge(patient.birthDate) : null;
    const genderZh = patient?.gender === 'male' ? '男' : patient?.gender === 'female' ? '女' : '';
    const who = age != null ? `${name}(${age}岁${genderZh})` : name;
    const reason = (plan.reasons[0]?.reason ?? '有应治未治缺口,建议尽快联系').slice(0, 110);
    const link = `${this.webBase}/plans/${plan.id}`;
    const score = Math.round(plan.priorityScore);

    // 按钮交互卡片:点整卡 → 跳 web 召回卡;点按钮 → 回写 plan_executions(见 handleCardEvent)。
    const card: TemplateCard = {
      card_type: 'button_interaction',
      source: { desc: 'PAC 召回 · 紧急', desc_color: 2 },
      main_title: { title: `⚠️ 紧急召回 · ${who}`, desc: `优先级 ${score}` },
      sub_title_text: reason,
      card_action: { type: 1, url: link },
      button_list: [
        { text: '已约到诊', style: 1, key: `exec:success_appointed:${plan.id}` },
        { text: '已跟进', style: 1, key: `exec:scheduled_next:${plan.id}` },
        { text: '未接通', style: 2, key: `exec:no_answer:${plan.id}` },
      ],
      // task_id 同一机器人不可重复(企微 42014)→ 加时间戳后缀;字符限 数字/字母/_-@
      task_id: `u_${plan.id.replace(/-/g, '')}_${Date.now().toString(36)}`,
    };
    await client.sendMessage(target, { msgtype: 'template_card', template_card: card });
    this.logger.log(`已推紧急卡片到群 ${target}: ${who}(优先级 ${score})`);
    return { sent: true };
  }

  /** 模板卡片按钮点击 → 回写 plan_executions + 更新卡片显示结果(模拟客服在企微里闭环)。 */
  private async handleCardEvent(frame: WsFrame): Promise<void> {
    const client = this.client;
    if (!client) return;
    // ⚠️ 真实报文 event_key 嵌在 event.template_card_event 下(SDK d.ts 标的 event.event_key 不准),两路兜底。
    const ev = frame.body.event;
    const eventKey: string | undefined = ev?.event_key ?? ev?.template_card_event?.event_key;
    const clicker: string = frame.body.from?.userid ?? 'unknown';
    if (!eventKey?.startsWith('exec:')) return;
    const [, outcome, planId] = eventKey.split(':');
    if (!outcome || !planId) return;

    const link = `${this.webBase}/plans/${planId}`;
    const errStr = (e: unknown): string =>
      e instanceof Error ? e.message : (() => { try { return JSON.stringify(e); } catch { return String(e); } })();

    // ① 回写(核心)——失败即返回
    let planStatus = '';
    let attempts = 0;
    try {
      const scope = await this.defaultScope(clicker);
      const res = await this.executions.submit(scope, planId, `wx:${clicker}`, {
        channel: 'wecom',
        outcome,
        notes: '企微卡片按钮回写(PoC)',
      });
      planStatus = res.planStatus;
      attempts = res.contactAttempts;
      this.logger.log(
        `卡片回写 plan=${planId} outcome=${outcome} → status=${planStatus} by wx:${clicker}`,
      );
    } catch (err) {
      this.logger.warn(`回写失败 plan=${planId}: ${errStr(err)}`);
      return;
    }

    // ② 更新卡片(仅展示,失败不影响已成功的回写)。
    //   企微对 button_interaction 卡片的按钮更新需保持**同类型**(text_notice 会 40058),
    //   故更新为同类 button_interaction:标题改"已处理",按钮换成一个跳转查看(noop key,不再触发回写)。
    const zh = OUTCOME_ZH[outcome] ?? outcome;
    try {
      // 注:card_action.url 为 http://localhost(非 HTTPS / 未登记域名)会被更新接口判 40058;
      //   本地先不带 card_action,线上配真实 HTTPS 域名(PAC_WEB_BASE_URL)再加回跳转。
      const updateCard: TemplateCard = {
        card_type: 'button_interaction',
        source: { desc: 'PAC 召回 · 已处理', desc_color: 3 },
        main_title: { title: `✅ 已处理:${zh}` },
        sub_title_text: `工单 ${planStatus} · 操作人 wx:${clicker} · 第 ${attempts} 次触达`,
        button_list: [{ text: '已完成', style: 1, key: `noop:${planId}` }],
      };
      if (link.startsWith('https://')) updateCard.card_action = { type: 1, url: link };
      await client.updateTemplateCard(frame, updateCard);
    } catch (err) {
      this.logger.warn(`卡片更新失败(回写已成功)plan=${planId}: ${errStr(err)}`);
    }
  }

  /** 默认客服 scope(PoC:统一 host/tenant;clicker 当 operatorUserId)。 */
  private async defaultScope(wxUserid: string): Promise<TenantScopeContext> {
    const hostId = await this.resolveHostId();
    return { hostId, tenantId: this.tenantId, clinicIds: [], sourceUnits: [], userId: `wx:${wxUserid}` };
  }

  /** 取会话历史副本(过期 / 不存在 → 空)。返回副本,避免本轮失败污染已存状态。 */
  private loadConvo(key: string): ModelMessage[] {
    const e = this.convos.get(key);
    if (!e || Date.now() - e.at > this.CONVO_TTL_MS) return [];
    return [...e.msgs];
  }

  /** 存会话历史(只留最近 CONVO_MAX 条;size 过大时顺手清过期项)。 */
  private saveConvo(key: string, msgs: ModelMessage[]): void {
    const trimmed = msgs.length > this.CONVO_MAX ? msgs.slice(-this.CONVO_MAX) : msgs;
    this.convos.set(key, { msgs: trimmed, at: Date.now() });
    if (this.convos.size > 500) {
      const now = Date.now();
      for (const [k, v] of this.convos) {
        if (now - v.at > this.CONVO_TTL_MS) this.convos.delete(k);
      }
    }
  }

  /** 企微 userid → PAC 客服 token(PoC:统一映射到默认 tenant scope)。 */
  private async mintToken(wxUserid: string): Promise<string> {
    const hostId = await this.resolveHostId();
    const role: UserRole = 'staff';
    return this.auth.signAccessToken({
      sub: `wx:${wxUserid}`,
      hostId,
      tenantId: this.tenantId,
      orgScope: [], // 空 = 不限(看该 tenant 整池,PoC);生产按客服归属 org 节点
      role,
      permissions: this.auth.resolvePermissions(role),
    });
  }

  private async resolveHostId(): Promise<string> {
    if (this.cachedHostId) return this.cachedHostId;
    const host = await this.prisma.host.findFirst({ where: { name: this.hostName } });
    if (!host) throw new Error(`host "${this.hostName}" 未找到(先 seed)`);
    return (this.cachedHostId = host.id);
  }
}
