import { Injectable } from '@nestjs/common';
// clean subpath(运行时 exports map 放行);类型见 src/types/mcp-sdk.d.ts ambient 声明
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { PERSONA_TAG_FILTER_DIMS, personaTagDimId } from '@pac/types';
import { PrismaService } from '../../prisma/prisma.service';
import { PatientService } from '../patient/patient.service';
import { PersonaService } from '../persona/persona.service';
import { PlanService } from '../plan/plan.service';
import type { ListPlansQueryDto } from '../plan/dto/plan.dto';
import type { TenantScopeContext } from '../../common/decorators/tenant-scope.decorator';
import type { McpAuthContext } from './mcp-auth.service';

function jsonResult(data: unknown) {
  return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
}

/**
 * personaTags 圈人字典(从 PERSONA_TAG_FILTER_DIMS 生成,单一真理源,自动同步)。
 * 格式给 LLM 看:`key(中文): code=中文 / code=中文 …`,一维一行。
 * 入参格式:"key:value" 逗号串;同维多选 OR,跨维 AND(如 "rfm:important_value,urgency_level:urgent")。
 */
const PERSONA_TAGS_HELP = PERSONA_TAG_FILTER_DIMS.map((d) =>
  // ⚠️ 用 personaTagDimId 不用 d.key:source='patient' 的维度 key 为空串、筛选串用 id,
  //   用 key 会给 LLM 一个空维度名,它照着发就永远筛不出人。
  // 开集维度(医生)options 为空 → 提示取值是自由文本,并指向名单接口,免得 LLM 瞎猜姓名。
  d.dynamic
    ? `${personaTagDimId(d)}(${d.nameZh}): <医生姓名,精确匹配;名单见 GET /pac/v1/plans/doctors>`
    : `${personaTagDimId(d)}(${d.nameZh}): ${d.options.map((o) => `${o.value}=${o.zh}`).join(' / ')}`,
).join('\n');

const PERSONA_TAGS_DESC =
  '画像圈人(可选):"key:value" 逗号串,同维多选 OR、跨维 AND。可用维度与取值:\n' +
  PERSONA_TAGS_HELP;

function maskPhone(phone: string | null): string | null {
  if (!phone) return null;
  const d = phone.replace(/\D/g, '');
  if (d.length < 7) return '***';
  return `${d.slice(0, 3)}****${d.slice(-4)}`;
}

/** 患者基础卡(掩码手机号,去掉真号 PII) */
function patientCard(p: {
  id: string;
  externalId: string;
  name: string | null;
  phone: string | null;
  gender: string | null;
  birthDate: string | null;
  status: string;
}) {
  return {
    id: p.id,
    externalId: p.externalId,
    name: p.name,
    phoneMasked: maskPhone(p.phone),
    gender: p.gender,
    birthDate: p.birthDate,
    status: p.status,
  };
}

/**
 * McpServerFactory — 每个(已鉴权的)请求构建一个 scoped McpServer。
 *
 * 6 个**只读**工具,全部薄封装现有 service。租户隔离两条路径(均等价安全):
 *   - 调不带 scope 的读(persona.getCurrent)的工具 → 先过 assertPatientInScope 堵越权;
 *   - get_facts → getTimeline 内部 findFirst{host,tenant}+NotFound 自守,无需额外 assert。
 */
@Injectable()
export class McpServerFactory {
  constructor(
    private readonly prisma: PrismaService,
    private readonly patient: PatientService,
    private readonly persona: PersonaService,
    private readonly plans: PlanService,
  ) {}

