<?php
declare(strict_types=1);

namespace Favo\Services;

use Favo\Services\AutoUnlockService;

use PDO;
use PDOException;

final class PurchaseService
{
    private bool $lastIdempotent = false;
    private array $lastAutoUnlocked = [];

    public function __construct(
        private PDO $db,
        private ?AuditService $audit = null,
        private ?LoyaltyService $loyalty = null,
        private ?AutoUnlockService $autoUnlock = null
    ) {
    }

    public function wasIdempotent(): bool
    {
        return $this->lastIdempotent;
    }

    public function lastAutoUnlocked(): array
    {
        return $this->lastAutoUnlocked;
    }

    /**
     * Create a purchase atomically.
     *
     * Security rules:
     * - reward values supplied by the client are ignored;
     * - program must belong to the merchant and be ACTIVE/in date;
     * - branch must belong to the merchant;
     * - customer must exist;
     * - points are calculated from program rules_json;
     * - stamps are only awarded when an explicit stamp rule exists;
     * - transaction_uuid is the idempotency key;
     * - immutable purchase/loyalty/audit changes are committed together.
     */
    public function create(array $data, int $accountId): int
    {
        $this->lastIdempotent = false;
        $this->lastAutoUnlocked = [];

        $uuid = trim((string)($data['transaction_uuid'] ?? ''));
        if ($uuid === '') {
            $uuid = $this->uuid();
        }

        $merchantId = (int)($data['merchant_id'] ?? 0);
        $branchId = (int)($data['branch_id'] ?? 0);
        $customerId = (int)($data['customer_id'] ?? 0);
        $programId = (int)($data['program_id'] ?? 0);
        $amount = (float)($data['purchase_amount'] ?? 0);

        if ($merchantId < 1 || $customerId < 1 || $amount <= 0) {
            throw new \InvalidArgumentException('Maklumat transaksi tidak sah.');
        }

        // Validate merchant/branch/customer/program before creating anything.
        $this->validateMerchant($merchantId);
        if ($branchId > 0) {
            $this->validateBranchBelongsToMerchant($branchId, $merchantId);
        }
        $this->validateCustomer($customerId);

        $program = null;
        if ($programId > 0) {
            $program = $this->validateProgram($programId, $merchantId);
        }

        // Existing UUID is idempotent only when it belongs to the same business
        // context. Never expose another merchant's transaction through a UUID.
        $q = $this->db->prepare(
            'SELECT id, merchant_id, branch_id, customer_id
             FROM purchase_transactions
             WHERE transaction_uuid=? LIMIT 1'
        );
        $q->execute([$uuid]);
        $existing = $q->fetch(PDO::FETCH_ASSOC);

        if ($existing) {
            if (
                (int)$existing['merchant_id'] !== $merchantId ||
                (int)$existing['customer_id'] !== $customerId ||
                ($branchId > 0 && (int)$existing['branch_id'] !== $branchId)
            ) {
                throw new \RuntimeException('Transaction UUID telah digunakan untuk transaksi lain.');
            }

            $this->lastIdempotent = true;
            return (int)$existing['id'];
        }

        // Server-side reward calculation. Never trust POST points/stamps.
        [$points, $stamps] = $this->calculateRewards($program, $amount);

        $started = false;

        try {
            if (!$this->db->inTransaction()) {
                $this->db->beginTransaction();
                $started = true;
            }

            $s = $this->db->prepare(
                'INSERT INTO purchase_transactions
                (transaction_uuid,merchant_id,branch_id,customer_id,program_id,staff_id,
                 purchase_amount,currency,transaction_type,source,status)
                 VALUES(?,?,?,?,?,?,?,?,?,?,?)'
            );

            try {
                $s->execute([
                    $uuid,
                    $merchantId,
                    $branchId ?: null,
                    $customerId,
                    $programId ?: null,
                    ($data['staff_id'] ?? null) ?: null,
                    $amount,
                    $data['currency'] ?? 'MYR',
                    'PURCHASE',
                    'FAVO',
                    'COMPLETED',
                ]);
            } catch (PDOException $e) {
                if ($this->isDuplicateKey($e)) {
                    if ($started && $this->db->inTransaction()) {
                        $this->db->rollBack();
                    }

                    $q = $this->db->prepare(
                        'SELECT id, merchant_id, branch_id, customer_id
                         FROM purchase_transactions
                         WHERE transaction_uuid=? LIMIT 1'
                    );
                    $q->execute([$uuid]);
                    $existing = $q->fetch(PDO::FETCH_ASSOC);

                    if ($existing) {
                        if (
                            (int)$existing['merchant_id'] !== $merchantId ||
                            (int)$existing['customer_id'] !== $customerId ||
                            ($branchId > 0 && (int)$existing['branch_id'] !== $branchId)
                        ) {
                            throw new \RuntimeException('Transaction UUID telah digunakan untuk transaksi lain.');
                        }

                        $this->lastIdempotent = true;
                        return (int)$existing['id'];
                    }
                }
                throw $e;
            }

            $id = (int)$this->db->lastInsertId();

            if ($this->loyalty && ($points > 0 || $stamps > 0)) {
                $this->loyalty->earn(
                    $customerId,
                    $merchantId,
                    $points,
                    $stamps,
                    'PURCHASE',
                    $id,
                    $accountId
                );
            }

            // Auto Unlock V1.1: evaluate newly earned loyalty before COMMIT.
            // Idempotent retries return above and therefore cannot create duplicate unlocks.
            $autoUnlocked = [];
            if (
                $this->autoUnlock
                && ($points > 0 || $stamps > 0)
            ) {
                $autoUnlocked = $this->autoUnlock->evaluateAfterEarn(
                    $customerId,
                    $merchantId,
                    $programId > 0 ? $programId : null,
                    $accountId
                );
                $this->lastAutoUnlocked = $autoUnlocked;
            }

            $this->audit?->log(
                $accountId,
                'purchase.create',
                'purchase_transactions',
                $id,
                [
                    'amount' => $amount,
                    'transaction_uuid' => $uuid,
                    'points' => $points,
                    'stamps' => $stamps,
                    'program_id' => $programId ?: null,
                ]
            );

            if ($started && $this->db->inTransaction()) {
                $this->db->commit();
            }

            return $id;
        } catch (\Throwable $e) {
            if ($started && $this->db->inTransaction()) {
                $this->db->rollBack();
            }
            throw $e;
        }
    }

