import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
import { hashSecret, randomCode, sha256 } from '@pac/utils';
import type { Host, Prisma } from '@prisma/client';
import type {
  CreateHostRequest,
  CreateHostResponse,
  ListHostsResponse,
  HostSummary,
  HostDetail,
  HostStats,
  PushSecretEntry,
  RotatePushSecretResponse,
  RotateCallbackSecretResponse,
  RotateSecretResponse,
  RemovePushSecretResponse,
  UpdateHostRequest,
  PullConfig,
  ActionUrlsConfig,
} from '@pac/types';
import { SyncDirection } from '@pac/types';
import { PrismaService } from '../../prisma/prisma.service';

/**
 * HostsService — PAC 团队上录入 / 维护 host 接入配置的 admin 后台。
 *
 * 这套接口是**给 PAC 自己人**用的(`PLATFORM_MANAGE` 权限),不暴露给 host。
 *
 * 接入新 host 的流程(对应 docs/host-integration.md §10):
 *   1. POST /admin/hosts                拿 appId + appSecret(plaintext 一次性)+ pushSecret(可选)
 *   2. 把 appId/appSecret 发给 host backend(他们去换 JWT)
 *   3. host 给裸表 + 数据字典 → PAC 团队写 `data/<host>/assemblers/*.yaml`(AssemblerEngine 消化)
 *   4. 跑 cold-import 验证,如有 drift 调 yaml 重跑
 *   5. 密钥失落 → POST /admin/hosts/:id/rotate-secret 重发
 *   6. push 密钥轮换 → POST /admin/hosts/:id/rotate-push-secret(数组前插 grace)
 *   7. 接入完成 / 退出 → POST /admin/hosts/:id/deactivate
 *
 * 注:v1 的 `Platform.fieldMapping` 字段已删除 — 字段映射 / 枚举翻译 / 结构投影
 *     全部交给 AssemblerEngine 的 yaml 配置,Host 表只管凭证 / endpoints / actionUrls。
 *
 * 安全:
 *   - `appSecret` / `pushSecret` plaintext **仅在创建 / 轮换时一次性返回**,落库 scrypt 哈希
 *   - 后续 GET / LIST 只读 hash 数(不读 hash 本身)
 *   - rotate-app-secret = 替换(旧立即失效);rotate-push-secret = 数组前插(支持 grace)
 */
@Injectable()
export class HostsService {
  constructor(private readonly prisma: PrismaService) {}

  /** 按 id 取 host,不存在抛 NotFound(收口多处重复的 findUnique + guard) */
  private async getHostOrThrow(id: string): Promise<Host> {
    const host = await this.prisma.host.findUnique({ where: { id } });
    if (!host) throw new NotFoundException(`host ${id} not found`);
    return host;
  }

  // ─────────────────────────────────────────────────────────
  // Create
  // ─────────────────────────────────────────────────────────

  async create(req: CreateHostRequest): Promise<CreateHostResponse> {
    const existing = await this.prisma.host.findUnique({ where: { name: req.name } });
    if (existing) {
      throw new ConflictException(`host name="${req.name}" already exists`);
    }

    const appId = this.generateAppId(req.name);
    const appSecret = randomCode(32);
    const pushSecret = req.pushEnabled ? randomCode(32) : null;

    const host = await this.prisma.host.create({
      data: {
        name: req.name,
        appId,
        appSecretHash: hashSecret(appSecret),
        pullConfig: this.toPrismaJson(req.pullConfig),
        // push 密钥存**原文**:HMAC 对称,PAC 验签必须与宿主持同一把 key(不能存哈希,否则永远验不过)。
        pushSecretHashes: pushSecret ? [pushSecret] : [],
        actionUrls: this.toPrismaJson(req.actionUrls) ?? {},
        active: true,
      },
    });

    return {
      id: host.id,
      name: host.name,
      appId: host.appId,
      appSecret,
      pushSecret,
      active: host.active,
    };
  }

  // ─────────────────────────────────────────────────────────
  // List / Get
  // ─────────────────────────────────────────────────────────

  async list(): Promise<ListHostsResponse> {
    const rows = await this.prisma.host.findMany({ orderBy: { createdAt: 'desc' } });
    return { items: rows.map((p) => this.toSummary(p)) };
  }

  async get(id: string): Promise<HostSummary> {
    const p = await this.getHostOrThrow(id);
    return this.toSummary(p);
  }

