<?php

namespace WPSN\Modules\AI;

use WPSN\Core\Settings;

defined('ABSPATH') || exit;

/**
 * Lightweight on-site "AI credits" wallet + ledger.
 *
 * Goals:
 * - Allow agencies/site owners to set a finite budget (credits) for external AI usage.
 * - Track debits (AI calls) and manual credits top-ups in a small ledger table.
 * - Keep balance in an option for fast reads; ledger is the audit trail.
 */
final class AICreditsRepository
{
    public const OPTION_NAME = 'wpsn_ai_credits';

    private Settings $settings;

    public function __construct(Settings $settings)
    {
        $this->settings = $settings;
    }

    public function isEnabled(): bool
    {
        return (bool) $this->settings->get('ai.credits.enabled', false);
    }

    /**
     * @return 'tokens'|'usd'
     */
    public function mode(): string
    {
        $mode = sanitize_key((string) $this->settings->get('ai.credits.mode', 'tokens'));
        if (!in_array($mode, ['tokens', 'usd'], true)) {
            $mode = 'tokens';
        }
        return $mode;
    }

    /**
     * How many tokens correspond to 1 credit (tokens-mode).
     */
    public function tokensPerCredit(): int
    {
        $v = (int) $this->settings->get('ai.credits.tokens_per_credit', 1000);
        // 1 credit should not be too small to avoid huge balances.
        if ($v < 100) {
            $v = 100;
        }
        if ($v > 1000000) {
            $v = 1000000;
        }
        return $v;
    }

    /**
     * How many credits correspond to $1.00 (usd-mode).
     */
    public function creditsPerUsd(): float
    {
        $v = (float) $this->settings->get('ai.credits.credits_per_usd', 100.0);
        if ($v < 0.0) {
            $v = 0.0;
        }
        if ($v > 10000000.0) {
            $v = 10000000.0;
        }
        return $v;
    }

    /**
     * Get current credit balance (in credits).
     */
    public function getBalance(): float
    {
        $state = get_option(self::OPTION_NAME);
        if (!is_array($state)) {
            $state = [];
        }
        $bal = isset($state['balance']) ? (float) $state['balance'] : 0.0;
        if ($bal < 0.0) {
            $bal = 0.0;
        }
        return (float) $bal;
    }

    /**
     * @return array{balance:float,updated_at:string,version:int}
     */
    public function getState(): array
    {
        $state = get_option(self::OPTION_NAME);
        if (!is_array($state)) {
            $state = [];
        }

        $bal = isset($state['balance']) ? (float) $state['balance'] : 0.0;
        if ($bal < 0.0) {
            $bal = 0.0;
        }

        $updatedAt = isset($state['updated_at']) ? (string) $state['updated_at'] : '';
        if ($updatedAt === '') {
            $updatedAt = gmdate('Y-m-d H:i:s');
        }

        $ver = isset($state['version']) ? (int) $state['version'] : 1;
        if ($ver <= 0) {
            $ver = 1;
        }

        return [
            'balance' => (float) $bal,
            'updated_at' => $updatedAt,
            'version' => $ver,
        ];
    }

    /**
     * Set balance explicitly. Records a ledger entry.
     */
    public function setBalance(float $newBalance, array $entry = []): float
    {
        $newBalance = $this->roundCredits(max(0.0, $newBalance));
        $old = $this->getBalance();
        $delta = $this->roundCredits($newBalance - $old);

        $this->writeState($newBalance);

        // Always write a ledger entry so there's an audit trail.
        $this->insertLedger(array_merge($entry, [
            'credits_delta' => $delta,
            'balance_after' => $newBalance,
            'note' => isset($entry['note']) && is_string($entry['note']) ? (string) $entry['note'] : 'set_balance',
        ]));

        return $newBalance;
    }

    /**
     * Add credits. Records a ledger entry.
     */
    public function add(float $credits, array $entry = []): float
    {
        $credits = $this->roundCredits(max(0.0, $credits));
        if ($credits <= 0.0) {
            return $this->getBalance();
        }

        $old = $this->getBalance();
        $new = $this->roundCredits($old + $credits);

        $this->writeState($new);

        $this->insertLedger(array_merge($entry, [
            'credits_delta' => $credits,
            'balance_after' => $new,
            'note' => isset($entry['note']) && is_string($entry['note']) ? (string) $entry['note'] : 'add',
        ]));

        return $new;
    }

    /**
     * Spend credits (debit). Returns false if balance is insufficient.
     */
    public function spend(float $credits, array $entry = []): bool
    {
        $credits = $this->roundCredits(max(0.0, $credits));
        if ($credits <= 0.0) {
            return true;
        }

        $old = $this->getBalance();
        if ($old + 0.000001 < $credits) {
            return false;
        }

        $new = $this->roundCredits($old - $credits);
        if ($new < 0.0) {
            $new = 0.0;
        }

        $this->writeState($new);

        $this->insertLedger(array_merge($entry, [
            'credits_delta' => -1.0 * $credits,
            'balance_after' => $new,
            'note' => isset($entry['note']) && is_string($entry['note']) ? (string) $entry['note'] : 'spend',
        ]));

        return true;
    }

    public function canAfford(float $credits): bool
    {
        $credits = $this->roundCredits(max(0.0, $credits));
        if ($credits <= 0.0) {
            return true;
        }
        return $this->getBalance() + 0.000001 >= $credits;
    }

    /**
     * Estimate credits needed for a request.
     */
    public function estimateRequiredCredits(int $tokensInEst, int $tokensOutEst, float $costUsdEst): float
    {
        $tokensInEst = max(0, $tokensInEst);
        $tokensOutEst = max(0, $tokensOutEst);

        if ($this->mode() === 'usd') {
            $credits = (float) max(0.0, $costUsdEst) * $this->creditsPerUsd();
            return $this->roundCredits($credits);
        }

        $tokensTotal = $tokensInEst + $tokensOutEst;
        $credits = $tokensTotal / max(1, $this->tokensPerCredit());
        return $this->roundCredits($credits);
    }

