/**
 * Tier Service for SentryAgent.ai AgentIdP.
 * Single authority for all tier-related business logic:
 * - Fetching current tier and usage status
 * - Initiating Stripe checkout for tier upgrades
 * - Applying a confirmed tier upgrade to the organizations table
 */

import { Pool } from 'pg';
import Stripe from 'stripe';
import type { RedisClientType } from 'redis';
import { TIER_CONFIG, TierName, TIER_RANK, isTierName } from '../config/tiers.js';
import { ValidationError, TierLimitError } from '../utils/errors.js';

/** Redis key prefixes for daily counters. */
const CALLS_KEY_PREFIX = 'rate:tier:calls:';
const TOKENS_KEY_PREFIX = 'rate:tier:tokens:';

/**
 * Current tier status snapshot returned by getStatus().
 */
export interface ITierStatus {
  /** The tenant's current tier name. */
  tier: TierName;
  /** Per-tier limits from TIER_CONFIG. */
  limits: {
    maxAgents: number;
    maxCallsPerDay: number;
    maxTokensPerDay: number;
  };
  /** Live usage counters for the current UTC day. */
  usage: {
    /** Number of API calls made today (from Redis). */
    callsToday: number;
    /** Number of tokens issued today (from Redis). */
    tokensToday: number;
    /** Number of non-decommissioned agents for this org. */
    agentCount: number;
  };
  /** ISO 8601 timestamp of the next UTC midnight (daily limit reset time). */
  resetAt: string;
}

/**
 * Result of initiateUpgrade() — contains the Stripe Checkout URL.
 */
export interface IUpgradeInitiation {
  /** URL the tenant should be redirected to for payment. */
  checkoutUrl: string;
}

/** DB row shape for organization tier queries. */
interface IOrgTierRow {
  tier: string;
}

/**
 * Service for tenant tier management.
 * Owns all tier logic — controllers and middleware delegate to this service.
 */
export class TierService {
  /**
   * @param pool - PostgreSQL connection pool.
   * @param redis - Redis client for usage counter access.
   * @param stripe - Configured Stripe client instance.
   */
  constructor(
    private readonly pool: Pool,
    private readonly redis: RedisClientType,
    private readonly stripe: Stripe,
  ) {}

  // ─────────────────────────────────────────────────────────────────────────
  // Public API
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * Returns the current tier, limits, usage, and daily reset time for an org.
   *
   * Usage counters are read directly from Redis (live counts).
   * Agent count is read from the database.
   * Falls back gracefully if Redis is unavailable (returns 0 for Redis-backed counters).
   *
   * @param orgId - The organization UUID.
   * @returns ITierStatus snapshot.
   */
  async getStatus(orgId: string): Promise<ITierStatus> {
    const tier = await this.fetchTier(orgId);
    const limits = TIER_CONFIG[tier];

    const [callsToday, tokensToday, agentCount] = await Promise.all([
      this.readRedisCounter(CALLS_KEY_PREFIX + orgId),
      this.readRedisCounter(TOKENS_KEY_PREFIX + orgId),
      this.fetchAgentCount(orgId),
    ]);

    const resetAt = this.nextUtcMidnight().toISOString();

    return {
      tier,
      limits: {
        maxAgents: limits.maxAgents,
        maxCallsPerDay: limits.maxCallsPerDay,
        maxTokensPerDay: limits.maxTokensPerDay,
      },
      usage: { callsToday, tokensToday, agentCount },
      resetAt,
    };
  }

