// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.

import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

/**
 * NERF Billing Engine — usage-based metering middleware.
 *
 * Tracks API usage per client via SQLite: request counting,
 * credit consumption, tier enforcement, and quota checks.
 */

import { homedir } from 'node:os';
import { join, dirname } from 'node:path';
import { mkdirSync } from 'node:fs';

/** @type {typeof import('better-sqlite3')} */
let Database;

/** Billing plan tiers. */
export const BillingTier = Object.freeze({
  FREE: 'free',
  STARTER: 'starter',
  PROFESSIONAL: 'professional',
  ENTERPRISE: 'enterprise',
});

/**
 * Tier limits keyed by BillingTier value.
 * Each entry: { requestsPerHour, scansPerDay, memoryEntries, skillSearchesPerHour,
 *               complianceReportsPerDay, marketplaceDownloadsPerDay, maxResponseSizeKb }
 */
export const TIER_LIMITS = {
  [BillingTier.FREE]: {
    requestsPerHour: 60,
    scansPerDay: 3,
    memoryEntries: 100,
    skillSearchesPerHour: 20,
    complianceReportsPerDay: 1,
    marketplaceDownloadsPerDay: 5,
    maxResponseSizeKb: 256,
  },
  [BillingTier.STARTER]: {
    requestsPerHour: 500,
    scansPerDay: 20,
    memoryEntries: 5000,
    skillSearchesPerHour: 200,
    complianceReportsPerDay: 10,
    marketplaceDownloadsPerDay: 50,
    maxResponseSizeKb: 1024,
  },
  [BillingTier.PROFESSIONAL]: {
    requestsPerHour: 5000,
    scansPerDay: 100,
    memoryEntries: 50000,
    skillSearchesPerHour: 2000,
    complianceReportsPerDay: 50,
    marketplaceDownloadsPerDay: 500,
    maxResponseSizeKb: 4096,
  },
  [BillingTier.ENTERPRISE]: {
    requestsPerHour: 50000,
    scansPerDay: 1000,
    memoryEntries: 500000,
    skillSearchesPerHour: 20000,
    complianceReportsPerDay: 500,
    marketplaceDownloadsPerDay: 5000,
    maxResponseSizeKb: 16384,
  },
};

/** Credit costs per endpoint type. */
export const ENDPOINT_CREDITS = {
  scan: 10.0,
  diff: 2.0,
  secrets: 2.0,
  'memory/store': 1.0,
  'memory/search': 1.0,
  score: 5.0,
  compliance: 5.0,
  workflow: 3.0,
  skills: 0.5,
  'skills/search': 0.5,
  leaderboard: 0.5,
  health: 0.0,
  stats: 0.0,
};

/**
 * Usage-based billing and metering engine backed by SQLite.
 */
export class MeteringEngine {
  /**
   * @param {string} [dbPath] - Path to SQLite database file. Default: ~/.nerf/billing.db
   */
  constructor(dbPath) {
    if (!Database) {
      Database = require('better-sqlite3');
    }
    const resolved = dbPath || join(homedir(), '.nerf', 'billing.db');
    mkdirSync(dirname(resolved), { recursive: true });
    this._db = new Database(resolved);
    this._db.pragma('journal_mode = WAL');
    this._clientTiers = new Map();
    this._initDb();
  }

  _initDb() {
    this._db.exec(`
      CREATE TABLE IF NOT EXISTS usage_events (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        client_id TEXT NOT NULL,
        endpoint TEXT NOT NULL,
        credits REAL NOT NULL DEFAULT 1.0,
        response_bytes INTEGER DEFAULT 0,
        timestamp REAL NOT NULL,
        metadata TEXT DEFAULT '{}'
      );
      CREATE INDEX IF NOT EXISTS idx_usage_client_ts
        ON usage_events(client_id, timestamp);
      CREATE INDEX IF NOT EXISTS idx_usage_endpoint
        ON usage_events(endpoint, timestamp);

      CREATE TABLE IF NOT EXISTS client_tiers (
        client_id TEXT PRIMARY KEY,
        tier TEXT NOT NULL DEFAULT 'free',
        created_at REAL NOT NULL,
        updated_at REAL NOT NULL
      );
    `);
  }

  /**
   * Set or update a client's billing tier.
   * @param {string} clientId
   * @param {string} tier - BillingTier value
   */
  setClientTier(clientId, tier) {
    const now = Date.now() / 1000;
    this._db
      .prepare(
        `INSERT INTO client_tiers (client_id, tier, created_at, updated_at)
         VALUES (?, ?, ?, ?)
         ON CONFLICT(client_id) DO UPDATE SET tier=?, updated_at=?`,
      )
      .run(clientId, tier, now, now, tier, now);
    this._clientTiers.set(clientId, tier);
  }

  /**
   * Get a client's billing tier.
   * @param {string} clientId
   * @returns {string} BillingTier value
   */
  getClientTier(clientId) {
    if (this._clientTiers.has(clientId)) {
      return this._clientTiers.get(clientId);
    }
    const row = this._db
      .prepare('SELECT tier FROM client_tiers WHERE client_id = ?')
      .get(clientId);
    const tier = row ? row.tier : BillingTier.FREE;
    this._clientTiers.set(clientId, tier);
    return tier;
  }

