import {
  Injectable,
  Logger,
  NotImplementedException,
  NotFoundException,
} from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as yaml from 'js-yaml';
import { PrismaService } from '../../prisma/prisma.service';
import { FactsService } from '../facts/facts.service';
import { AlertService } from '../../common/alerting/alert.service';
import { PullStrategyRegistry } from './adapters/adapter.registry';
import { ColdImportService } from './cold-import/cold-import.service';
import { PersonaService } from '../persona/persona.service';
import { PlanEngineService } from '../plan/engine/plan-engine.service';
import type { TenantScopeContext } from '../../common/decorators/tenant-scope.decorator';

/**
 * SyncService — 增量拉取 + 漂移检测 orchestration + 单患者刷新
 *
 * 暂未实施部分:周期性 pull / push 路径(host 真实 API 就绪后再启用)
 *
 * 已实施:
 *   - W4 末 单患者刷新(runPullForPatient)— 详情页"刷新"按钮调用
 *     · ClickHouse 直连 DW,SQL 注入 WHERE patient_id+brand 精确范围
 *     · resource='patient_refresh' 跟 daily incremental cursor 完全隔离
 *     · 跑完顺手 persona.recompute + planEngine.recomputeForPatient
 */
@Injectable()
export class SyncService {
  private readonly logger = new Logger(SyncService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly facts: FactsService,
    private readonly registry: PullStrategyRegistry,
    private readonly alerter: AlertService,
    private readonly coldImport: ColdImportService,
    private readonly persona: PersonaService,
    private readonly planEngine: PlanEngineService,
  ) {}

  /**
   * Sync 拉数据 cron — env 驱动(env 未设默认不跑)
   * PAC_SYNC_HOURLY_CRON:
   *   不设         → 不跑(local + 当前所有环境默认 — pull 通道未启用)
   *   '0 * * * *'  → host pull 通道接通后启用,小时级拉
   *
   * 事件驱动主路径(秒级响应)由 pipeline-dispatcher 入队 persona-recompute,本 cron 只是兜底拉数据。
   */
  @Cron(process.env.PAC_SYNC_HOURLY_CRON || '0 0 31 12 *' /* never — pull 通道未启用 */, {
    name: 'sync-scheduled-tick',
  })
  async runScheduledForAll(): Promise<void> {
    // 暂未启用 — pull 通道接通后挂回本 cron
  }

  async runPull(_hostId: string): Promise<{ runId: string; emitted: number }> {
    throw new NotImplementedException(
      'Pull 通道暂未实施,数据请通过 cold-import 入站(见 pnpm cold-import)',
    );
  }