  /**
   * Validates that the target tier is a valid upgrade and creates a Stripe Checkout
   * Session for the new tier's price. Returns the checkout URL.
   *
   * Metadata on the session includes `{ orgId, targetTier }` so the webhook handler
   * can apply the upgrade after payment succeeds.
   *
   * @param orgId - The organization UUID.
   * @param targetTier - The desired new tier.
   * @returns IUpgradeInitiation with the Stripe checkout URL.
   * @throws ValidationError if targetTier is not higher than the current tier.
   * @throws Error if Stripe does not return a session URL.
   */
  async initiateUpgrade(orgId: string, targetTier: TierName): Promise<IUpgradeInitiation> {
    const currentTier = await this.fetchTier(orgId);

    if (TIER_RANK[targetTier] <= TIER_RANK[currentTier]) {
      throw new ValidationError(
        `Cannot downgrade or remain on the same tier. Current tier: ${currentTier}. Downgrades require contacting support.`,
        { currentTier, targetTier },
      );
    }

    // Resolve the Stripe price ID for the target tier.
    // Each tier maps to a dedicated price ID env var: STRIPE_PRICE_ID_PRO, STRIPE_PRICE_ID_ENTERPRISE.
    const priceIdEnvKey = `STRIPE_PRICE_ID_${targetTier.toUpperCase()}`;
    const priceId = process.env[priceIdEnvKey] ?? process.env['STRIPE_PRICE_ID'];

    const session = await this.stripe.checkout.sessions.create({
      mode: 'subscription',
      client_reference_id: orgId,
      metadata: { orgId, targetTier },
      line_items: priceId ? [{ price: priceId, quantity: 1 }] : undefined,
      success_url:
        process.env['STRIPE_SUCCESS_URL'] ??
        `${process.env['APP_BASE_URL'] ?? 'http://localhost:3000'}/dashboard?billing=success`,
      cancel_url:
        process.env['STRIPE_CANCEL_URL'] ??
        `${process.env['APP_BASE_URL'] ?? 'http://localhost:3000'}/dashboard?billing=cancel`,
    });

    if (!session.url) {
      throw new Error('Stripe did not return a checkout session URL.');
    }

    return { checkoutUrl: session.url };
  }

  /**
   * Applies a confirmed tier upgrade to the organizations table.
   * Sets both `tier` and `tier_updated_at`.
   * Called by the Stripe webhook handler after `checkout.session.completed`.
   *
   * @param orgId - The organization UUID.
   * @param tier - The new tier to apply.
   * @returns Promise that resolves when the update is persisted.
   */
  async applyUpgrade(orgId: string, tier: TierName): Promise<void> {
    await this.pool.query(
      `UPDATE organizations
       SET tier = $1, tier_updated_at = NOW()
       WHERE organization_id = $2`,
      [tier, orgId],
    );
  }

  /**
   * Fetches the current tier for an org from the database.
   * Returns 'free' as the safe default when no row is found.
   *
   * @param orgId - The organization UUID.
   * @returns The current TierName.
   */
  async fetchTier(orgId: string): Promise<TierName> {
    const result = await this.pool.query<IOrgTierRow>(
      `SELECT tier FROM organizations WHERE organization_id = $1 LIMIT 1`,
      [orgId],
    );
    if (result.rows.length === 0) return 'free';
    const raw = result.rows[0].tier;
    return isTierName(raw) ? raw : 'free';
  }

  // ─────────────────────────────────────────────────────────────────────────
  // Internal helpers
  // ─────────────────────────────────────────────────────────────────────────

  /**
   * Reads an integer counter from Redis. Returns 0 on error or missing key.
   *
   * @param key - The full Redis key.
   * @returns The counter value, or 0 when unavailable.
   */
  private async readRedisCounter(key: string): Promise<number> {
    try {
      const value = await this.redis.get(key);
      if (value === null) return 0;
      const parsed = parseInt(value, 10);
      return Number.isNaN(parsed) ? 0 : parsed;
    } catch {
      return 0;
    }
  }

  /**
   * Counts non-decommissioned agents for an org.
   *
   * @param orgId - The organization UUID.
   * @returns Agent count.
   */
  private async fetchAgentCount(orgId: string): Promise<number> {
    const result = await this.pool.query<{ count: string }>(
      `SELECT COUNT(*) AS count
       FROM agents
       WHERE organization_id = $1 AND status != 'decommissioned'`,
      [orgId],
    );
    return parseInt(result.rows[0]?.count ?? '0', 10);
  }

  /**
   * Returns a Date object representing the next UTC midnight.
   *
   * @returns Date set to 00:00:00.000 UTC of the next calendar day.
   */
  private nextUtcMidnight(): Date {
    const d = new Date();
    d.setUTCHours(24, 0, 0, 0);
    return d;
  }

  /**
   * Enforces the per-org agent count limit for the given tier.
   * Throws TierLimitError when the current count is at or over the allowed maximum.
   * Used by AgentService before creating a new agent.
   *
   * @param orgId - The organization UUID.
   * @param tier - The organization's current tier.
   * @throws TierLimitError when the limit is reached.
   */
  async enforceAgentLimit(orgId: string, tier: TierName): Promise<void> {
    const limit = TIER_CONFIG[tier].maxAgents;
    // Infinity means enterprise — no enforcement
    if (!isFinite(limit)) return;

    const count = await this.fetchAgentCount(orgId);
    if (count >= limit) {
      throw new TierLimitError('agent', limit, { orgId, tier, current: count });
    }
  }
}
