import { Injectable, NotFoundException } from '@nestjs/common';
import type {
  AiInvocationDetail,
  AiInvocationListItem,
  AiInvocationListQuery,
  AiInvocationStats,
  ListAiInvocationsResponse,
} from '@pac/types';
import type { Prisma, AgentInvocation } from '@prisma/client';
import { PrismaService } from '../../prisma/prisma.service';
import type { TenantScopeContext } from '../../common/decorators/tenant-scope.decorator';

/**
 * AiInvocationsService — 提供 /admin/ai-invocations 的查询接口。
 *
 * Scope:严格按 caller 的 host_id 过滤(host self admin 只看自己的调用)。
 * 不暴露写操作(replay 等留给后续 phase)。
 */
@Injectable()
export class AiInvocationsService {
  constructor(private readonly prisma: PrismaService) {}

  async list(
    scope: TenantScopeContext,
    query: AiInvocationListQuery,
  ): Promise<ListAiInvocationsResponse> {
    const where: Prisma.AgentInvocationWhereInput = {
      hostId: scope.hostId,
      tenantId: scope.tenantId,
    };
    if (query.callKey) where.callKey = query.callKey;
    if (query.promptVersion) where.promptVersion = query.promptVersion;
    if (query.modelName) where.modelName = query.modelName;
    if (query.status) where.status = query.status;
    if (query.evalMode) where.evalMode = query.evalMode;
    if (query.linkedPlanId) where.linkedPlanId = query.linkedPlanId;
    if (query.linkedPatientId) where.linkedPatientId = query.linkedPatientId;

    const skip = (query.page - 1) * query.pageSize;
    const [total, rows] = await Promise.all([
      this.prisma.agentInvocation.count({ where }),
      this.prisma.agentInvocation.findMany({
        where,
        orderBy: { startedAt: 'desc' },
        skip,
        take: query.pageSize,
        // 显式 select 不带 inputSnapshot / promptTemplate / output(那些大,列表不需要)
        select: this.listSelect(),
      }),
    ]);

    return {
      items: rows.map(toListItem),
      page: query.page,
      pageSize: query.pageSize,
      total,
    };
  }

  async detail(scope: TenantScopeContext, id: string): Promise<AiInvocationDetail> {
    const row = await this.prisma.agentInvocation.findUnique({ where: { id } });
    if (!row || row.hostId !== scope.hostId || row.tenantId !== scope.tenantId) {
      throw new NotFoundException(`AiInvocation ${id} not found`);
    }
    return toDetail(row);
  }

  async stats(scope: TenantScopeContext): Promise<AiInvocationStats> {
    const t24h = new Date(Date.now() - 86400_000);
    const where = { hostId: scope.hostId, tenantId: scope.tenantId, startedAt: { gte: t24h } };

    const [total, failed, costAgg, byCallKeyRows] = await Promise.all([
      this.prisma.agentInvocation.count({ where }),
      this.prisma.agentInvocation.count({ where: { ...where, status: 'failed' } }),
      this.prisma.agentInvocation.aggregate({
        where,
        _sum: { costYuan: true },
      }),
      this.prisma.agentInvocation.groupBy({
        by: ['callKey'],
        where,
        _count: { _all: true },
        _sum: { costYuan: true },
        // Prisma groupBy 不直接支持 timestamp 差值平均;avgDurationMs 在后续算
      }),
    ]);

    // 二次 query 拿每个 callKey 的平均 latency(endedAt - startedAt)— Prisma raw 最简
    // 为了不爆 SQL,只在数据量小(<=20 callKey)时算;数量多直接 null
    const callKeys = byCallKeyRows.map((r) => r.callKey);
    const avgDurationMap = new Map<string, number | null>();
    if (callKeys.length <= 20) {
      for (const k of callKeys) {
        const rows = await this.prisma.agentInvocation.findMany({
          where: { ...where, callKey: k, endedAt: { not: null } },
          select: { startedAt: true, endedAt: true },
          take: 200, // 采样上限
        });
        if (rows.length === 0) {
          avgDurationMap.set(k, null);
        } else {
          const sum = rows.reduce(
            (acc, r) => acc + (r.endedAt!.getTime() - r.startedAt.getTime()),
            0,
          );
          avgDurationMap.set(k, Math.round(sum / rows.length));
        }
      }
    }

    return {
      count24h: total,
      cost24h: Number(costAgg._sum.costYuan ?? 0),
      failureRate24h: total > 0 ? failed / total : 0,
      byCallKey: byCallKeyRows
        .map((r) => ({
          callKey: r.callKey,
          count: r._count._all,
          costYuan: Number(r._sum.costYuan ?? 0),
          avgDurationMs: avgDurationMap.get(r.callKey) ?? null,
        }))
        .sort((a, b) => b.count - a.count),
    };
  }