  /**
   * 单患者按需刷新(详情页"刷新"按钮)
   *
   * 流程:
   *   1. 查 patient(scope 限定 host+tenant 隔离)
   *   2. 反查 manifest tenant_map → brand 中文名(SQL WHERE 用)
   *   3. ColdImportService.importPatient → 单患者全量从 DW 拉(SQL 注入 patient_id+brand)
   *   4. Persona.recompute(该 patient)
   *   5. PlanEngine.recomputeForPatient(该 patient)
   *
   * 不动 cursor / 不阻塞 daily incremental cron(resource='patient_refresh' 隔离)
   */
  async runPullForPatient(
    scope: TenantScopeContext,
    patientId: string,
  ): Promise<{
    syncLogId: string;
    status: string;
    transactionsWritten: number;
    factsEmitted: number;
    plansCreated: number;
  }> {
    // 1. patient lookup(隔离基线)
    const patient = await this.prisma.patient.findFirst({
      where: { id: patientId, hostId: scope.hostId, tenantId: scope.tenantId },
    });
    if (!patient) {
      throw new NotFoundException(`Patient ${patientId} not found in tenant`);
    }

    // 2. host + manifest 反查 brand
    const host = await this.prisma.host.findUnique({ where: { id: scope.hostId } });
    if (!host) {
      throw new NotFoundException(`Host ${scope.hostId} not found`);
    }
    const dataDir = this.resolveDataDir(host.name);
    const brand = this.resolveBrandFromTenant(dataDir, patient.tenantId);

    this.logger.log(
      `patient-refresh: host=${host.name} patient=${patient.externalId}(${patient.id}) brand=${brand} dir=${dataDir}`,
    );

    // 3. 跑单患者 cold-import(SQL 注入 WHERE patient_id+brand)
    const r = await this.coldImport.importPatient(dataDir, {
      patientExternalId: patient.externalId,
      brand,
      triggeredBy: `patient_refresh:${patient.externalId}`,
    });

    // 4. Persona 重算(失败不阻断,log warn)
    try {
      await this.persona.recompute({
        patientId: patient.id,
        source: `patient_refresh:${r.runId}`,
      });
    } catch (err) {
      this.logger.warn(
        `patient-refresh persona patient=${patient.id}: ${(err as Error).message}`,
      );
    }

    // 5. Plan 重算(per patient — runForPatient 会跑该 tenant 全量但 selector 只命中该 patient)
    let plansCreated = 0;
    try {
      const r2 = await this.planEngine.recomputeForPatient({
        hostId: scope.hostId,
        tenantId: scope.tenantId,
        patientId: patient.id,
      });
      plansCreated = r2.plansCreated;
    } catch (err) {
      this.logger.warn(
        `patient-refresh plan patient=${patient.id}: ${(err as Error).message}`,
      );
    }

    this.logger.log(
      `patient-refresh DONE: patient=${patient.externalId} status=${r.status} ` +
        `txns=${r.totals.transactionsWritten} facts=${r.totals.factsCreated + r.totals.factsSuperseded} ` +
        `plansCreated=${plansCreated}`,
    );

    return {
      syncLogId: r.syncLogId!,
      status: r.status,
      transactionsWritten: r.totals.transactionsWritten,
      factsEmitted: r.totals.factsCreated + r.totals.factsSuperseded,
      plansCreated,
    };
  }

  async checkPullDrift(): Promise<void> {
    // 暂未启用 — pull 通道接通后启用漂移监控
  }

  // ─── helpers ───

  /// 数据目录:env PAC_INCREMENTAL_DATA_DIR 优先,否则用 cwd/data
  /// (跟 SyncIncrementalSchedulerService 共用 env,运维只需配一处)
  /// 用 cwd 而非 __dirname:
  ///   - dev:  swc 编译到 dist,__dirname=dist/modules/sync → ../../../data 落到 dist/data(不存在)
  ///   - prod: 容器 WORKDIR=/app/apps/pac-service,__dirname=dist/modules/sync 同样越界
  ///   两边 cwd 都是 apps/pac-service 根,cwd/data 稳定指向源 data 目录
  private resolveDataDir(hostName: string): string {
    const base =
      process.env.PAC_INCREMENTAL_DATA_DIR ??
      path.resolve(process.cwd(), 'data');
    return path.join(base, hostName);
  }

  /// 从 manifest.yaml tenant_map 反查 tenantId(UUID) → brand 中文名(SQL WHERE 用)
  /// 找不到回退用 tenantId 本身(动态 pass-through host)
  private resolveBrandFromTenant(dir: string, tenantId: string): string {
    const manifestPath = path.join(dir, 'manifest.yaml');
    if (!fs.existsSync(manifestPath)) {
      this.logger.warn(`manifest.yaml not found at ${manifestPath}, fallback brand=${tenantId}`);
      return tenantId;
    }
    try {
      const parsed = yaml.load(fs.readFileSync(manifestPath, 'utf-8')) as {
        tenant_map?: Record<string, string>;
        brand_to_tenant?: Record<string, string>;
      };
      const map = parsed.tenant_map ?? parsed.brand_to_tenant ?? {};
      // 反向找:map { 瑞尔: <UUID> } → 给 UUID 找回 "瑞尔"
      for (const [brand, uuid] of Object.entries(map)) {
        if (uuid === tenantId) return brand;
      }
      // 未匹配 → pass-through(field 模式 host 直接用 brand 当 tenantId)
      this.logger.warn(
        `tenant_map 反查未命中 tenantId=${tenantId},pass-through 用原值当 brand`,
      );
      return tenantId;
    } catch (err) {
      this.logger.warn(`manifest 解析失败:${(err as Error).message},fallback`);
      return tenantId;
    }
  }
}
