import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { and, desc, eq, sql } from 'drizzle-orm';
import { DbService } from '@/common/db/db.service';
import { usageRecords, subscriptions, plans } from '@/db/schema';
import { DomainError, ErrorCodes } from '@/common/errors/domain-error';
import { D, todayIso } from '@/common/util';

/**
 * Per-tenant API call metering and daily plan limit (maxApiCallsPerDay).
 *
 * Counting one row per request would hammer the database, so calls are
 * accumulated in memory and flushed every few seconds into usage_records
 * (one row per tenant per day). The limit check uses the persisted count
 * plus the un-flushed delta, so a tenant cannot exceed the limit by more
 * than one flush interval across API nodes. Limits are cached per minute.
 */
@Injectable()
export class ApiUsageService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(ApiUsageService.name);
  private pending = new Map<string, number>(); // tenantId -> calls not yet flushed
  private persisted = new Map<string, { date: string; count: number; limit: number | null; at: number }>();
  private timer?: NodeJS.Timeout;

  constructor(private readonly db: DbService) {}

  onModuleInit() {
    this.timer = setInterval(() => void this.flush(), 5000);
    this.timer.unref();
  }

  async onModuleDestroy() {
    if (this.timer) clearInterval(this.timer);
    await this.flush();
  }

  /** Drops the cached plan limit for a tenant (call after a subscription change). Other API nodes refresh within a minute. */
  invalidate(tenantId: string) {
    this.persisted.delete(tenantId);
  }

  /** Counts the call and throws PLAN_LIMIT (429) when the tenant's daily API quota is exhausted. */
  async record(tenantId: string): Promise<void> {
    const today = todayIso();
    let snap = this.persisted.get(tenantId);
    if (!snap || snap.date !== today || Date.now() - snap.at > 60_000) {
      snap = await this.load(tenantId, today);
      this.persisted.set(tenantId, snap);
    }
    const delta = (this.pending.get(tenantId) ?? 0) + 1;
    if (snap.limit && snap.count + delta > snap.limit) {
      throw new DomainError(ErrorCodes.PLAN_LIMIT, `Daily API call limit reached (${snap.limit.toLocaleString()}). Resets at midnight (Asia/Kuwait).`, 429);
    }
    this.pending.set(tenantId, delta);
  }

  private async load(tenantId: string, today: string) {
    return this.db.runBypass(async (tx) => {
      const [u] = await tx.select({ q: usageRecords.quantity }).from(usageRecords).where(and(eq(usageRecords.tenantId, tenantId), eq(usageRecords.metric, 'API_CALLS'), eq(usageRecords.usageDate, today))).limit(1);
      const [p] = await tx.select({ limits: plans.limits }).from(subscriptions).innerJoin(plans, eq(plans.id, subscriptions.planId)).where(eq(subscriptions.tenantId, tenantId)).orderBy(desc(subscriptions.createdAt)).limit(1);
      const limit = Number((p?.limits as Record<string, unknown> | undefined)?.maxApiCallsPerDay ?? 0) || null;
      return { date: today, count: Number(u?.q ?? 0), limit, at: Date.now() };
    });
  }

  async flush(): Promise<void> {
    if (!this.pending.size) return;
    const batch = this.pending;
    this.pending = new Map();
    const today = todayIso();
    try {
      await this.db.runBypass(async (tx) => {
        for (const [tenantId, n] of batch) {
          await tx
            .insert(usageRecords)
            .values({ tenantId, metric: 'API_CALLS', usageDate: today, quantity: D(n) })
            .onConflictDoUpdate({ target: [usageRecords.tenantId, usageRecords.metric, usageRecords.usageDate], set: { quantity: sql`${usageRecords.quantity} + ${n}` } });
          const snap = this.persisted.get(tenantId);
          if (snap && snap.date === today) snap.count += n;
        }
      });
    } catch (e) {
      // Put the counts back so they are not lost; next flush retries.
      for (const [t, n] of batch) this.pending.set(t, (this.pending.get(t) ?? 0) + n);
      this.logger.error(`API usage flush failed: ${(e as Error).message}`);
    }
  }
}
