<?php
declare(strict_types=1);

require_once __DIR__ . '/PricingCatalog.php';

/**
 * Credit + tier system for SSO users of tools.dobetternorge.no.
 *
 * balance = monthly credits, reset/refilled by tier.
 * bonus_balance = prepaid/top-up credits, never expires.
 * Spend order: monthly first, prepaid second.
 */
final class FreeTier
{
    public static function cost(string $tool): int
    {
        return PricingCatalog::toolCost($tool);
    }

    public static function monthlyAllowance(string $tier): int
    {
        return PricingCatalog::monthlyAllowance($tier);
    }

    public static function hourlyCap(string $tier): int
    {
        return PricingCatalog::hourlyCap($tier);
    }

    public static function storageQuota(string $tier): int
    {
        return PricingCatalog::storageQuota($tier);
    }

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

    public static function isPaidTier(string $tier): bool
    {
        return PricingCatalog::isPaidTier($tier);
    }

    public static function isTrialActive(int $userId): bool
    {
        $row = self::row($userId);
        if (!$row || ($row['tier'] ?? '') !== 'plus') {
            return false;
        }
        $expires = $row['trial_expires_at'] ?? null;
        return $expires ? strtotime((string)$expires) > time() : false;
    }

    public static function trialDaysRemaining(int $userId): int
    {
        if (!self::isTrialActive($userId)) {
            return 0;
        }
        $row = self::row($userId);
        $expires = strtotime((string)$row['trial_expires_at']);
        return max(0, (int)ceil(($expires - time()) / 86400));
    }