  // ─────────────────────────────────────────────────────────
  // Update(PATCH 语义 — 只改传入的字段)
  // ─────────────────────────────────────────────────────────

  async update(id: string, req: UpdateHostRequest): Promise<HostSummary> {
    const before = await this.getHostOrThrow(id);

    const data: Prisma.HostUpdateInput = {};
    if (req.pullConfig !== undefined) data.pullConfig = this.toPrismaJson(req.pullConfig);
    if (req.actionUrls !== undefined) data.actionUrls = this.toPrismaJson(req.actionUrls);
    if (req.active !== undefined) data.active = req.active;
    // callbackUrl:传 null → 关闭回执;传 url → 启用/改地址
    if (req.callbackUrl !== undefined) data.callbackUrl = req.callbackUrl ?? null;

    const updated = await this.prisma.host.update({ where: { id }, data });
    return this.toSummary(updated);
  }

  // ─────────────────────────────────────────────────────────
  // Rotate appSecret(替换,旧 secret 立即失效)
  // ─────────────────────────────────────────────────────────

  async rotateAppSecret(id: string): Promise<RotateSecretResponse> {
    const p = await this.getHostOrThrow(id);
    const appSecret = randomCode(32);
    await this.prisma.host.update({
      where: { id },
      data: { appSecretHash: hashSecret(appSecret) },
    });
    return { appSecret };
  }

  // ─────────────────────────────────────────────────────────
  // Rotate pushSecret(数组前插 — 旧 key 仍接受,grace period)
  // ─────────────────────────────────────────────────────────

  async rotatePushSecret(id: string): Promise<RotatePushSecretResponse> {
    const p = await this.getHostOrThrow(id);
    const pushSecret = randomCode(32);
    // 存原文(理由同 create):HMAC 验签需双方持同一 key。
    const newHashes = [pushSecret, ...p.pushSecretHashes];
    await this.prisma.host.update({
      where: { id },
      data: { pushSecretHashes: newHashes },
    });
    return { pushSecret, totalKeys: newHashes.length };
  }

  // ─────────────────────────────────────────────────────────
  // Rotate callbackSecret(数组前插;PAC 签名用 → 存**原文**,非哈希)
  // ─────────────────────────────────────────────────────────

  async rotateCallbackSecret(id: string): Promise<RotateCallbackSecretResponse> {
    const p = await this.getHostOrThrow(id);
    const callbackSecret = randomCode(32);
    // 存原文:PAC 出站签名必须持有与宿主同一把 key(HMAC 对称)。
    const next = [callbackSecret, ...p.callbackSecrets];
    await this.prisma.host.update({
      where: { id },
      data: { callbackSecrets: next },
    });
    return { callbackSecret, totalKeys: next.length };
  }

  async removeCallbackSecretBySuffix(id: string, suffix: string): Promise<RemovePushSecretResponse> {
    const p = await this.getHostOrThrow(id);
    const matchIdx = p.callbackSecrets.findIndex((s) => fingerprintSuffix(s) === suffix);
    if (matchIdx < 0) {
      throw new BadRequestException(`callback secret suffix=${suffix} not found in rotation`);
    }
    if (p.callbackSecrets.length === 1) {
      throw new BadRequestException('不能删除唯一的 callback secret;请先添加新 secret 再删旧的');
    }
    const next = p.callbackSecrets.filter((_, i) => i !== matchIdx);
    await this.prisma.host.update({
      where: { id },
      data: { callbackSecrets: next },
    });
    return { totalKeysRemaining: next.length };
  }

  // ─────────────────────────────────────────────────────────
  // Deactivate(软停)
  // ─────────────────────────────────────────────────────────

  async deactivate(id: string): Promise<HostSummary> {
    const p = await this.getHostOrThrow(id);
    const updated = await this.prisma.host.update({
      where: { id },
      data: { active: false },
    });
    return this.toSummary(updated);
  }

  // ─────────────────────────────────────────────────────────
  // Detail(给 host self admin 页用 — 含 pullConfig 内容 + push secret meta + stats)
  // ─────────────────────────────────────────────────────────

