import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { AlertService, type AlertLevel } from '../common/alerting/alert.service';
import { ColdImportService } from '../modules/sync/cold-import/cold-import.service';

/**
 * 数据对账 scope —— DW 源(sql_source.queries key)↔ PAC 侧计数口径。
 * 复用摄入查询做源计数(同闸),PAC 侧按对应 fact_type / 表数去重患者。设计见 /docs/data-reconciliation。
 */
type PacCountKind =
  | { kind: 'patient' }
  | { kind: 'fact'; types: string[] }
  | { kind: 'return_visit' }
  | { kind: 'image_ai' };
const RECONCILE_SCOPES: Array<{ label: string; dwKey: string; pac: PacCountKind }> = [
  { label: '总量',   dwKey: 'fact_client_out',           pac: { kind: 'patient' } },
  { label: '治疗病历', dwKey: 'fact_emr_treatment_out',   pac: { kind: 'fact', types: ['encounter_record', 'emr_record', 'diagnosis_record', 'treatment_record', 'recommendation_record'] } },
  { label: '预约',   dwKey: 'fact_appointment_out',      pac: { kind: 'fact', types: ['appointment_record'] } },
  { label: '结算',   dwKey: 'fact_settlement_out',       pac: { kind: 'fact', types: ['payment_record'] } },
  { label: '影像',   dwKey: 'image_finding_rows',        pac: { kind: 'image_ai' } },
  { label: '咨询',   dwKey: 'fact_consult_out',          pac: { kind: 'fact', types: ['consultation_record'] } },
  { label: '回访',   dwKey: 'fact_returnvisit_out',      pac: { kind: 'return_visit' } },
  { label: '转介',   dwKey: 'fact_customer_referee_out', pac: { kind: 'fact', types: ['referral_record'] } },
];

/**
 * DailyHealthReportService — 每日 09:00(沪)给企微推一份**简洁的线上健康日报**。
 *
 * 目的:把分散的监控(DW 延迟 / 画像覆盖 / 摄入 / 重算 / 召回 / AI 用量)收口成**一条**
 *      每日可对比的指标快照。每条都带"近 24h 增量(diff)",一眼看出昨天系统动了多少、有没有异常。
 *
 * 通道:复用 AlertService → 企微群机器人 webhook(ALERT_WEBHOOK_URL)。
 *      报告整体级别(info/warning/critical)取最差项,企微图标自动 🟢/🟡/🔴。
 *
 * cron:PAC_HEALTH_REPORT_CRON(默认 '0 9 * * *',Asia/Shanghai);DW 08:00 落库后跑。
 */
@Injectable()
export class DailyHealthReportService {
  private readonly logger = new Logger(DailyHealthReportService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly alerter: AlertService,
    private readonly coldImport: ColdImportService,
  ) {}

  @Cron(process.env.PAC_HEALTH_REPORT_CRON || '0 9 * * *', {
    name: 'daily-health-report',
    timeZone: 'Asia/Shanghai',
  })
  async run(): Promise<void> {
    try {
      const { level, title, body } = await this.build();
      await this.alerter.send({ level, title, body });
      this.logger.log(`每日健康报告已推送(${level})`);
    } catch (err) {
      this.logger.error(`每日健康报告生成/推送失败: ${err instanceof Error ? err.message : String(err)}`);
    }
  }

