import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service';
import { AlertService, type AlertLevel } from '../common/alerting/alert.service';

/**
 * 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,
  ) {}

  @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 ? '🟡' : '🟢';

    // ── 重算:画像 / 召回 最近一次 ──
    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';
    if (lagHours != null && lagHours > errorH) level = 'critical';

    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}`,
      `🔄 重算  画像 ${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)}`,
    ].join('\n');

    return { level, title, body };
  }
}

/** 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 });
}
