<?php

namespace App\Services\Billing;

use App\Models\CreditBalanceModel;
use App\Models\CreditCostModel;
use App\Models\CreditTransactionModel;
use App\Services\Support\ActivityLogger;
use Config\Database;

/**
 * See 05-saas-architecture.md §3: balance is never trusted from a cached
 * column in the debit path — every debit locks the row (SELECT ... FOR
 * UPDATE) inside a transaction, recomputes, and only then writes, so two
 * concurrent requests can't both succeed past a balance that only covers one.
 */
class CreditService
{
    private CreditBalanceModel $balances;
    private CreditCostModel $costs;
    private CreditTransactionModel $transactions;
    private ActivityLogger $activity;

    public function __construct()
    {
        $this->balances     = new CreditBalanceModel();
        $this->costs        = new CreditCostModel();
        $this->transactions = new CreditTransactionModel();
        $this->activity     = new ActivityLogger();
    }

    public function costFor(string $action): int
    {
        return $this->costs->costFor($action);
    }

    public function balanceFor(int $agencyId): int
    {
        $row = $this->balances->ensureExists($agencyId);
        return (int) $row['balance'];
    }

    /**
     * Atomic debit. Throws InsufficientCreditsException if the locked
     * balance can't cover the cost — caller (e.g. CandidateSearchService::unlock)
     * should catch this and surface a "buy more credits" prompt.
     */
    public function debit(int $agencyId, int $amount, string $referenceType, ?int $referenceId, ?int $userId = null): int
    {
        $db = Database::connect();
        $db->transStart();

        $this->balances->ensureExists($agencyId); // no-op if already present, outside the lock

        // Raw query specifically for FOR UPDATE — CI4's Query Builder
        // doesn't expose row locking uniformly across drivers.
        $locked = $db->query('SELECT balance FROM credit_balances WHERE agency_id = ? FOR UPDATE', [$agencyId])->getRow();
        $currentBalance = (int) $locked->balance;

        if ($currentBalance < $amount) {
            $db->transComplete();
            throw new \App\Exceptions\InsufficientCreditsException(
                "Insufficient credits: balance {$currentBalance}, required {$amount}."
            );
        }

        $newBalance = $currentBalance - $amount;

        $db->table('credit_balances')->where('agency_id', $agencyId)->update([
            'balance'      => $newBalance,
            'last_updated' => date('Y-m-d H:i:s'),
        ]);

        $txId = $this->transactions->insert([
            'agency_id'      => $agencyId,
            'user_id'        => $userId,
            'type'           => 'usage',
            'amount'         => -$amount,
            'balance_after'  => $newBalance,
            'reference_type' => $referenceType,
            'reference_id'   => $referenceId,
            'created_at'     => date('Y-m-d H:i:s'),
        ], true);

        $db->transComplete();

        $this->activity->log($userId, $agencyId, null, 'credit_usage', ['amount' => $amount, 'reference_type' => $referenceType]);

        return $txId;
    }

    public function credit(int $agencyId, int $amount, string $type, ?string $referenceType = null, ?int $referenceId = null, ?string $expiresAt = null): int
    {
        if (! in_array($type, ['purchase', 'refund', 'bonus'], true)) {
            throw new \InvalidArgumentException('Invalid credit type.');
        }

        $db = Database::connect();
        $db->transStart();

        $this->balances->ensureExists($agencyId);
        $locked = $db->query('SELECT balance FROM credit_balances WHERE agency_id = ? FOR UPDATE', [$agencyId])->getRow();
        $newBalance = (int) $locked->balance + $amount;

        $db->table('credit_balances')->where('agency_id', $agencyId)->update([
            'balance'      => $newBalance,
            'last_updated' => date('Y-m-d H:i:s'),
        ]);

        $txId = $this->transactions->insert([
            'agency_id'      => $agencyId,
            'type'           => $type,
            'amount'         => $amount,
            'balance_after'  => $newBalance,
            'reference_type' => $referenceType,
            'reference_id'   => $referenceId,
            'expires_at'     => $expiresAt,
            'created_at'     => date('Y-m-d H:i:s'),
        ], true);

        $db->transComplete();

        return $txId;
    }

    /**
     * Cron entry point — expires bonus credits past their expires_at,
     * debiting the balance and logging a distinct 'expiry' transaction
     * (kept separate from debit() so the ledger clearly shows expiry vs
     * usage, per 05-saas-architecture.md §3).
     */
    public function runExpiryCheck(): int
    {
        $expired = $this->transactions->expiringUnexpired();
        $count = 0;

        foreach ($expired as $tx) {
            $currentBalance = $this->balanceFor((int) $tx['agency_id']);
            $toExpire = min((int) $tx['amount'], $currentBalance);

            if ($toExpire > 0) {
                $this->expire((int) $tx['agency_id'], $toExpire, (int) $tx['id']);
                $count++;
            }
        }

        return $count;
    }

    private function expire(int $agencyId, int $amount, int $originalTransactionId): void
    {
        $db = Database::connect();
        $db->transStart();

        $locked = $db->query('SELECT balance FROM credit_balances WHERE agency_id = ? FOR UPDATE', [$agencyId])->getRow();
        $newBalance = max(0, (int) $locked->balance - $amount);

        $db->table('credit_balances')->where('agency_id', $agencyId)->update([
            'balance'      => $newBalance,
            'last_updated' => date('Y-m-d H:i:s'),
        ]);

        $this->transactions->insert([
            'agency_id'      => $agencyId,
            'type'           => 'expiry',
            'amount'         => -$amount,
            'balance_after'  => $newBalance,
            'reference_type' => 'credit_transactions',
            'reference_id'   => $originalTransactionId,
            'created_at'     => date('Y-m-d H:i:s'),
        ]);

        $db->transComplete();
    }
}
