<?php
declare(strict_types=1);

/**
 * Credit + tier system for users of tools.dobetternorge.no.
 *
 * Tables:
 *   user_tool_credits — balance (monthly, resets), bonus_balance (never expires), tier, Stripe links
 *   user_tool_usage_log — every tool call with credits_used
 *   user_subscriptions — Stripe subscription ledger
 *
 * Effective balance = balance + bonus_balance.
 * Spend order: deduct from balance first, overflow to bonus_balance.
 * pro_plus tier bypasses balance checks (still subject to hourly cap).
 *
 * CaveauAI client sessions (dbn_tools_user_id + client_id) bypass all checks.
 * Only SSO sessions are subject to limits.
 */
final class FreeTier
{
    private const COSTS = [
        'corpus-search' => 0,
        'search'        => 0,
        'ask'           => 1,
        'extract'       => 1,
        'timeline'      => 2,
        'redact'        => 2,
        'barnevernet'   => 3,
        'advocate'      => 3,
        'deep-research' => 5,
        'transcribe'    => 2,
        'discrepancy'   => 4,
        'korrespond'    => 3,
    ];

    /** Monthly credit allowance per tier. pro_plus is "effectively unlimited" but hourly-capped. */
    private const MONTHLY_ALLOWANCE = [
        'free'     => 30,
        'light'    => 120,
        'pro'      => 500,
        'pro_plus' => 999999,
    ];

    /** Hourly rate-limit per tier (number of paid tool calls per rolling hour). */
    private const HOURLY_CAP = [
        'free'     => 10,
        'light'    => 15,
        'pro'      => 30,
        'pro_plus' => 50,
    ];

    /** Per-user case-storage quota in bytes. */
    private const STORAGE_QUOTA = [
        'free'     => 0,
        'light'    =>      104857600, // 100 MB
        'pro'      =>     1073741824, // 1 GB
        'pro_plus' =>    10737418240, // 10 GB
    ];

    /** Credit cost for a given tool slug. Returns 1 for unknown tools. */
    public static function cost(string $tool): int
    {
        return self::COSTS[$tool] ?? 1;
    }

    public static function monthlyAllowance(string $tier): int
    {
        return self::MONTHLY_ALLOWANCE[$tier] ?? self::MONTHLY_ALLOWANCE['free'];
    }

    public static function hourlyCap(string $tier): int
    {
        return self::HOURLY_CAP[$tier] ?? self::HOURLY_CAP['free'];
    }

    public static function storageQuota(string $tier): int
    {
        return self::STORAGE_QUOTA[$tier] ?? 0;
    }

    /** Fetch a user's tier (defaults to 'free' if no row). */
    public static function tier(int $userId): string
    {
        $row = self::row($userId);
        return $row['tier'] ?? 'free';
    }

