import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { Prisma } from '@prisma/client';
import { PERSONA_FEATURE_SPECS } from '@pac/types';
import { PrismaService } from '../../prisma/prisma.service';
import { AlertService } from '../../common/alerting/alert.service';

/** 相对降幅 ≥10% 且绝对减少 ≥20 人才算"显著下跌"(避免小基数抖动刷屏) */
const DROP_RATIO = 0.1;
const DROP_MIN_ABS = 20;

/**
 * 画像标签覆盖率日报 — 标签质量运营的第一道防线。
 *
 * 动机:规则标签会"静默腐烂"(上游字段改名/枚举漂移/摄入断供时,标签悄悄变空,
 * 没人会发现)。每天早上把 16 标签覆盖率与 24h 前对比,显著下跌发企微 warning,
 * 平时发 info 日报。
 *
 * 实现取巧:画像是版本流(computed_at / superseded_at),"昨天此刻的覆盖率"直接
 * 从版本流反推(当时 active 的版本),无需建快照表、无迁移。
 */
@Injectable()
export class PersonaCoverageMonitorService {
  private readonly logger = new Logger(PersonaCoverageMonitorService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly alert: AlertService,
  ) {}

  /** 默认每天北京时间 08:00(增量 02:30 + 画像重算之后,上班之前) */
  @Cron(process.env.PAC_COVERAGE_CRON || '0 8 * * *', {
    name: 'persona-coverage-daily',
    timeZone: 'Asia/Shanghai',
  })
  async runDaily(): Promise<void> {
    try {
      await this.reportAllTenants();
    } catch (err) {
      this.logger.error(`coverage report failed: ${err instanceof Error ? err.message : err}`);
    }
  }

  async reportAllTenants(): Promise<void> {
    const tenants = await this.prisma.$queryRaw<Array<{ host_id: string; tenant_id: string }>>(
      Prisma.sql`SELECT DISTINCT host_id, tenant_id FROM personas`,
    );
    for (const t of tenants) {
      await this.reportTenant(t.host_id, t.tenant_id);
    }
  }

  private async reportTenant(hostId: string, tenantId: string): Promise<void> {
    const now = new Date();
    const dayAgo = new Date(now.getTime() - 24 * 3600_000);

    const [curTotal, prevTotal, curByKey, prevByKey] = await Promise.all([
      this.totalAt(hostId, tenantId, now),
      this.totalAt(hostId, tenantId, dayAgo),
      this.coverageAt(hostId, tenantId, now),
      this.coverageAt(hostId, tenantId, dayAgo),
    ]);
    if (curTotal === 0) return; // 租户尚无画像,不发

    const drops: string[] = [];
    const lines: string[] = [];
    for (const spec of Object.values(PERSONA_FEATURE_SPECS)) {
      const cur = curByKey.get(spec.key) ?? 0;
      const prev = prevByKey.get(spec.key) ?? 0;
      const pct = ((cur / curTotal) * 100).toFixed(1);
      const delta = cur - prev;
      const deltaStr = delta === 0 ? '—' : delta > 0 ? `+${delta}` : `${delta}`;
      const dropped = prev > 0 && -delta >= DROP_MIN_ABS && -delta / prev >= DROP_RATIO;
      if (dropped) drops.push(`${spec.nameZh} ${prev}→${cur}`);
      lines.push(`${dropped ? '⚠️ ' : ''}${spec.nameZh}: ${cur} (${pct}%) ${deltaStr}`);
    }

    const totalDelta = curTotal - prevTotal;
    await this.alert.send({
      level: drops.length > 0 ? 'warning' : 'info',
      title:
        drops.length > 0
          ? `画像覆盖率显著下跌:${drops.join(';')}`
          : `画像标签覆盖率日报(${tenantId})`,
      body: [
        `租户 ${tenantId} · 画像患者 ${curTotal}(${totalDelta >= 0 ? '+' : ''}${totalDelta} vs 昨日)`,
        '',
        ...lines,
      ].join('\n'),
      context: { kind: 'persona-coverage', hostId, tenantId },
    });
  }

  /** 时刻 at 的画像患者总数(版本流反推:当时 active 的版本) */
  private async totalAt(hostId: string, tenantId: string, at: Date): Promise<number> {
    const rows = await this.prisma.$queryRaw<Array<{ total: number }>>(Prisma.sql`
      SELECT COUNT(DISTINCT p.patient_id)::int AS total
      FROM personas p
      WHERE p.host_id = ${hostId}::uuid AND p.tenant_id = ${tenantId}
        AND p.computed_at <= ${at}
        AND (p.superseded_at IS NULL OR p.superseded_at > ${at})
    `);
    return rows[0]?.total ?? 0;
  }

  /** 时刻 at 各标签覆盖人数 */
  private async coverageAt(hostId: string, tenantId: string, at: Date): Promise<Map<string, number>> {
    const rows = await this.prisma.$queryRaw<Array<{ key: string; cnt: number }>>(Prisma.sql`
      SELECT pf.key AS key, COUNT(DISTINCT p.patient_id)::int AS cnt
      FROM personas p
      JOIN persona_features pf ON pf.persona_id = p.id
      WHERE p.host_id = ${hostId}::uuid AND p.tenant_id = ${tenantId}
        AND p.computed_at <= ${at}
        AND (p.superseded_at IS NULL OR p.superseded_at > ${at})
      GROUP BY pf.key
    `);
    return new Map(rows.map((r) => [r.key, r.cnt]));
  }
}