  /**
   * Record a usage event.
   * @param {object} record
   * @param {string} record.clientId
   * @param {string} record.endpoint
   * @param {number} [record.timestamp]
   * @param {number} [record.creditsConsumed]
   * @param {number} [record.responseSizeBytes]
   * @param {object} [record.metadata]
   */
  recordUsage(record) {
    const ts = record.timestamp ?? Date.now() / 1000;
    const credits = record.creditsConsumed ?? 1.0;
    this._db
      .prepare(
        `INSERT INTO usage_events (client_id, endpoint, credits, response_bytes, timestamp, metadata)
         VALUES (?, ?, ?, ?, ?, ?)`,
      )
      .run(
        record.clientId,
        record.endpoint,
        credits,
        record.responseSizeBytes ?? 0,
        ts,
        JSON.stringify(record.metadata ?? {}),
      );
  }

  /**
   * Check if a client has quota remaining for an endpoint.
   * @param {string} clientId
   * @param {string} endpoint
   * @returns {{ allowed: boolean, remaining: number, limit: number, resetsInS: number, reason?: string }}
   */
  checkQuota(clientId, endpoint) {
    const tier = this.getClientTier(clientId);
    const limits = TIER_LIMITS[tier] || TIER_LIMITS[BillingTier.FREE];
    const now = Date.now() / 1000;
    const hourAgo = now - 3600;
    const dayAgo = now - 86400;

    // Hourly request limit
    const hourlyRow = this._db
      .prepare('SELECT COUNT(*) as cnt FROM usage_events WHERE client_id = ? AND timestamp > ?')
      .get(clientId, hourAgo);
    const hourlyCount = hourlyRow?.cnt ?? 0;

    if (hourlyCount >= limits.requestsPerHour) {
      return {
        allowed: false,
        remaining: 0,
        limit: limits.requestsPerHour,
        resetsInS: 3600,
        reason: 'hourly request limit exceeded',
      };
    }

    // Scan-specific daily limit
    if (endpoint === 'scan') {
      const scanRow = this._db
        .prepare(
          "SELECT COUNT(*) as cnt FROM usage_events WHERE client_id = ? AND endpoint = 'scan' AND timestamp > ?",
        )
        .get(clientId, dayAgo);
      const dailyScans = scanRow?.cnt ?? 0;
      if (dailyScans >= limits.scansPerDay) {
        return {
          allowed: false,
          remaining: 0,
          limit: limits.scansPerDay,
          resetsInS: 86400,
          reason: 'daily scan limit exceeded',
        };
      }
      return {
        allowed: true,
        remaining: limits.scansPerDay - dailyScans,
        limit: limits.scansPerDay,
        resetsInS: 86400,
      };
    }

    return {
      allowed: true,
      remaining: limits.requestsPerHour - hourlyCount,
      limit: limits.requestsPerHour,
      resetsInS: 3600,
    };
  }

  /**
   * Get usage summary for a client over a time period.
   * @param {string} clientId
   * @param {number} [periodHours=24]
   * @returns {object}
   */
  getUsageSummary(clientId, periodHours = 24) {
    const tier = this.getClientTier(clientId);
    const now = Date.now() / 1000;
    const periodStart = now - periodHours * 3600;

    const rows = this._db
      .prepare(
        `SELECT endpoint, COUNT(*) as cnt, SUM(credits) as total_credits
         FROM usage_events
         WHERE client_id = ? AND timestamp > ?
         GROUP BY endpoint`,
      )
      .all(clientId, periodStart);

    const byEndpoint = {};
    let totalRequests = 0;
    let totalCredits = 0;
    let totalScans = 0;

    for (const row of rows) {
      byEndpoint[row.endpoint] = row.cnt;
      totalRequests += row.cnt;
      totalCredits += row.total_credits;
      if (row.endpoint === 'scan') totalScans = row.cnt;
    }

    const limits = TIER_LIMITS[tier] || TIER_LIMITS[BillingTier.FREE];
    return {
      clientId,
      tier,
      periodStart,
      periodEnd: now,
      totalRequests,
      totalScans,
      totalCredits,
      byEndpoint,
      quotaRemaining: {
        requestsHourly: Math.max(0, limits.requestsPerHour - totalRequests),
        scansDaily: Math.max(0, limits.scansPerDay - totalScans),
      },
      overage: totalRequests > limits.requestsPerHour,
    };
  }

  /**
   * Get the credit cost for an endpoint.
   * @param {string} endpoint
   * @returns {number}
   */
  getCreditsForEndpoint(endpoint) {
    for (const [ep, credits] of Object.entries(ENDPOINT_CREDITS)) {
      if (endpoint.endsWith(ep)) return credits;
    }
    return 1.0;
  }

  /** Close database connection. */
  close() {
    this._db.close();
  }
}
