import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { randomUUID } from 'node:crypto';
import { SyncDirection, SyncStatus, type PullConfig } from '@pac/types';
import { PrismaService } from '../../../prisma/prisma.service';
import { PullStrategyRegistry } from '../adapters/adapter.registry';
import { PipelineDispatcher, type DispatchMetrics } from '../pipeline/pipeline-dispatcher.service';
import type { SyncContext } from '../adapters/adapter.interface';

/**
 * PullOrchestrator — 主动 pull 单 host 单次完整跑。
 *
 * 流程:
 *   1. SyncLog start(direction=pull, triggered_by='scheduled:pull' 或 'manual:...')
 *   2. 读上次 cursor(SyncLog 表里同 host/tenant 最近一条 success 的 cursor_after)
 *   3. 循环 pullPage(cursor) 直到 nextCursor=null
 *   4. 每页 → PipelineDispatcher 写 transaction + fact
 *   5. cursor 推进:本次写完所有页才 advance(中间失败保留上次水位重跑)
 *   6. SyncLog finalize
 *
 * 触发场景:
 *   - cron 调度(SyncService 挂 @Cron 调本服务)
 *   - manual API trigger(/sync/trigger)
 */
@Injectable()
export class PullOrchestrator {
  private readonly logger = new Logger(PullOrchestrator.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly registry: PullStrategyRegistry,
    private readonly dispatcher: PipelineDispatcher,
  ) {}

  async runForHost(
    hostId: string,
    options: { triggeredBy?: string } = {},
  ): Promise<PullRunResult> {
    const host = await this.prisma.host.findUnique({ where: { id: hostId } });
    if (!host) throw new NotFoundException(`Host ${hostId} not found`);
    if (!host.active) {
      this.logger.warn(`host=${host.name} 已停用,跳过 pull`);
      return { skipped: true, reason: 'host_inactive' };
    }
    if (!host.pullConfig) {
      this.logger.warn(`host=${host.name} 无 pullConfig,跳过 pull`);
      return { skipped: true, reason: 'no_pull_config' };
    }

    const pullCfg = host.pullConfig as PullConfig;
    const tenantId = this.resolveTenantId(host.name);
    const runId = randomUUID();
    const triggeredBy = options.triggeredBy ?? 'scheduled:pull';

    // ─── 1. SyncLog start ───
    const lastCursor = await this.readLastCursor(host.id, tenantId);
    const syncLog = await this.prisma.syncLog.create({
      data: {
        hostId: host.id,
        tenantId,
        direction: SyncDirection.PULL,
        resource: 'pull_run',
        triggeredBy,
        status: SyncStatus.RUNNING,
        cursorBefore: lastCursor,
      },
    });

    // ─── 2. 调 strategy ───
    const ctx: SyncContext = { hostId: host.id, tenantId, pullConfig: pullCfg, runId };
    const strategy = this.registry.resolve(ctx);
    if (strategy.prepare) await strategy.prepare(ctx);

    let cursor: string | null = lastCursor;
    const totals = zeroDispatchMetrics();
    let pages = 0;
    let firstError: string | null = null;

    try {
      while (true) {
        const page = await strategy.pullPage(ctx, cursor);
        pages++;
        const isEmpty = Object.values(page.tables).every((t) => t.length === 0);
        if (!isEmpty) {
          // emits 来源:
          //   - mock / shortcut adapter:页里自带 emitsPerResource(canonical 表名 → emits)
          //   - 真实 HTTP adapter(W5+):未提供时,后续接 AssemblerEngine 跑 yaml 拿 emits
          const emitsPerResource = page.emitsPerResource ?? {};
          const metrics = await this.dispatcher.dispatchTables({
            tables: page.tables,
            hostId: host.id,
            tenantId,
            sourceContext: `pull:${host.name}`,
            syncLogId: syncLog.id,
            emitsResolver: (resource) => emitsPerResource[resource],
          });
          addMetrics(totals, metrics);
        }
        if (!page.nextCursor) break;
        cursor = page.nextCursor;
      }
    } catch (err) {
      firstError = err instanceof Error ? err.message : String(err);
      this.logger.error(`pull host=${host.name} 失败: ${firstError}`);
    }

    // ─── 3. SyncLog finalize ───
    const status =
      firstError && totals.transactionsWritten === 0
        ? SyncStatus.FAILED
        : firstError
          ? SyncStatus.PARTIAL
          : SyncStatus.SUCCESS;

    await this.prisma.syncLog.update({
      where: { id: syncLog.id },
      data: {
        status,
        cursorAfter: cursor,  // 成功才推进;失败仍写,但只下次会用 cursorBefore
        fetched: totals.patientsUpserted + totals.transactionsWritten + totals.duplicates,
        transactionsWritten: totals.transactionsWritten,
        factsEmitted: totals.factsCreated + totals.factsSuperseded,
        duplicates: totals.duplicates,
        failed: totals.failed,
        errorMessage: firstError,
        endedAt: new Date(),
      },
    });

    this.logger.log(
      `pull done host=${host.name} status=${status} pages=${pages} txn=${totals.transactionsWritten} dups=${totals.duplicates} failed=${totals.failed}`,
    );

    return {
      skipped: false,
      syncLogId: syncLog.id,
      status,
      pages,
      metrics: totals,
      cursorAdvancedTo: cursor,
    };
  }

  private async readLastCursor(hostId: string, tenantId: string): Promise<string | null> {
    const last = await this.prisma.syncLog.findFirst({
      where: {
        hostId,
        tenantId,
        direction: SyncDirection.PULL,
        status: SyncStatus.SUCCESS,
        // 排除 reconcile run(它不动 cursor,但为安全过滤 triggered_by)
        NOT: { triggeredBy: { startsWith: 'reconcile:' } },
      },
      orderBy: { startedAt: 'desc' },
      select: { cursorAfter: true },
    });
    return last?.cursorAfter ?? null;
  }

  // demo / mock host 用 'demo' tenant;真实接入后从 host config 或 token 取
  private resolveTenantId(hostName: string): string {
    return hostName.includes('demo') || hostName.includes('mock') ? 't_demo' : 't_default';
  }
}

export type PullRunResult =
  | { skipped: true; reason: string }
  | {
      skipped: false;
      syncLogId: string;
      status: string;
      pages: number;
      metrics: DispatchMetrics;
      cursorAdvancedTo: string | null;
    };

function zeroDispatchMetrics(): DispatchMetrics {
  return {
    patientsUpserted: 0,
    transactionsWritten: 0,
    duplicates: 0,
    failed: 0,
    factsCreated: 0,
    factsSuperseded: 0,
    factsUnchanged: 0,
    factsFailed: 0,
    personaEnqueued: 0,
  };
}

function addMetrics(target: DispatchMetrics, add: DispatchMetrics): void {
  target.patientsUpserted += add.patientsUpserted;
  target.transactionsWritten += add.transactionsWritten;
  target.duplicates += add.duplicates;
  target.failed += add.failed;
  target.factsCreated += add.factsCreated;
  target.factsSuperseded += add.factsSuperseded;
  target.factsUnchanged += add.factsUnchanged;
  target.factsFailed += add.factsFailed;
  target.personaEnqueued += add.personaEnqueued;
}