    /**
     * Actual credits to debit for an executed call.
     */
    public function actualRequiredCredits(int $tokensTotal, float $costUsd): float
    {
        $tokensTotal = max(0, $tokensTotal);
        if ($this->mode() === 'usd') {
            $credits = (float) max(0.0, $costUsd) * $this->creditsPerUsd();
            return $this->roundCredits($credits);
        }

        $credits = $tokensTotal / max(1, $this->tokensPerCredit());
        return $this->roundCredits($credits);
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    public function recentLedger(int $limit = 50, int $offset = 0): array
    {
        global $wpdb;
        $limit = max(1, min(200, $limit));
        $offset = max(0, $offset);

        $table = $this->ledgerTable();
        // phpcs:ignore WordPress.DB.PreparedSQL
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$table} ORDER BY id DESC LIMIT %d OFFSET %d",
                $limit,
                $offset
            ),
            ARRAY_A
        );
        return is_array($rows) ? $rows : [];
    }

    public function clearLedger(): void
    {
        global $wpdb;
        $table = $this->ledgerTable();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $wpdb->query("TRUNCATE TABLE {$table}");
    }

    /**
     * @param array<string,mixed> $entry
     */
    public function insertLedger(array $entry): int
    {
        global $wpdb;

        $table = $this->ledgerTable();

        $createdAt = isset($entry['created_at']) && is_string($entry['created_at']) && $entry['created_at'] !== ''
            ? (string) $entry['created_at']
            : gmdate('Y-m-d H:i:s');

        $userId = isset($entry['user_id']) ? (int) $entry['user_id'] : (get_current_user_id() ?: 0);
        if ($userId <= 0) {
            $userId = 0;
        }

        $creditsDelta = isset($entry['credits_delta']) ? (float) $entry['credits_delta'] : 0.0;
        $creditsDelta = $this->roundCredits($creditsDelta);

        $balanceAfter = isset($entry['balance_after']) ? (float) $entry['balance_after'] : $this->getBalance();
        $balanceAfter = $this->roundCredits(max(0.0, $balanceAfter));

        $mode = isset($entry['mode']) ? sanitize_key((string) $entry['mode']) : $this->mode();
        if (!in_array($mode, ['tokens', 'usd'], true)) {
            $mode = $this->mode();
        }

        $provider = isset($entry['provider']) ? substr(sanitize_key((string) $entry['provider']), 0, 32) : '';
        $model = isset($entry['model']) ? substr(sanitize_text_field((string) $entry['model']), 0, 64) : '';
        $purpose = isset($entry['purpose']) ? substr(sanitize_key((string) $entry['purpose']), 0, 64) : '';

        $objectType = isset($entry['object_type']) ? substr(sanitize_key((string) $entry['object_type']), 0, 20) : '';
        $objectId = isset($entry['object_id']) ? (int) $entry['object_id'] : 0;
        if ($objectId <= 0) {
            $objectId = 0;
        }

        $tokensTotal = isset($entry['tokens_total']) ? (int) $entry['tokens_total'] : 0;
        $tokensTotal = max(0, $tokensTotal);

        $costUsd = isset($entry['cost_usd']) ? (float) $entry['cost_usd'] : 0.0;
        if ($costUsd < 0.0) {
            $costUsd = 0.0;
        }

        $note = isset($entry['note']) ? (string) $entry['note'] : '';
        $note = sanitize_text_field($note);
        if (strlen($note) > 255) {
            $note = substr($note, 0, 255);
        }

        $meta = null;
        if (isset($entry['meta'])) {
            $m = $entry['meta'];
            if (is_array($m) || is_object($m)) {
                $m = wp_json_encode($m);
            }
            if (is_string($m) && $m !== '') {
                if (strlen($m) > 20000) {
                    $m = substr($m, 0, 20000);
                }
                $meta = $m;
            }
        }

        $row = [
            'created_at' => $createdAt,
            'user_id' => $userId > 0 ? $userId : null,
            'credits_delta' => $creditsDelta,
            'balance_after' => $balanceAfter,
            'mode' => $mode,
            'provider' => $provider,
            'model' => $model,
            'purpose' => $purpose,
            'object_type' => $objectType,
            'object_id' => $objectId > 0 ? $objectId : null,
            'tokens_total' => $tokensTotal,
            'cost_usd' => $costUsd,
            'note' => $note,
            'meta' => $meta,
        ];

        $format = [
            '%s',
            '%d',
            '%f',
            '%f',
            '%s',
            '%s',
            '%s',
            '%s',
            '%s',
            '%d',
            '%d',
            '%f',
            '%s',
            '%s',
        ];

        // Allow NULLs for user_id/object_id.
        if ($row['user_id'] === null) {
            $format[1] = '%d';
        }
        if ($row['object_id'] === null) {
            $format[9] = '%d';
        }

        $wpdb->insert($table, $row, $format);
        return (int) $wpdb->insert_id;
    }

    private function writeState(float $balance): void
    {
        $state = $this->getState();
        $state['balance'] = $this->roundCredits(max(0.0, $balance));
        $state['updated_at'] = gmdate('Y-m-d H:i:s');
        $state['version'] = 1;
        update_option(self::OPTION_NAME, $state, false);
    }

    private function roundCredits(float $credits): float
    {
        return (float) round($credits, 6);
    }

    private function ledgerTable(): string
    {
        global $wpdb;
        return $wpdb->prefix . 'wpsn_ai_credits_ledger';
    }
}
