import { Injectable, ForbiddenException } from '@nestjs/common';
import { PrismaService } from '@core/database/prisma/prisma.service';
import { createLogger } from '@core/observability/logging/config/winston.config';
import type { AgentQuotaScope } from '@prisma/client';

const logger = createLogger('QuotaService');

export interface QuotaCheck {
  /** 是否在硬上限内（false → 直接拒绝） */
  readonly allowed: boolean;
  /** 是否已超过软上限（ModelRouter 应 degrade 到便宜模型） */
  readonly degradeRequested: boolean;
  readonly remainingTokens: number;
  readonly remainingCostUsd: number;
  /** 命中的 scope（先 USER 再 ORGANIZATION），用于审计 */
  readonly limitingScope?: AgentQuotaScope;
}

/**
 * PR3.6 Quota / Rate limiting。
 *
 * 检查顺序（任一硬限制击穿即拒绝）：
 *   1. USER scope（如果配置了）
 *   2. ORGANIZATION scope
 *
 * 软上限达到 → 返回 `degradeRequested=true`，由调用方（ModelRouter / messages.service）
 *              改路由到便宜模型而不是拒绝。
 *
 * 用量按月度滚动；月初自动开新周期（lazy create on first usage）。
 */
@Injectable()
export class QuotaService {
  constructor(private readonly prisma: PrismaService) {}

  async check(args: { organizationId: string; userId: string }): Promise<QuotaCheck> {
    const periodStart = currentPeriodStart();
    // 按 USER 优先
    const userCheck = await this.checkScope({
      organizationId: args.organizationId,
      scope: 'USER',
      scopeRefId: args.userId,
      periodStart,
    });
    if (userCheck && !userCheck.allowed) return userCheck;

    const orgCheck = await this.checkScope({
      organizationId: args.organizationId,
      scope: 'ORGANIZATION',
      scopeRefId: args.organizationId,
      periodStart,
    });
    if (orgCheck) {
      // 任一 scope 要 degrade → degrade
      const degrade =
        (userCheck?.degradeRequested ?? false) || orgCheck.degradeRequested;
      return { ...orgCheck, degradeRequested: degrade };
    }

    // 都没配 quota → 无限制
    return {
      allowed: true,
      degradeRequested: userCheck?.degradeRequested ?? false,
      remainingTokens: Number.MAX_SAFE_INTEGER,
      remainingCostUsd: Number.MAX_SAFE_INTEGER,
    };
  }

  async assertAllowed(args: { organizationId: string; userId: string }): Promise<QuotaCheck> {
    const c = await this.check(args);
    if (!c.allowed) {
      throw new ForbiddenException(
        `Quota exceeded (${c.limitingScope}): tokens or cost over monthly hard limit`,
      );
    }
    return c;
  }

  /**
   * turn 跑完回填用量（USER + ORGANIZATION 双计）。
   */
  async recordUsage(args: {
    organizationId: string;
    userId: string;
    tokens: number;
    costUsd: number;
  }): Promise<void> {
    const periodStart = currentPeriodStart();
    await Promise.all([
      this.upsertUsage({
        organizationId: args.organizationId,
        scope: 'USER',
        scopeRefId: args.userId,
        periodStart,
        tokens: args.tokens,
        costUsd: args.costUsd,
      }),
      this.upsertUsage({
        organizationId: args.organizationId,
        scope: 'ORGANIZATION',
        scopeRefId: args.organizationId,
        periodStart,
        tokens: args.tokens,
        costUsd: args.costUsd,
      }),
    ]);
  }

  // ----- private -----

  private async checkScope(args: {
    organizationId: string;
    scope: AgentQuotaScope;
    scopeRefId: string;
    periodStart: Date;
  }): Promise<QuotaCheck | null> {
    const quota = await this.prisma.agentQuota.findUnique({
      where: { scope_scopeRefId: { scope: args.scope, scopeRefId: args.scopeRefId } },
    });
    if (!quota || !quota.enabled) return null;

    const usage = await this.prisma.agentQuotaUsage.findUnique({
      where: {
        scope_scopeRefId_periodStart: {
          scope: args.scope,
          scopeRefId: args.scopeRefId,
          periodStart: args.periodStart,
        },
      },
    });
    const tokensUsed = usage?.tokensUsed ?? 0;
    const costUsed = usage?.costUsdUsed ?? 0;

    const remainingTokens = quota.monthlyTokens - tokensUsed;
    const remainingCostUsd = quota.monthlyCostUsd - costUsed;
    const hardOverflow = remainingTokens <= 0 || remainingCostUsd <= 0;
    const softHitRatio = Math.max(
      tokensUsed / quota.monthlyTokens,
      costUsed / quota.monthlyCostUsd,
    );

    return {
      allowed: !hardOverflow,
      degradeRequested: softHitRatio >= quota.softLimitRatio && !hardOverflow,
      remainingTokens: Math.max(0, remainingTokens),
      remainingCostUsd: Math.max(0, remainingCostUsd),
      limitingScope: args.scope,
    };
  }

  private async upsertUsage(args: {
    organizationId: string;
    scope: AgentQuotaScope;
    scopeRefId: string;
    periodStart: Date;
    tokens: number;
    costUsd: number;
  }) {
    try {
      await this.prisma.agentQuotaUsage.upsert({
        where: {
          scope_scopeRefId_periodStart: {
            scope: args.scope,
            scopeRefId: args.scopeRefId,
            periodStart: args.periodStart,
          },
        },
        create: {
          organizationId: args.organizationId,
          scope: args.scope,
          scopeRefId: args.scopeRefId,
          periodStart: args.periodStart,
          tokensUsed: args.tokens,
          costUsdUsed: args.costUsd,
        },
        update: {
          tokensUsed: { increment: args.tokens },
          costUsdUsed: { increment: args.costUsd },
        },
      });
    } catch (err) {
      logger.warn(`upsertUsage failed (${args.scope}): ${(err as Error).message}`);
    }
  }
}

function currentPeriodStart(): Date {
  const now = new Date();
  return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
}