  // ─────────────────────────────────────────────
  // helpers
  // ─────────────────────────────────────────────

  private listSelect() {
    return {
      id: true,
      kind: true,
      callKey: true,
      promptVersion: true,
      modelProvider: true,
      modelName: true,
      status: true,
      cacheHit: true,
      evalMode: true,
      workflowRunId: true,
      parentInvocationId: true,
      linkedPatientId: true,
      linkedPlanId: true,
      promptTokens: true,
      completionTokens: true,
      totalTokens: true,
      cachedInputTokens: true,
      reasoningTokens: true,
      costYuan: true,
      judgeScore: true,
      errorMessage: true,
      startedAt: true,
      endedAt: true,
    } satisfies Prisma.AgentInvocationSelect;
  }
}

// ─────────────────────────────────────────────
// Serializers
// ─────────────────────────────────────────────

type ListRowSubset = Pick<
  AgentInvocation,
  | 'id' | 'kind' | 'callKey' | 'promptVersion' | 'modelProvider' | 'modelName'
  | 'status' | 'cacheHit' | 'evalMode' | 'workflowRunId' | 'parentInvocationId'
  | 'linkedPatientId' | 'linkedPlanId' | 'promptTokens' | 'completionTokens'
  | 'totalTokens' | 'cachedInputTokens' | 'reasoningTokens'
  | 'costYuan' | 'judgeScore' | 'errorMessage' | 'startedAt' | 'endedAt'
>;

function toListItem(r: ListRowSubset): AiInvocationListItem {
  const durationMs = r.endedAt ? r.endedAt.getTime() - r.startedAt.getTime() : null;
  return {
    id: r.id,
    kind: r.kind,
    callKey: r.callKey,
    promptVersion: r.promptVersion,
    modelProvider: r.modelProvider,
    modelName: r.modelName,
    status: r.status,
    cacheHit: r.cacheHit,
    evalMode: r.evalMode,
    workflowRunId: r.workflowRunId,
    parentInvocationId: r.parentInvocationId,
    linkedPatientId: r.linkedPatientId,
    linkedPlanId: r.linkedPlanId,
    promptTokens: r.promptTokens,
    completionTokens: r.completionTokens,
    totalTokens: r.totalTokens,
    cachedInputTokens: r.cachedInputTokens,
    reasoningTokens: r.reasoningTokens,
    costYuan: Number(r.costYuan ?? 0),
    judgeScore: r.judgeScore !== null ? Number(r.judgeScore) : null,
    errorMessage: r.errorMessage,
    startedAt: r.startedAt.toISOString(),
    endedAt: r.endedAt?.toISOString() ?? null,
    durationMs,
  };
}

function toDetail(r: AgentInvocation): AiInvocationDetail {
  return {
    ...toListItem(r),
    inputSnapshot: r.inputSnapshot,
    promptTemplate: r.promptTemplate,
    output: r.output,
    outputText: r.outputText,
    judgeRubric: r.judgeRubric,
  };
}