  build(ctx: McpAuthContext): McpServer {
    const { scope, permissions } = ctx;
    const server = new McpServer({ name: 'pac-patient-tools', version: '0.1.0' });

    server.registerTool(
      'find_patient',
      {
        description:
          '按姓名/手机号/患者号模糊检索患者,返回极简候选卡片(手机号掩码)用于消歧。先用它拿到 patientId,再调其它工具。',
        inputSchema: {
          query: z.string().describe('姓名 / 手机号 / 患者号(模糊匹配)'),
          limit: z.number().int().min(1).max(50).optional(),
        },
      },
      async ({ query, limit }) => jsonResult(await this.patient.search(scope, query, limit ?? 10)),
    );

    server.registerTool(
      'get_patient_overview',
      {
        description:
          '一次拉取患者 360 全景:画像要点 + 近期事实 + 当前召回计划。召回前的首选工具,省去多次往返。',
        inputSchema: { patientId: z.string() },
      },
      async ({ patientId }) => {
        await this.assertPatientInScope(scope, patientId);
        const [persona, recallPlan, timeline] = await Promise.all([
          this.persona.getCurrent(scope, patientId),
          this.activeRecallPlan(scope, patientId),
          this.patient.getTimeline(scope, patientId, 20),
        ]);
        return jsonResult({
          patient: patientCard(timeline.patient),
          persona,
          recallPlan,
          recentFacts: timeline.facts,
        });
      },
    );

    server.registerTool(
      'get_persona',
      {
        description:
          '患者全量画像:RFM 价值分群 / 生命周期 / 治疗史 / 治疗敏感 / 禁忌 / 潜在治疗 等所有 persona 特征(PAC 的推断分析层)。',
        inputSchema: { patientId: z.string() },
      },
      async ({ patientId }) => {
        await this.assertPatientInScope(scope, patientId);
        return jsonResult(await this.persona.getCurrent(scope, patientId));
      },
    );

    server.registerTool(
      'get_facts',
      {
        description:
          '患者客观事实数据(诊断/治疗/预约/收费等 active 版本时间轴)。只给事实,不含推断分析(分析见 get_persona)。',
        inputSchema: {
          patientId: z.string(),
          limit: z.number().int().min(1).max(200).optional(),
        },
      },
      async ({ patientId, limit }) => {
        // 无需单独 assertPatientInScope:getTimeline 内部 findFirst{hostId,tenantId} 且
        // 越租户即抛 NotFound —— 已自带租户隔离(与其它 4 个工具的显式 assert 等价安全)。
        const t = await this.patient.getTimeline(scope, patientId, limit ?? 50);
        return jsonResult({ patient: patientCard(t.patient), facts: t.facts });
      },
    );

    server.registerTool(
      'get_recall_plan',
      {
        description:
          '患者当前的召回计划:为什么召回(场景/原因)、优先级、目标诊所、状态。无活跃计划则返回 null。',
        inputSchema: { patientId: z.string() },
      },
      async ({ patientId }) => {
        await this.assertPatientInScope(scope, patientId);
        return jsonResult(await this.activeRecallPlan(scope, patientId));
      },
    );

    server.registerTool(
      'list_recall_queue',
      {
        description:
          '召回工作台:列出待跟进的召回计划(view=pool 未分配召回池 / mine 我名下),按优先级排序。回答"现在该联系谁 / 今日推荐"。' +
          '\n支持画像圈人(personaTags)+ 病种(scenario)+ 关键字(keyword:姓名/手机/患者号)+ 真实号(phoneVerified)筛选。' +
          '\n' +
          PERSONA_TAGS_DESC,
        inputSchema: {
          view: z.enum(['pool', 'mine']).optional().describe('pool=未分配召回池(默认) / mine=我名下'),
          personaTags: z.string().optional().describe(PERSONA_TAGS_DESC),
          scenario: z
            .string()
            .optional()
            .describe('召回场景过滤,如 treatment_initiation_recall(启治召回)'),
          status: z.enum(['active', 'assigned', 'completed', 'abandoned']).optional(),
          keyword: z.string().optional().describe('姓名 / 手机 / 患者号 模糊匹配'),
          phoneVerified: z.boolean().optional().describe('只看真实号码(已核实)'),
          sort: z
            .enum(['priority_desc', 'priority_asc', 'created_desc'])
            .optional()
            .describe('排序,默认 priority_desc'),
          clinicId: z.string().optional(),
          limit: z.number().int().min(1).max(100).optional(),
        },
      },
      async ({ view, personaTags, scenario, status, keyword, phoneVerified, sort, clinicId, limit }) => {
        // 注:schema 的诊所过滤字段是 targetClinicId(不是 clinicId);以前 cast 成 any 传 clinicId
        // 被静默忽略。这里类型化构造,过滤真正生效。
        const query: ListPlansQueryDto = {
          view: view ?? 'pool',
          sort: sort ?? 'priority_desc',
          page: 1,
          pageSize: limit ?? 20,
          ...(clinicId ? { targetClinicId: clinicId } : {}),
          ...(personaTags ? { personaTags } : {}),
          ...(scenario ? { scenario } : {}),
          ...(status ? { status } : {}),
          ...(keyword ? { keyword } : {}),
          ...(phoneVerified !== undefined ? { phoneVerified } : {}),
        };
        return jsonResult(await this.plans.list(scope, query, permissions));
      },
    );

    server.registerTool(
      'recall_queue_stats',
      {
        description:
          '召回池数字概览:总量 + 优先级分档(高≥70 / 中 40-69 / 低<40)+ 病种分布。' +
          '"今日推荐"先用它给摘要(如"今天 320 条,缺牙 80 / 牙周 60,高优 45"),再用 list_recall_queue 取明细。' +
          '支持同样的 view / personaTags / scenario / clinicId 圈定范围。',
        inputSchema: {
          view: z.enum(['pool', 'mine']).optional().describe('pool=召回池(默认) / mine=我名下'),
          personaTags: z.string().optional().describe(PERSONA_TAGS_DESC),
          scenario: z.string().optional(),
          clinicId: z.string().optional(),
        },
      },
      async ({ view, personaTags, scenario, clinicId }) => {
        const query: ListPlansQueryDto = {
          view: view ?? 'pool',
          sort: 'priority_desc',
          page: 1,
          pageSize: 1, // stats 不取明细行
          ...(clinicId ? { targetClinicId: clinicId } : {}),
          ...(personaTags ? { personaTags } : {}),
          ...(scenario ? { scenario } : {}),
        };
        return jsonResult(await this.plans.queueStats(scope, query, permissions));
      },
    );

    return server;
  }