  async getDetail(id: string): Promise<HostDetail> {
    const p = await this.getHostOrThrow(id);
    const stats = await this.getStats(id);
    return {
      id: p.id,
      name: p.name,
      appId: p.appId,
      createdAt: p.createdAt.toISOString(),
      updatedAt: p.updatedAt.toISOString(),
      active: p.active,
      appSecret: maskHash(p.appSecretHash, p.updatedAt),
      pushSecrets: p.pushSecretHashes.map((h, i) => toPushSecretEntry(h, i)),
      // 回执密钥存原文 → suffix 用指纹(sha256 末 8),不回显原文任何片段
      callbackSecrets: p.callbackSecrets.map((s, i) => toCallbackSecretEntry(s, i)),
      pullConfig: (p.pullConfig as PullConfig | null) ?? null,
      actionUrls: (p.actionUrls as ActionUrlsConfig | null) ?? {},
      callbackUrl: p.callbackUrl ?? null,
      stats,
    };
  }

  async getStats(id: string): Promise<HostStats> {
    const now = Date.now();
    const t24h = new Date(now - 86400_000);
    const t30d = new Date(now - 30 * 86400_000);

    const [
      patientCount,
      txn24h,
      txn30d,
      factCount,
      lastPullRow,
      lastPushRow,
      ai24h,
      pullFailed24h,
      pullTotal24h,
      recentEventsRows,
    ] = await Promise.all([
      this.prisma.patient.count({ where: { hostId: id } }),
      this.prisma.patientTransaction.count({ where: { hostId: id, occurredAt: { gte: t24h } } }),
      this.prisma.patientTransaction.count({ where: { hostId: id, occurredAt: { gte: t30d } } }),
      this.prisma.patientFact.count({ where: { hostId: id, status: 'active' } }),
      this.prisma.syncLog.findFirst({
        where: { hostId: id, direction: 'pull', NOT: { triggeredBy: { startsWith: 'reconcile:' } } },
        orderBy: { startedAt: 'desc' },
        select: { startedAt: true, status: true, cursorAfter: true },
      }),
      this.prisma.syncLog.findFirst({
        where: { hostId: id, direction: 'push' },
        orderBy: { startedAt: 'desc' },
        select: { startedAt: true, status: true },
      }),
      this.prisma.agentInvocation.aggregate({
        where: { hostId: id, startedAt: { gte: t24h } },
        _count: { _all: true },
        _sum: { costYuan: true },
      }),
      this.prisma.syncLog.count({
        where: { hostId: id, startedAt: { gte: t24h }, status: 'failed' },
      }),
      this.prisma.syncLog.count({ where: { hostId: id, startedAt: { gte: t24h } } }),
      this.prisma.syncLog.findMany({
        where: { hostId: id },
        orderBy: { startedAt: 'desc' },
        take: 10,
        select: {
          direction: true,
          triggeredBy: true,
          status: true,
          startedAt: true,
          transactionsWritten: true,
          errorMessage: true,
        },
      }),
    ]);

    return {
      patientCount,
      transactionCount24h: txn24h,
      transactionCount30d: txn30d,
      factCount,
      lastPullAt: lastPullRow?.startedAt.toISOString() ?? null,
      lastPullStatus: lastPullRow?.status ?? null,
      lastPullCursor: lastPullRow?.cursorAfter ?? null,
      lastPushAt: lastPushRow?.startedAt.toISOString() ?? null,
      lastPushStatus: lastPushRow?.status ?? null,
      aiInvocations24h: ai24h._count._all,
      aiCostYuan24h: Number(ai24h._sum.costYuan ?? 0),
      failureRate24h: pullTotal24h > 0 ? pullFailed24h / pullTotal24h : 0,
      recentEvents: recentEventsRows.map((e) => ({
        kind: detectKind(e.direction, e.triggeredBy ?? ''),
        status: e.status,
        startedAt: e.startedAt.toISOString(),
        transactionsWritten: e.transactionsWritten ?? 0,
        errorMessage: e.errorMessage,
      })),
    };
  }

  /**
   * 宿主推送记录(自助排查)—— 列最近 N 批 push 的结果。
   *
   * 对接期宿主开发最需要的"事后可查":当时的同步响应若没打进日志,这里仍能看到
   * 每批 source / 收行数 / 落库数 / 去重 / 失败 / 报错原因。`?dryRun=1` 的预检批次
   * 由 triggeredBy 前缀 `push-dryrun:` 标识,单独打标,不与正式推送混淆。
   */
  async getPushLogs(hostId: string, limit = 50) {
    const take = Math.min(Math.max(limit, 1), 200);
    const rows = await this.prisma.syncLog.findMany({
      where: { hostId, direction: SyncDirection.PUSH },
      orderBy: { startedAt: 'desc' },
      take,
    });
    return {
      items: rows.map((r) => ({
        id: r.id,
        source: r.resource,
        status: r.status,
        dryRun: (r.triggeredBy ?? '').startsWith('push-dryrun:'),
        fetched: r.fetched ?? 0,
        transactionsWritten: r.transactionsWritten ?? 0,
        duplicates: r.duplicates ?? 0,
        failed: r.failed ?? 0,
        errorMessage: r.errorMessage,
        startedAt: r.startedAt.toISOString(),
        endedAt: r.endedAt?.toISOString() ?? null,
      })),
    };
  }