  /** 生成报告(也供手动触发端点预览)。 */
  async build(): Promise<{ level: AlertLevel; title: string; body: string }> {
    const now = Date.now();
    const since24h = new Date(now - 24 * 3_600_000);
    const since7d = new Date(now - 7 * 24 * 3_600_000);
    const warnH = Number(process.env.PAC_LAG_WARN_HOURS ?? '24');
    const errorH = Number(process.env.PAC_LAG_ERROR_HOURS ?? '48');

    // ── 数据量 + 近 24h 增量 ──
    const [patientsTotal, patients24h, factsTotal, facts24h, plansActive, plansAssigned] =
      await Promise.all([
        this.prisma.patient.count(),
        this.prisma.patient.count({ where: { createdAt: { gte: since24h } } }),
        this.prisma.patientFact.count(),
        this.prisma.patientFact.count({ where: { createdAt: { gte: since24h } } }),
        this.prisma.followupPlan.count({ where: { status: 'active', supersededAt: null } }),
        this.prisma.followupPlan.count({ where: { status: 'assigned', supersededAt: null } }),
      ]);

    // ── 摄入:最近一次成功增量 + DW 延迟 + 近 7 日失败 ──
    const lastSync = await this.prisma.syncLog.findFirst({
      where: { resource: 'incremental_bundle', status: 'success', cursorAfter: { not: null } },
      orderBy: { startedAt: 'desc' },
      select: { startedAt: true, transactionsWritten: true, factsEmitted: true, cursorAfter: true },
    });
    const fail7d = await this.prisma.syncLog.count({
      where: { resource: 'incremental_bundle', status: { not: 'success' }, startedAt: { gte: since7d } },
    });
    const lagHours = lastSync?.cursorAfter ? cursorLagHours(lastSync.cursorAfter, now) : null;
    const lagTag = lagHours == null ? '⚪' : lagHours > errorH ? '🔴' : lagHours > warnH ? '🟡' : '🟢';

    // ── 每轮增量明细(近 24h)──
    // DW 现为 2h 刷新 → 每轮理应有数据;**空轮(fetched=0)大概率是异常**(DW 没刷 / 游标失效),
    // 计入 warning。失败轮同样计入。逐轮列出让"每次 pull 的情况"一眼可见。
    const rounds = await this.prisma.syncLog.findMany({
      where: { resource: 'incremental_bundle', startedAt: { gte: since24h } },
      orderBy: { startedAt: 'desc' },
      select: {
        startedAt: true, endedAt: true, status: true,
        fetched: true, transactionsWritten: true, factsEmitted: true, duplicates: true, failed: true,
      },
      take: 20,
    });
    const roundEmpty = rounds.filter((r) => r.status === 'success' && r.fetched === 0).length;
    const roundFailed = rounds.filter((r) => r.status !== 'success' && r.status !== 'running').length;
    const roundLines = rounds.map((r) => {
      const t = fmtClock(r.startedAt);
      const mins = r.endedAt ? Math.round((+new Date(r.endedAt) - +new Date(r.startedAt)) / 60000) : null;
      const dur = mins == null ? '' : mins >= 60 ? ` ${(mins / 60).toFixed(1)}h` : ` ${mins}m`;
      if (r.status === 'running') return `    ${t} ⏳ 进行中`;
      if (r.status !== 'success') return `    ${t} ❌ 失败${dur}`;
      if (r.fetched === 0) return `    ${t} ⚠️ 空轮(0行)${dur}`;
      return `    ${t} ✅ 拉${fmtNum(r.fetched)} tx${fmtNum(r.transactionsWritten)}/dup${fmtNum(r.duplicates)}/失${r.failed} f+${fmtNum(r.factsEmitted)}${dur}`;
    });
    const roundTag = roundFailed > 0 ? '🔴' : roundEmpty > 0 ? '🟡' : '🟢';

    // ── 重算:画像 / 召回 最近一次 ──
    const [lastPersona, lastPlanGen] = await Promise.all([
      this.prisma.personaRecomputeLog.findFirst({ orderBy: { startedAt: 'desc' }, select: { startedAt: true, status: true } }),
      this.prisma.planGenerationLog.findFirst({ orderBy: { startedAt: 'desc' }, select: { startedAt: true, plansCreated: true } }),
    ]);

    // ── 客服执行(近 24h)按 outcome ──
    const execGroups = await this.prisma.planExecution.groupBy({
      by: ['outcome'],
      where: { createdAt: { gte: since24h } },
      _count: { _all: true },
    });
    const execTotal = execGroups.reduce((s, g) => s + g._count._all, 0);
    const execBreakdown = execGroups
      .sort((a, b) => b._count._all - a._count._all)
      .map((g) => `${OUTCOME_ZH[g.outcome] ?? g.outcome} ${g._count._all}`)
      .join(' / ');

    // ── 画像覆盖 ──
    const personaPatients = await this.prisma.persona.count({ where: { supersededAt: null } });
    const coveragePct = patientsTotal > 0 ? Math.round((personaPatients / patientsTotal) * 100) : 0;

    // ── 变动 diff(近 24h)──
    //   计划:每次重算把旧 plan 置 superseded + 写新版本,故"createdAt 计数"里大头是**翻版**,
    //         必须拆:净新召回(version=1,首次入池患者) vs 重算翻版(version>1,旧版本被替换)。
    //   画像:按 personaRecomputeLog.status 统计当日重算次数(成功/部分/失败)。
    //   流出(原有召回没了)有两种:
    //     完成 = status='completed'(客服闭环做成),按 updatedAt 计入当日;
    //     失效 = 24h 内被 superseded 且该患者**已无任何 open plan**(重算发现信号没了 → 自然掉出,
    //            非翻版)。这是 plan_reasons.closedAt 未被填充时唯一能反查的"召回消失",按患者去重。
    const [planNetNew24h, planRevised24h, planCompleted24h, churnedRows, personaRuns] =
      await Promise.all([
        this.prisma.followupPlan.count({ where: { version: 1, createdAt: { gte: since24h } } }),
        this.prisma.followupPlan.count({ where: { version: { gt: 1 }, createdAt: { gte: since24h } } }),
        this.prisma.followupPlan.count({ where: { status: 'completed', updatedAt: { gte: since24h } } }),
        this.prisma.$queryRaw<Array<{ n: number }>>`
          SELECT count(DISTINCT fp.patient_id)::int AS n
          FROM followup_plans fp
          WHERE fp.superseded_at >= ${since24h}
            AND NOT EXISTS (
              SELECT 1 FROM followup_plans o
              WHERE o.patient_id = fp.patient_id
                AND o.superseded_at IS NULL
                AND o.status IN ('active', 'assigned')
            )`,
        this.prisma.personaRecomputeLog.groupBy({
          by: ['status'],
          where: { startedAt: { gte: since24h } },
          _count: { _all: true },
        }),
      ]);
    const planChurnedOut24h = Number(churnedRows[0]?.n ?? 0);
    const prMap: Record<string, number> = Object.fromEntries(
      personaRuns.map((g) => [g.status, g._count._all]),
    );
    const personaRuns24h = personaRuns.reduce((s, g) => s + g._count._all, 0);
    // 各状态都列,保证 breakdown 之和 = 总次数(noop = 重算了但无变化的空跑,健康)
    const personaRunsZh =
      `成功 ${prMap.success ?? 0}` +
      (prMap.noop ? ` / 空跑 ${prMap.noop}` : '') +
      (prMap.partial ? ` / 部分 ${prMap.partial}` : '') +
      (prMap.failed ? ` / 失败 ${prMap.failed}` : '');

    // ── AI 用量(近 24h)──
    const [inv24h, costAgg] = await Promise.all([
      this.prisma.agentInvocation.count({ where: { startedAt: { gte: since24h } } }),
      this.prisma.agentInvocation.aggregate({ _sum: { costYuan: true }, where: { startedAt: { gte: since24h } } }),
    ]);
    const cost24h = Number(costAgg._sum.costYuan ?? 0);

    // ── 整体级别:取最差(摄入失败/DW 严重滞后 → critical;滞后/重算异常 → warning)──
    let level: AlertLevel = 'info';
    if (fail7d > 0 && lastSync == null) level = 'critical';
    if (lagHours != null && lagHours > warnH) level = 'warning';
    if (lastPersona?.status === 'failed' || (lastPlanGen == null)) level = 'warning';
    // DW 2h 刷新下,空轮/失败轮是异常信号
    if (roundEmpty > 0) level = 'warning';
    if (lagHours != null && lagHours > errorH) level = 'critical';
    if (roundFailed > 0) level = 'critical';

    // ── 数据对账(DW 源 vs PAC,去重患者数;设计见 /docs/data-reconciliation)──
    // v1 观察模式:原样报 diff,不设基线、不驱动报告级别(diff 含已知差如瑞泰未全摄,
    // 先看一两周摸清"正常差"再回填基线)。DW 不可达 → 该行降级 "—",不阻断日报。
    const reconcileLines = await this.buildReconcile();

    const dateStr = new Date(now).toLocaleDateString('zh-CN', { timeZone: 'Asia/Shanghai' }).replace(/\//g, '-');
    const title = `每日健康报告 · ${dateStr}`;

    const body = [
      `📥 摄入  ${lagTag} 上次 ${fmtTime(lastSync?.startedAt)} · DW延迟 ${lagHours == null ? '—' : lagHours.toFixed(1) + 'h'} · 近7日失败 ${fail7d}`,
      `📥 增量每轮(近24h · ${roundTag} ${rounds.length}轮${roundEmpty ? ` · 空${roundEmpty}` : ''}${roundFailed ? ` · 失败${roundFailed}` : ''})` +
        (roundLines.length ? '\n' + roundLines.join('\n') : '  无'),
      `🔄 重算  画像 ${fmtTime(lastPersona?.startedAt)}${lastPersona?.status && lastPersona.status !== 'success' ? `(${lastPersona.status})` : ''} · 召回 ${fmtTime(lastPlanGen?.startedAt)}`,
      `📈 数据(近24h↑)  患者 ${fmtNum(patientsTotal)}(+${patients24h}) · 事实 ${fmtWan(factsTotal)}(+${fmtNum(facts24h)})`,
      `🖼 画像(24h)  重算 ${fmtNum(personaRuns24h)} 次(${personaRunsZh}) · 覆盖 ${fmtNum(personaPatients)}/${fmtNum(patientsTotal)}(${coveragePct}%)`,
      `📋 召回(24h)  进:净新 +${planNetNew24h} / 翻版 ~${fmtNum(planRevised24h)} · 出:失效 -${planChurnedOut24h} / 完成 -${planCompleted24h}`,
      `📋 召回池  待办 ${fmtNum(plansActive)} · 进行中 ${fmtNum(plansAssigned)}`,
      `📞 客服执行(24h)  ${execTotal} 次${execBreakdown ? ` · ${execBreakdown}` : ''}`,
      `🤖 AI(24h)  ${inv24h} 次 · ¥${cost24h.toFixed(2)}`,
      `⚖️ 对账(去重患者·DW源vsPAC)${reconcileLines.length ? '\n' + reconcileLines.join('\n') : '  —'}`,
    ].join('\n');

    return { level, title, body };
  }

  /**
   * 数据对账 —— 逐 scope 比 DW 源 vs PAC 的去重患者数。
   * DW 侧复用摄入 sql_source 查询(同闸口径);PAC 侧按对应 fact_type / 表数去重患者。
   * v1 只渲染原始 diff(不设基线、不驱动级别),供观察。失败降级 "—",绝不阻断日报。
   */
  private async buildReconcile(): Promise<string[]> {
    try {
      const host = await this.prisma.host.findFirst({ where: { name: 'jvs-dw' }, select: { id: true } });
      if (!host) return [];
      const dwCounts = await this.coldImport.reconcileDwPatientCounts('jvs-dw', RECONCILE_SCOPES.map((s) => s.dwKey));
      if (Object.keys(dwCounts).length === 0) return []; // 无 sql_source(file/push 宿主)
      const lines: string[] = [];
      for (const s of RECONCILE_SCOPES) {
        const dw = dwCounts[s.dwKey] ?? null;
        const pac = await this.pacPatientCount(host.id, s.pac);
        lines.push(fmtReconcileLine(s.label, dw, pac));
      }
      return lines;
    } catch (err) {
      this.logger.warn(`对账段生成失败(降级,不阻断日报): ${err instanceof Error ? err.message : String(err)}`);
      return [];
    }
  }

  /** PAC 侧去重患者数(当前版本:superseded_at IS NULL)。 */
  private async pacPatientCount(hostId: string, pac: PacCountKind): Promise<number | null> {
    try {
      if (pac.kind === 'patient') {
        return this.prisma.patient.count({ where: { hostId } });
      }
      if (pac.kind === 'return_visit') {
        const r = await this.prisma.$queryRaw<Array<{ c: bigint }>>`
          SELECT count(distinct patient_id) AS c FROM patient_return_visit WHERE host_id = ${hostId}::uuid`;
        return Number(r[0]?.c ?? 0);
      }
      if (pac.kind === 'image_ai') {
        // 影像 AI → diagnosis_record 且 content.code_source='image_ai'(image_finding 装配打的标)
        const r = await this.prisma.$queryRaw<Array<{ c: bigint }>>`
          SELECT count(distinct patient_id) AS c FROM patient_facts
          WHERE host_id = ${hostId}::uuid AND superseded_at IS NULL
            AND type = 'diagnosis_record' AND content->>'code_source' = 'image_ai'`;
        return Number(r[0]?.c ?? 0);
      }
      // fact 类型:跨多类型取去重患者并集(不是各类型之和)
      const r = await this.prisma.$queryRaw<Array<{ c: bigint }>>`
        SELECT count(distinct patient_id) AS c FROM patient_facts
        WHERE host_id = ${hostId}::uuid AND superseded_at IS NULL
          AND type IN (${Prisma.join(pac.types)})`;
      return Number(r[0]?.c ?? 0);
    } catch (err) {
      this.logger.warn(`PAC 计数失败(${JSON.stringify(pac)}): ${err instanceof Error ? err.message : String(err)}`);
      return null;
    }
  }
}

/** cursor_after JSON({resource: "2026-06-21 19:30:00", ...})→ 距今小时数(取最大游标)。 */
function cursorLagHours(cursorAfter: string, now: number): number | null {
  try {
    const cursors = JSON.parse(cursorAfter) as Record<string, string>;
    const maxTs = Math.max(...Object.values(cursors).map((v) => new Date(v).getTime()).filter((t) => !isNaN(t)));
    if (!isFinite(maxTs)) return null;
    return (now - maxTs) / 3_600_000;
  } catch {
    return null;
  }
}

const OUTCOME_ZH: Record<string, string> = {
  success_appointed: '约到诊',
  scheduled_next: '约下次',
  no_answer: '未接通',
  abandoned: '放弃',
  refused: '拒绝',
  rescheduled: '改约',
  considering: '考虑中',
  declined_recent: '近期不考虑',
  needs_doctor: '需医生',
  external_treatment: '外院已治',
  pending_info: '待确认',
  sms_sent: '已发短信',
  proactive_sms: '主动短信',
  marked_invalid: '标记无效',
};

const fmtNum = (n: number): string => n.toLocaleString('en-US');
const fmtWan = (n: number): string => (n >= 10000 ? `${(n / 10000).toFixed(1)}万` : `${n}`);
function fmtTime(d: Date | null | undefined): string {
  if (!d) return '—';
  return new Date(d).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false });
}

/** 只要 HH:mm(每轮明细用,同日多轮不必重复日期)。 */
function fmtClock(d: Date | null | undefined): string {
  if (!d) return '—';
  return new Date(d).toLocaleTimeString('zh-CN', { timeZone: 'Asia/Shanghai', hour: '2-digit', minute: '2-digit', hour12: false });
}

/** 对账单行:DW / PAC / 差(+ = PAC 缺,源比 PAC 多)。v1 只报数不判级,大差多为已知基线(如瑞泰未全摄)。 */
function fmtReconcileLine(label: string, dw: number | null, pac: number | null): string {
  if (dw == null || pac == null) return `    ${label}  DW ${dw == null ? '—' : fmtNum(dw)} / PAC ${pac == null ? '—' : fmtNum(pac)}`;
  const diff = dw - pac;
  const pct = dw > 0 ? (diff / dw) * 100 : 0;
  const sign = diff > 0 ? '+' : '';
  return `    ${label}  DW${fmtNum(dw)}/PAC${fmtNum(pac)} 差${sign}${fmtNum(diff)}(${pct.toFixed(0)}%)`;
}
