<?php
declare(strict_types=1);

final class OrderStore
{
    public function __construct(private PDO $db) { $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); }
    public static function fixture(): self
    {
        $db = new PDO('sqlite::memory:');
        $db->exec('CREATE TABLE inventory (sku TEXT PRIMARY KEY, stock INTEGER NOT NULL CHECK(stock >= 0))');
        $db->exec('CREATE TABLE orders (request_key TEXT PRIMARY KEY, quantity INTEGER NOT NULL, body_hash TEXT NOT NULL)');
        $db->exec('CREATE TABLE outbox (event_key TEXT PRIMARY KEY, request_key TEXT NOT NULL, delivered INTEGER NOT NULL DEFAULT 0)');
        $db->exec('CREATE TABLE inbox (event_key TEXT PRIMARY KEY)');
        $db->exec("INSERT INTO inventory VALUES ('DEMO-KIT', 1)");
        return new self($db);
    }
    public function connection(): PDO { return $this->db; }
    public function reserve(string $key, int $quantity = 1, bool $failBeforeCommit = false): array
    {
        if (!preg_match('/\A[a-zA-Z0-9-]{8,80}\z/D', $key) || $quantity < 1 || $quantity > 64) {
            throw new InvalidArgumentException('Use an 8–80 character request key and a quantity of 1–64.');
        }
        $hash = hash('sha256', json_encode(['sku' => 'DEMO-KIT', 'quantity' => $quantity], JSON_THROW_ON_ERROR));
        $this->db->beginTransaction();
        try {
            $find = $this->db->prepare('SELECT * FROM orders WHERE request_key = ?'); $find->execute([$key]);
            if ($previous = $find->fetch(PDO::FETCH_ASSOC)) {
                if (!hash_equals($previous['body_hash'], $hash)) { throw new DomainException('That key already identifies a different request.'); }
                $this->db->commit(); return ['accepted' => true, 'replayed' => true, 'key' => $key];
            }
            // One database statement checks availability AND changes it.
            $update = $this->db->prepare('UPDATE inventory SET stock = stock - ? WHERE sku = ? AND stock >= ?');
            $update->execute([$quantity, 'DEMO-KIT', $quantity]);
            if ($update->rowCount() !== 1) { $this->db->rollBack(); return ['accepted' => false, 'replayed' => false, 'reason' => 'The last item is already reserved.']; }
            $this->db->prepare('INSERT INTO orders VALUES (?, ?, ?)')->execute([$key, $quantity, $hash]);
            $this->db->prepare('INSERT INTO outbox (event_key, request_key) VALUES (?, ?)')->execute(['order-' . $key, $key]);
            if ($failBeforeCommit) { throw new RuntimeException('Injected failure before commit.'); }
            $this->db->commit(); return ['accepted' => true, 'replayed' => false, 'key' => $key];
        } catch (Throwable $error) {
            if ($this->db->inTransaction()) { $this->db->rollBack(); }
            throw $error;
        }
    }
    public function deliver(string $eventKey): bool
    {
        // A local, transactional receiver. This does NOT promise exactly-once email/payment.
        $this->db->beginTransaction();
        try {
            $event = $this->db->prepare('SELECT event_key FROM outbox WHERE event_key = ?'); $event->execute([$eventKey]);
            if ($event->fetchColumn() === false) { throw new InvalidArgumentException('Unknown event.'); }
            $insert = $this->db->prepare('INSERT OR IGNORE INTO inbox VALUES (?)'); $insert->execute([$eventKey]);
            $new = $insert->rowCount() === 1;
            $this->db->prepare('UPDATE outbox SET delivered = 1 WHERE event_key = ?')->execute([$eventKey]);
            $this->db->commit(); return $new;
        } catch (Throwable $error) { if ($this->db->inTransaction()) { $this->db->rollBack(); } throw $error; }
    }
    public function snapshot(): array
    {
        return ['stock' => (int)$this->db->query('SELECT stock FROM inventory')->fetchColumn(),
                'orders' => (int)$this->db->query('SELECT COUNT(*) FROM orders')->fetchColumn(),
                'events' => (int)$this->db->query('SELECT COUNT(*) FROM outbox')->fetchColumn(),
                'receipts' => (int)$this->db->query('SELECT COUNT(*) FROM inbox')->fetchColumn()];
    }
}