  // ─────────────────────────────────────────────────────────
  // Remove push secret(按 suffix 定位,精确移除某个 grace key)
  // ─────────────────────────────────────────────────────────

  async removePushSecretBySuffix(id: string, suffix: string): Promise<RemovePushSecretResponse> {
    const p = await this.getHostOrThrow(id);
    const matchIdx = p.pushSecretHashes.findIndex((h) => fingerprintSuffix(h) === suffix);
    if (matchIdx < 0) {
      throw new BadRequestException(`push secret suffix=${suffix} not found in rotation`);
    }
    if (p.pushSecretHashes.length === 1) {
      throw new BadRequestException('不能删除唯一的 push secret;请先添加新 secret 再删旧的');
    }
    const next = p.pushSecretHashes.filter((_, i) => i !== matchIdx);
    await this.prisma.host.update({
      where: { id },
      data: { pushSecretHashes: next },
    });
    return { totalKeysRemaining: next.length };
  }

  // ─────────────────────────────────────────────────────────
  // Helpers
  // ─────────────────────────────────────────────────────────

  private toSummary(p: Host): HostSummary {
    return {
      id: p.id,
      name: p.name,
      appId: p.appId,
      pullEnabled: !!p.pullConfig,
      pushEnabled: p.pushSecretHashes.length > 0,
      pushKeyCount: p.pushSecretHashes.length,
      active: p.active,
      createdAt: p.createdAt.toISOString(),
      updatedAt: p.updatedAt.toISOString(),
    };
  }

  /// appId:`pac_<slug>_<rand8>`(对齐 host-integration §2.2 例子风格)
  private generateAppId(name: string): string {
    const slug = name.toLowerCase().replace(/[^a-z0-9_-]/g, '').slice(0, 32);
    const rand = randomCode(8).slice(0, 8).toLowerCase().replace(/[^a-z0-9]/g, '0');
    return `pac_${slug}_${rand}`;
  }

  private toPrismaJson(v: unknown): Prisma.InputJsonValue | undefined {
    if (v === undefined) return undefined;
    if (v === null) return undefined;
    return v as Prisma.InputJsonValue;
  }
}

// ─────────────────────────────────────────────────────────
// 模块辅助:secret mask + suffix(用于 detail 展示 + 删除定位)
// ─────────────────────────────────────────────────────────

function maskHash(hash: string, updatedAt: Date): { masked: string; rotatedAt: string | null } {
  if (!hash) return { masked: '(empty)', rotatedAt: null };
  // hash 已不是明文,但仍 mask 中间;admin 只关心"是不是同一条"
  const prefix = hash.slice(0, 6);
  const suffix = hash.slice(-4);
  return {
    masked: `${prefix}...${suffix}`,
    rotatedAt: updatedAt.toISOString(),
  };
}

/// 密钥存原文(HMAC 对称)→ 用 sha256 指纹的末 8 位做识别/删除定位,绝不回显原文片段。
/// push / callback 共用。
function fingerprintSuffix(secret: string): string {
  return sha256(secret).slice(-8);
}

function toPushSecretEntry(secret: string, index: number): PushSecretEntry {
  return {
    index,
    suffix: fingerprintSuffix(secret),
    role: index === 0 ? 'current' : 'grace',
  };
}

const toCallbackSecretEntry = toPushSecretEntry;

/**
 * sync_log.direction + triggeredBy → 事件 kind(给前端时间轴用)
 */
function detectKind(
  direction: string,
  triggeredBy: string,
): 'pull' | 'push' | 'reconcile' | 'cold_import' {
  if (direction === 'push') return 'push';
  if (triggeredBy?.startsWith('reconcile:')) return 'reconcile';
  if (triggeredBy?.startsWith('cold_import:')) return 'cold_import';
  return 'pull';
}