    private function validateMerchant(int $merchantId): void
    {
        $s = $this->db->prepare(
            'SELECT id FROM merchants WHERE id=? AND status IN ("ACTIVE","PENDING") LIMIT 1'
        );
        $s->execute([$merchantId]);

        if (!$s->fetchColumn()) {
            throw new \RuntimeException('Merchant tidak sah atau tidak aktif.');
        }
    }

    private function validateBranchBelongsToMerchant(int $branchId, int $merchantId): void
    {
        $s = $this->db->prepare(
            'SELECT id FROM branches
             WHERE id=? AND merchant_id=? AND status="ACTIVE"
             LIMIT 1'
        );
        $s->execute([$branchId, $merchantId]);

        if (!$s->fetchColumn()) {
            throw new \RuntimeException('Branch tidak sah untuk merchant ini.');
        }
    }

    private function validateCustomer(int $customerId): void
    {
        $s = $this->db->prepare(
            'SELECT id FROM customers WHERE id=? LIMIT 1'
        );
        $s->execute([$customerId]);

        if (!$s->fetchColumn()) {
            throw new \RuntimeException('Customer tidak sah.');
        }
    }

    private function validateProgram(int $programId, int $merchantId): array
    {
        $s = $this->db->prepare(
            'SELECT id, merchant_id, branch_id, type, status, rules_json, starts_at, ends_at
             FROM programs
             WHERE id=? AND merchant_id=?
             LIMIT 1'
        );
        $s->execute([$programId, $merchantId]);
        $program = $s->fetch(PDO::FETCH_ASSOC);

        if (!$program) {
            throw new \RuntimeException('Program tidak sah untuk merchant ini.');
        }

        if ((string)$program['status'] !== 'ACTIVE') {
            throw new \RuntimeException('Program bukan dalam status ACTIVE.');
        }

        $now = new \DateTimeImmutable('now');
        if (!empty($program['starts_at']) && $now < new \DateTimeImmutable((string)$program['starts_at'])) {
            throw new \RuntimeException('Program belum bermula.');
        }
        if (!empty($program['ends_at']) && $now > new \DateTimeImmutable((string)$program['ends_at'])) {
            throw new \RuntimeException('Program telah tamat.');
        }

        return $program;
    }

    /**
     * Returns [points, stamps].
     *
     * Supported points rule:
     *   {"ratio":1}
     * means RM1 = 1 point.
     *
     * Stamps are deliberately zero unless the program explicitly defines
     * a supported stamp rule. This prevents a client from inventing stamps.
     */
    private function calculateRewards(?array $program, float $amount): array
    {
        if (!$program) {
            return [0.0, 0];
        }

        $rules = [];
        $raw = (string)($program['rules_json'] ?? '');
        if ($raw !== '') {
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) {
                $rules = $decoded;
            }
        }

        $points = 0.0;
        if (isset($rules['ratio']) && is_numeric($rules['ratio'])) {
            $ratio = max(0.0, (float)$rules['ratio']);
            $points = round($amount * $ratio, 2);
        }

        $stamps = 0;
        foreach (['stamps_per_purchase', 'stamp_per_purchase'] as $key) {
            if (isset($rules[$key]) && is_numeric($rules[$key])) {
                $stamps = max(0, (int)$rules[$key]);
                break;
            }
        }

        return [$points, $stamps];
    }

    private function isDuplicateKey(PDOException $e): bool
    {
        return (int)($e->errorInfo[1] ?? 0) === 1062;
    }

    private function uuid(): string
    {
        $d = random_bytes(16);
        $d[6] = chr((ord($d[6]) & 15) | 64);
        $d[8] = chr((ord($d[8]) & 63) | 128);

        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($d), 4));
    }
}