    public static function row(int $userId): ?array
    {
        $db = dbnmDb();
        $freeAllowance = self::monthlyAllowance('free');
        $plusAllowance = self::monthlyAllowance('plus');
        $proAllowance = self::monthlyAllowance('pro');

        $db->prepare(
            "UPDATE user_tool_credits
             SET balance = CASE tier
                   WHEN 'free' THEN {$freeAllowance}
                   WHEN 'plus' THEN {$plusAllowance}
                   WHEN 'pro'  THEN {$proAllowance}
                   ELSE balance END,
                 allowance = CASE tier
                   WHEN 'free' THEN {$freeAllowance}
                   WHEN 'plus' THEN {$plusAllowance}
                   WHEN 'pro'  THEN {$proAllowance}
                   ELSE allowance 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;
    }

    public static function check(int $userId, string $tool): array
    {
        return self::checkAmount($userId, $tool, self::cost($tool));
    }

    public static function checkAmount(int $userId, string $tool, int $credits): array
    {
        $db = dbnmDb();
        $row = self::row($userId);
        $credits = max(0, $credits);

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

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

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

        $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'];
        }

        if ($credits === 0) {
            return $base + ['ok' => true];
        }

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

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

    public static function deduct(int $userId, string $tool): int
    {
        return self::deductAmount($userId, $tool, self::cost($tool));
    }

    public static function deductAmount(int $userId, string $tool, int $credits, array $metadata = []): int
    {
        $db = dbnmDb();
        $credits = max(0, $credits);
        $row = self::row($userId);
        $beforeMonthly = $row ? (int)$row['balance'] : 0;
        $beforePrepaid = $row ? (int)$row['bonus_balance'] : 0;
        $fromMonthly = 0;
        $fromPrepaid = 0;

        if ($credits > 0 && $row !== null) {
            $fromMonthly = min($credits, $beforeMonthly);
            $fromPrepaid = $credits - $fromMonthly;

            $stmt = $db->prepare(
                'UPDATE user_tool_credits
                 SET balance = GREATEST(0, balance - ?),
                     bonus_balance = GREATEST(0, bonus_balance - ?)
                 WHERE user_id = ?
                   AND (balance + bonus_balance) >= ?'
            );
            $stmt->execute([$fromMonthly, $fromPrepaid, $userId, $credits]);

            if ($stmt->rowCount() < 1) {
                $check = self::checkAmount($userId, $tool, $credits);
                if (empty($check['ok'])) {
                    throw new RuntimeException('Insufficient credits while settling tool charge.');
                }
            }
        }

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

        $latest = self::row($userId);
        $afterMonthly = $latest ? (int)$latest['balance'] : 0;
        $afterPrepaid = $latest ? (int)$latest['bonus_balance'] : 0;
        self::recordLedger($userId, 'tool_charge', $tool, -$credits, [
            'from_monthly' => $fromMonthly,
            'from_prepaid' => $fromPrepaid,
            'before_monthly' => $beforeMonthly,
            'before_prepaid' => $beforePrepaid,
            'after_monthly' => $afterMonthly,
            'after_prepaid' => $afterPrepaid,
        ] + $metadata);

        return $afterMonthly + $afterPrepaid;
    }

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

    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,
            'trial_started_at' => $row['trial_started_at'] ?? null,
            'trial_expires_at' => $row['trial_expires_at'] ?? null,
            'trial_active' => self::isTrialActive($userId),
            'trial_days_remaining' => self::trialDaysRemaining($userId),
        ];
    }

    public static function awardBonus(int $userId, int $credits, string $source): int
    {
        if ($credits <= 0) {
            return self::balance($userId);
        }
        $db = dbnmDb();
        self::ensureRow($userId);
        $row = self::row($userId);
        $beforeMonthly = $row ? (int)$row['balance'] : 0;
        $beforePrepaid = $row ? (int)$row['bonus_balance'] : 0;

        $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]);

        $latest = self::row($userId);
        $afterMonthly = $latest ? (int)$latest['balance'] : 0;
        $afterPrepaid = $latest ? (int)$latest['bonus_balance'] : 0;
        self::recordLedger($userId, 'credit_grant', 'bonus:' . substr($source, 0, 40), $credits, [
            'source' => $source,
            'before_monthly' => $beforeMonthly,
            'before_prepaid' => $beforePrepaid,
            'after_monthly' => $afterMonthly,
            'after_prepaid' => $afterPrepaid,
        ]);

        return $afterMonthly + $afterPrepaid;
    }

    public static function setTier(
        int $userId,
        string $tier,
        ?string $stripeCustomerId,
        ?string $subscriptionId,
        ?string $periodEndIso,
        ?string $trialEndIso = null
    ): void {
        $db = dbnmDb();
        self::ensureRow($userId);
        $allowance = self::monthlyAllowance($tier);

        if ($trialEndIso !== null) {
            $db->prepare(
                'UPDATE user_tool_credits
                 SET tier = ?, balance = ?, allowance = ?,
                     stripe_customer_id = COALESCE(?, stripe_customer_id),
                     subscription_id = ?,
                     subscription_period_end = ?,
                     trial_started_at = COALESCE(trial_started_at, NOW()),
                     trial_expires_at = ?,
                     trial_downgraded_at = NULL,
                     last_reset = CURDATE()
                 WHERE user_id = ?'
            )->execute([$tier, $allowance, $allowance, $stripeCustomerId, $subscriptionId, $periodEndIso, $trialEndIso, $userId]);
        } else {
            $db->prepare(
                'UPDATE user_tool_credits
                 SET tier = ?, balance = ?, allowance = ?,
                     stripe_customer_id = COALESCE(?, stripe_customer_id),
                     subscription_id = ?,
                     subscription_period_end = ?,
                     trial_expires_at = NULL,
                     trial_downgraded_at = NULL,
                     last_reset = CURDATE()
                 WHERE user_id = ?'
            )->execute([$tier, $allowance, $allowance, $stripeCustomerId, $subscriptionId, $periodEndIso, $userId]);
        }

        self::recordLedger($userId, 'subscription_refill', 'subscription:' . $tier, $allowance, [
            'tier' => $tier,
            'subscription_id' => $subscriptionId,
            'stripe_customer_id' => $stripeCustomerId,
            'period_end' => $periodEndIso,
            'trial_end' => $trialEndIso,
        ]);
    }

    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 = ?,
                 allowance = ?,
                 subscription_period_end = ?,
                 last_reset = CURDATE()
             WHERE user_id = ?'
        )->execute([$allowance, $allowance, $periodEndIso, $userId]);

        self::recordLedger($userId, 'subscription_refill', 'invoice:' . $tier, $allowance, [
            'tier' => $tier,
            'period_end' => $periodEndIso,
        ]);
    }

    public static function clearTier(int $userId): void
    {
        $db = dbnmDb();
        $allowance = self::monthlyAllowance('free');
        $db->prepare(
            "UPDATE user_tool_credits
             SET tier = 'free',
                 allowance = ?,
                 balance = ?,
                 subscription_id = NULL,
                 subscription_period_end = NULL,
                 trial_downgraded_at = CASE
                     WHEN trial_expires_at IS NOT NULL AND trial_downgraded_at IS NULL
                     THEN NOW()
                     ELSE trial_downgraded_at
                 END,
                 trial_expires_at = NULL
             WHERE user_id = ?"
        )->execute([$allowance, $allowance, $userId]);

        self::recordLedger($userId, 'tier_change', 'subscription:clear', 0, ['tier' => 'free']);
    }

    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']);
    }

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

    public static function transcribeCreditsForSeconds(float $seconds): int
    {
        return PricingCatalog::transcribeCreditsForSeconds($seconds);
    }

    public static function estimateTranscribeCreditsFromBytes(int $bytes): int
    {
        return PricingCatalog::estimateTranscribeCreditsFromBytes($bytes);
    }

    public static function estimateTranscribeCreditsFromFile(string $filename, int $bytes): int
    {
        return PricingCatalog::estimateTranscribeCreditsFromFile($filename, $bytes);
    }

    public static function createReservation(int $userId, string $tool, int $credits, array $metadata = []): int
    {
        try {
            $db = dbnmDb();
            $db->prepare(
                'INSERT INTO user_tool_credit_reservations
                   (user_id, tool, reserved_credits, status, metadata_json, created_at, expires_at)
                 VALUES (?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 2 HOUR))'
            )->execute([
                $userId,
                substr($tool, 0, 40),
                max(0, $credits),
                'reserved',
                json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            ]);
            return (int)$db->lastInsertId();
        } catch (Throwable $e) {
            return 0;
        }
    }

    public static function settleReservation(int $reservationId, int $credits, string $provider, float $durationSeconds, array $metadata = []): void
    {
        if ($reservationId <= 0) {
            return;
        }
        try {
            $db = dbnmDb();
            $db->prepare(
                "UPDATE user_tool_credit_reservations
                 SET status = 'settled',
                     settled_credits = ?,
                     provider = ?,
                     duration_seconds = ?,
                     metadata_json = ?,
                     settled_at = NOW()
                 WHERE id = ? AND status = 'reserved'"
            )->execute([
                max(0, $credits),
                substr($provider, 0, 40),
                round($durationSeconds, 2),
                json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                $reservationId,
            ]);
        } catch (Throwable $e) {
            // Non-fatal audit path.
        }
    }

    public static function releaseReservation(int $reservationId, array $metadata = []): void
    {
        if ($reservationId <= 0) {
            return;
        }
        try {
            $db = dbnmDb();
            $db->prepare(
                "UPDATE user_tool_credit_reservations
                 SET status = 'released',
                     metadata_json = ?
                 WHERE id = ? AND status = 'reserved'"
            )->execute([
                json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                $reservationId,
            ]);
        } catch (Throwable $e) {
            // Non-fatal audit path.
        }
    }

    private static function recordLedger(int $userId, string $eventType, string $source, int $creditsDelta, array $metadata = []): void
    {
        try {
            $db = dbnmDb();
            $db->prepare(
                'INSERT INTO user_tool_credit_ledger
                   (user_id, event_type, source, credits_delta, metadata_json, created_at)
                 VALUES (?, ?, ?, ?, ?, NOW())'
            )->execute([
                $userId,
                substr($eventType, 0, 40),
                substr($source, 0, 100),
                $creditsDelta,
                json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            ]);
        } catch (Throwable $e) {
            // Ledger is additive and should not block core credit behavior before migration is applied.
        }
    }
}
