/**
 * Free tier enforcement middleware for SentryAgent.ai AgentIdP.
 * Rejects requests that exceed free-tier usage limits (agents or API calls).
 * Skipped entirely when BILLING_ENABLED=false.
 */

import { Request, Response, NextFunction, RequestHandler } from 'express';
import { Pool } from 'pg';
import type { RedisClientType } from 'redis';
import { SentryAgentError } from '../utils/errors.js';
import { billingLimitRejectionsTotal } from '../metrics/registry.js';
import { UsageService } from '../services/UsageService.js';

/** Free tier: maximum number of active (non-decommissioned) agents per tenant. */
const FREE_TIER_MAX_AGENTS = 10;

/** Free tier: maximum number of API calls per day per tenant. */
const FREE_TIER_MAX_API_CALLS = 1_000;

/** Redis cache TTL for billing usage checks, in seconds. */
const CACHE_TTL_SECONDS = 60;

/**
 * Thrown when a free-tier tenant exceeds the agent creation limit.
 * HTTP 429 with code FREE_TIER_AGENT_LIMIT.
 */
class FreeTierAgentLimitError extends SentryAgentError {
  constructor(current: number) {
    super(
      `Free tier limit of ${FREE_TIER_MAX_AGENTS} active agents reached. Upgrade to create more agents.`,
      'FREE_TIER_AGENT_LIMIT',
      429,
      { limit: FREE_TIER_MAX_AGENTS, current },
    );
  }
}

/**
 * Thrown when a free-tier tenant exceeds the daily API call limit.
 * HTTP 429 with code FREE_TIER_API_LIMIT.
 */
class FreeTierApiLimitError extends SentryAgentError {
  constructor(current: number) {
    super(
      `Free tier daily API call limit of ${FREE_TIER_MAX_API_CALLS} reached. Upgrade for unlimited calls.`,
      'FREE_TIER_API_LIMIT',
      429,
      { limit: FREE_TIER_MAX_API_CALLS, current },
    );
  }
}

/**
 * Returns true when the tenant is on the free plan.
 * A tenant is considered free when no row exists in tenant_subscriptions,
 * or the existing row has status = 'free'.
 *
 * @param pool - PostgreSQL connection pool.
 * @param tenantId - The tenant UUID.
 * @returns True if the tenant is on the free tier.
 */
async function isFreeTenant(pool: Pool, tenantId: string): Promise<boolean> {
  const result = await pool.query<{ status: string }>(
    `SELECT status FROM tenant_subscriptions WHERE tenant_id = $1 LIMIT 1`,
    [tenantId],
  );

  if (result.rows.length === 0) return true;
  return result.rows[0].status === 'free';
}

/**
 * Returns the cached API call count for a tenant on a given date.
 * Returns null when the cache is cold.
 *
 * @param redis - Redis client.
 * @param tenantId - The tenant UUID.
 * @param date - Date string in YYYY-MM-DD format.
 * @returns Cached count or null.
 */
async function getCachedApiCalls(
  redis: RedisClientType,
  tenantId: string,
  date: string,
): Promise<number | null> {
  const cacheKey = `billing:usage:${tenantId}:${date}`;
  const cached = await redis.get(cacheKey);
  if (cached === null) return null;
  return parseInt(cached, 10);
}

/**
 * Writes the API call count to the Redis cache with a TTL.
 *
 * @param redis - Redis client.
 * @param tenantId - The tenant UUID.
 * @param date - Date string in YYYY-MM-DD format.
 * @param count - The API call count to cache.
 */
async function setCachedApiCalls(
  redis: RedisClientType,
  tenantId: string,
  date: string,
  count: number,
): Promise<void> {
  const cacheKey = `billing:usage:${tenantId}:${date}`;
  await redis.set(cacheKey, String(count), { EX: CACHE_TTL_SECONDS });
}

/**
 * Creates the free tier enforcement middleware.
 *
 * Behaviour:
 * - When BILLING_ENABLED env var is 'false' (string), calls next() immediately.
 * - On agent creation (POST on an agents path): checks active agent count.
 *   If >= FREE_TIER_MAX_AGENTS and tenant is free → rejects with HTTP 429 FREE_TIER_AGENT_LIMIT.
 * - On every authenticated request: checks daily API call count.
 *   If >= FREE_TIER_MAX_API_CALLS and tenant is free → rejects with HTTP 429 FREE_TIER_API_LIMIT.
 * - Uses Redis to cache usage lookups (TTL: 60s) to minimise DB queries.
 *
 * @param pool - PostgreSQL connection pool.
 * @param redis - Redis client.
 * @returns Express RequestHandler.
 */
export function createFreeTierEnforcementMiddleware(
  pool: Pool,
  redis: RedisClientType,
): RequestHandler {
  const usageService = new UsageService(pool);

  return (req: Request, _res: Response, next: NextFunction): void => {
    // Skip if billing is disabled
    if (process.env['BILLING_ENABLED'] === 'false') {
      next();
      return;
    }

    // Only enforce for authenticated requests
    if (!req.user?.organization_id) {
      next();
      return;
    }

    const tenantId = req.user.organization_id;
    const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD

    void (async (): Promise<void> => {
      try {
        const free = await isFreeTenant(pool, tenantId);

        if (!free) {
          next();
          return;
        }

        // ── API call limit check ────────────────────────────────────────────
        let apiCalls = await getCachedApiCalls(redis, tenantId, today);

        if (apiCalls === null) {
          const summary = await usageService.getDailyUsage(tenantId, today);
          apiCalls = summary.apiCalls;
          await setCachedApiCalls(redis, tenantId, today, apiCalls);
        }

        if (apiCalls >= FREE_TIER_MAX_API_CALLS) {
          billingLimitRejectionsTotal.inc({ tenant_id: tenantId, limit_type: 'api_limit' });
          throw new FreeTierApiLimitError(apiCalls);
        }

        // ── Agent creation limit check ──────────────────────────────────────
        const isAgentCreation =
          req.method === 'POST' && /\/agents(\/?)$/.test(req.path);

        if (isAgentCreation) {
          const agentCount = await usageService.getActiveAgentCount(tenantId);

          if (agentCount >= FREE_TIER_MAX_AGENTS) {
            billingLimitRejectionsTotal.inc({ tenant_id: tenantId, limit_type: 'agent_limit' });
            throw new FreeTierAgentLimitError(agentCount);
          }
        }

        next();
      } catch (err) {
        next(err);
      }
    })();
  };
}
