import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
import { hashSecret, randomCode } from '@pac/utils';
import type { Host, Prisma } from '@prisma/client';
import type {
  CreateHostRequest,
  CreateHostResponse,
  ListHostsResponse,
  HostSummary,
  HostDetail,
  HostStats,
  PushSecretEntry,
  RotatePushSecretResponse,
  RotateSecretResponse,
  RemovePushSecretResponse,
  UpdateHostRequest,
  PullConfig,
  ActionUrlsConfig,
} 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) {}

  // ─────────────────────────────────────────────────────────
  // 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),
        pushSecretHashes: pushSecret ? [hashSecret(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.prisma.host.findUnique({ where: { id } });
    if (!p) throw new NotFoundException(`host ${id} not found`);
    return this.toSummary(p);
  }

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

  async update(id: string, req: UpdateHostRequest): Promise<HostSummary> {
    const before = await this.prisma.host.findUnique({ where: { id } });
    if (!before) throw new NotFoundException(`host ${id} not found`);

    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;

    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.prisma.host.findUnique({ where: { id } });
    if (!p) throw new NotFoundException(`host ${id} not found`);
    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.prisma.host.findUnique({ where: { id } });
    if (!p) throw new NotFoundException(`host ${id} not found`);
    const pushSecret = randomCode(32);
    const newHashes = [hashSecret(pushSecret), ...p.pushSecretHashes];
    await this.prisma.host.update({
      where: { id },
      data: { pushSecretHashes: newHashes },
    });
    return { pushSecret, totalKeys: newHashes.length };
  }

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

  async deactivate(id: string): Promise<HostSummary> {
    const p = await this.prisma.host.findUnique({ where: { id } });
    if (!p) throw new NotFoundException(`host ${id} not found`);
    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.prisma.host.findUnique({ where: { id } });
    if (!p) throw new NotFoundException(`host ${id} not found`);
    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)),
      pullConfig: (p.pullConfig as PullConfig | null) ?? null,
      actionUrls: (p.actionUrls as ActionUrlsConfig | 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,
      })),
    };
  }

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

  async removePushSecretBySuffix(id: string, suffix: string): Promise<RemovePushSecretResponse> {
    const p = await this.prisma.host.findUnique({ where: { id } });
    if (!p) throw new NotFoundException(`host ${id} not found`);
    const matchIdx = p.pushSecretHashes.findIndex((h) => extractSuffix(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 展示 + 删除定位)
// ─────────────────────────────────────────────────────────

/**
 * Hash 形态:`salt:hash`(scrypt)或 `dev:hash`(dev stub)。
 * 这里只是给 admin "认出来"是哪条密钥用,不暴露原文也不暴露完整 hash。
 * 取末 8 个字符做后缀。
 */
function extractSuffix(hashOrSecret: string): string {
  const s = hashOrSecret.length > 8 ? hashOrSecret.slice(-8) : hashOrSecret;
  return s;
}

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(),
  };
}

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

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