    /** Fetch the full credits row, applying lazy monthly reset. */
    public static function row(int $userId): ?array
    {
        $db = dbnmDb();
        // Auto-refill monthly balance based on tier-specific allowance, only if a new calendar month has begun.
        $db->prepare(
            "UPDATE user_tool_credits
             SET balance = CASE tier
                   WHEN 'free'     THEN " . self::MONTHLY_ALLOWANCE['free'] . "
                   WHEN 'light'    THEN " . self::MONTHLY_ALLOWANCE['light'] . "
                   WHEN 'pro'      THEN " . self::MONTHLY_ALLOWANCE['pro'] . "
                   WHEN 'pro_plus' THEN " . self::MONTHLY_ALLOWANCE['pro_plus'] . "
                   ELSE balance END,
                 last_reset = CURDATE()
             WHERE user_id = ?
               AND (YEAR(last_reset) < YEAR(CURDATE()) OR MONTH(last_reset) < MONTH(CURDATE()))"
        )->execute([$userId]);

        $stmt = $db->prepare('SELECT * FROM user_tool_credits WHERE user_id = ? LIMIT 1');
        $stmt->execute([$userId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $row : null;
    }

    /**
     * Check whether the user may proceed with a tool call.
     *
     * Returns:
     *   ['ok' => true,  'balance' => int, 'bonus_balance' => int, 'tier' => string]
     *   ['ok' => false, 'balance' => int, 'bonus_balance' => int, 'tier' => string,
     *    'reason' => 'no_credits'|'rate_limit']
     */
    public static function check(int $userId, string $tool): array
    {
        $db = dbnmDb();
        $cost = self::cost($tool);
        $row = self::row($userId);

        if ($row === null) {
            return [
                'ok' => false, 'balance' => 0, 'bonus_balance' => 0,
                'tier' => 'free', 'reason' => 'no_credits',
            ];
        }

        $balance = (int)$row['balance'];
        $bonus   = (int)$row['bonus_balance'];
        $tier    = (string)$row['tier'];

        $base = [
            'balance' => $balance,
            'bonus_balance' => $bonus,
            'tier' => $tier,
        ];

        // Hourly rate limit (always applies, even to pro_plus)
        $stmt = $db->prepare(
            'SELECT COUNT(*) FROM user_tool_usage_log
             WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR) AND credits_used > 0'
        );
        $stmt->execute([$userId]);
        $hourly = (int)$stmt->fetchColumn();
        if ($hourly >= self::hourlyCap($tier)) {
            return $base + ['ok' => false, 'reason' => 'rate_limit'];
        }

        // Free tool (cost=0) always passes credit check
        if ($cost === 0) {
            return $base + ['ok' => true];
        }

        // pro_plus bypasses credit check
        if ($tier === 'pro_plus') {
            return $base + ['ok' => true];
        }

        if (($balance + $bonus) < $cost) {
            return $base + ['ok' => false, 'reason' => 'no_credits'];
        }

        return $base + ['ok' => true];
    }

    /**
     * Deduct credits for a completed tool call and log the usage.
     * Spends from `balance` first, then `bonus_balance`.
     * pro_plus tier logs the call but does not deduct.
     *
     * Returns the new effective balance (balance + bonus_balance).
     */
    public static function deduct(int $userId, string $tool): int
    {
        $db   = dbnmDb();
        $cost = self::cost($tool);
        $row  = self::row($userId);
        $tier = $row['tier'] ?? 'free';

        if ($cost > 0 && $tier !== 'pro_plus' && $row !== null) {
            $balance = (int)$row['balance'];
            $bonus   = (int)$row['bonus_balance'];

            $fromBalance = min($cost, $balance);
            $fromBonus   = $cost - $fromBalance;

            $db->prepare(
                'UPDATE user_tool_credits
                 SET balance = GREATEST(0, balance - ?),
                     bonus_balance = GREATEST(0, bonus_balance - ?)
                 WHERE user_id = ?'
            )->execute([$fromBalance, $fromBonus, $userId]);
        }

        $db->prepare(
            'INSERT INTO user_tool_usage_log (user_id, tool, credits_used) VALUES (?, ?, ?)'
        )->execute([$userId, $tool, $cost]);

        $latest = self::row($userId);
        return $latest ? ((int)$latest['balance'] + (int)$latest['bonus_balance']) : 0;
    }

    /** Effective balance (monthly + bonus). */
    public static function balance(int $userId): int
    {
        $row = self::row($userId);
        return $row ? ((int)$row['balance'] + (int)$row['bonus_balance']) : 0;
    }

    /** Detailed balance breakdown for UI rendering. */
    public static function balanceDetail(int $userId): array
    {
        $row = self::row($userId);
        if (!$row) {
            return ['balance' => 0, 'bonus_balance' => 0, 'tier' => 'free'];
        }
        return [
            'balance' => (int)$row['balance'],
            'bonus_balance' => (int)$row['bonus_balance'],
            'tier' => (string)$row['tier'],
            'storage_used_bytes' => (int)($row['storage_used_bytes'] ?? 0),
            'storage_quota_bytes' => self::storageQuota((string)$row['tier']),
            'survey_completed_at' => $row['survey_completed_at'] ?? null,
            'subscription_period_end' => $row['subscription_period_end'] ?? null,
        ];
    }

    /**
     * Award one-time bonus credits (survey reward, Stripe topup, manual grant).
     * Source is logged via user_tool_usage_log with a negative credits_used value.
     */
    public static function awardBonus(int $userId, int $credits, string $source): int
    {
        if ($credits <= 0) {
            return self::balance($userId);
        }
        $db = dbnmDb();
        self::ensureRow($userId);
        $db->prepare('UPDATE user_tool_credits SET bonus_balance = bonus_balance + ? WHERE user_id = ?')
           ->execute([$credits, $userId]);
        $db->prepare('INSERT INTO user_tool_usage_log (user_id, tool, credits_used) VALUES (?, ?, ?)')
           ->execute([$userId, 'bonus:' . substr($source, 0, 40), -$credits]);
        return self::balance($userId);
    }

    /**
     * Set or upgrade a user's tier (called by Stripe subscription webhook).
     * Refills monthly balance to the new tier's allowance.
     */
    public static function setTier(
        int $userId,
        string $tier,
        ?string $stripeCustomerId,
        ?string $subscriptionId,
        ?string $periodEndIso
    ): void {
        $db = dbnmDb();
        self::ensureRow($userId);
        $allowance = self::monthlyAllowance($tier);
        $db->prepare(
            'UPDATE user_tool_credits
             SET tier = ?, balance = ?, allowance = ?,
                 stripe_customer_id = COALESCE(?, stripe_customer_id),
                 subscription_id = ?,
                 subscription_period_end = ?,
                 last_reset = CURDATE()
             WHERE user_id = ?'
        )->execute([$tier, $allowance, $allowance, $stripeCustomerId, $subscriptionId, $periodEndIso, $userId]);
    }

    /**
     * Refill monthly balance at subscription renewal (invoice.paid).
     * Does not touch bonus_balance.
     */
    public static function refillForRenewal(int $userId, string $tier, ?string $periodEndIso): void
    {
        $db = dbnmDb();
        $allowance = self::monthlyAllowance($tier);
        $db->prepare(
            'UPDATE user_tool_credits
             SET balance = ?,
                 subscription_period_end = ?,
                 last_reset = CURDATE()
             WHERE user_id = ?'
        )->execute([$allowance, $periodEndIso, $userId]);
    }

    /**
     * Revert a user to free tier (subscription canceled or fully ended).
     * Preserves bonus_balance and case_documents (handled by 90-day cron).
     */
    public static function clearTier(int $userId): void
    {
        $db = dbnmDb();
        $db->prepare(
            'UPDATE user_tool_credits
             SET tier = ?, allowance = ?, subscription_id = NULL, subscription_period_end = NULL
             WHERE user_id = ?'
        )->execute(['free', self::monthlyAllowance('free'), $userId]);
    }

    /** Mark survey as completed so the bonus can only be claimed once per account. */
    public static function markSurveyCompleted(int $userId): void
    {
        $db = dbnmDb();
        self::ensureRow($userId);
        $db->prepare('UPDATE user_tool_credits SET survey_completed_at = NOW() WHERE user_id = ?')
           ->execute([$userId]);
    }

    public static function hasCompletedSurvey(int $userId): bool
    {
        $row = self::row($userId);
        return !empty($row['survey_completed_at']);
    }

    /** Create the user_tool_credits row if missing (idempotent). */
    public static function ensureRow(int $userId): void
    {
        $db = dbnmDb();
        $db->prepare(
            'INSERT IGNORE INTO user_tool_credits (user_id, balance, allowance, tier, last_reset, created_at)
             VALUES (?, ?, ?, ?, CURDATE(), NOW())'
        )->execute([$userId, self::monthlyAllowance('free'), self::monthlyAllowance('free'), 'free']);
    }
}