  /** 越权防护:确认该 patientId 属当前租户(persona.getCurrent 等不带 scope 的读之前必调)。 */
  private async assertPatientInScope(scope: TenantScopeContext, patientId: string): Promise<void> {
    const p = await this.prisma.patient.findFirst({
      // MCP 走 @Public 无 ALS 上下文 → 纵深防御不兜,品牌隔离全靠这里显式过滤
      where: {
        id: patientId,
        hostId: scope.hostId,
        tenantId: scope.tenantId,
        ...(scope.sourceUnits.length ? { sourceUnit: { in: scope.sourceUnits } } : {}),
      },
      select: { id: true },
    });
    if (!p) throw new Error(`patient ${patientId} 不在当前租户范围内`);
  }

  private async activeRecallPlan(scope: TenantScopeContext, patientId: string) {
    const plan = await this.prisma.followupPlan.findFirst({
      where: {
        hostId: scope.hostId,
        tenantId: scope.tenantId,
        patientId,
        ...(scope.sourceUnits.length ? { patient: { sourceUnit: { in: scope.sourceUnits } } } : {}),
        status: { in: ['active', 'assigned'] },
        supersededAt: null,
      },
      orderBy: { priorityScore: 'desc' },
      select: {
        id: true,
        status: true,
        priorityScore: true,
        targetClinicId: true,
        assigneeUserId: true,
        snoozedUntil: true,
      },
    });
    if (!plan) return null;
    const reasons = await this.prisma.planReason.findMany({
      where: { planId: plan.id },
      select: { scenario: true, priorityScore: true, reason: true },
    });
    return {
      planId: plan.id,
      status: plan.status,
      priorityScore: plan.priorityScore,
      targetClinicId: plan.targetClinicId,
      assigned: !!plan.assigneeUserId,
      snoozedUntil: plan.snoozedUntil ? plan.snoozedUntil.toISOString() : null,
      reasons,
    };
  }
}
