<?php
declare(strict_types=1);
namespace Workshop;

use PDO;
use RuntimeException;
use Throwable;

final class Repository
{
    public PDO $db;
    public function __construct(string $path)
    {
        $this->db = new PDO('sqlite:'.$path, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
        $this->db->exec('PRAGMA busy_timeout=3000');
        $this->db->exec('CREATE TABLE IF NOT EXISTS tickets(id TEXT PRIMARY KEY, marking TEXT NOT NULL, amount INTEGER NOT NULL, version INTEGER NOT NULL)');
        $this->db->exec('CREATE TABLE IF NOT EXISTS receipts(operation_key TEXT PRIMARY KEY, fingerprint TEXT NOT NULL, response TEXT NOT NULL)');
        $this->db->exec('CREATE TABLE IF NOT EXISTS outbox(id INTEGER PRIMARY KEY, operation_key TEXT NOT NULL UNIQUE, ticket_id TEXT NOT NULL, event TEXT NOT NULL, payload TEXT NOT NULL)');
    }
    public function seed(string $id = 'REQ-1042', string $marking = 'draft'): void
    {
        $this->db->prepare('INSERT INTO tickets(id,marking,amount,version) VALUES(?,?,45000,1)')->execute([$id,$marking]);
    }
    public function read(string $id = 'REQ-1042'): array
    {
        $statement = $this->db->prepare('SELECT * FROM tickets WHERE id=?'); $statement->execute([$id]);
        return $statement->fetch() ?: throw new RuntimeException('ticket_not_found');
    }
    public function counts(): array
    {
        return ['outbox' => (int)$this->db->query('SELECT COUNT(*) FROM outbox')->fetchColumn(), 'receipts' => (int)$this->db->query('SELECT COUNT(*) FROM receipts')->fetchColumn()];
    }
    public function execute(string $id, int $expectedVersion, string $transition, string $operationKey, string $role = 'manager', string $reason = ''): array
    {
        // A real API obtains id/role from its authenticated request, never from an untrusted role field.
        $fingerprint = hash('sha256', json_encode([$id,$expectedVersion,$transition,$role,$reason], JSON_THROW_ON_ERROR));
        $this->db->exec('BEGIN IMMEDIATE');
        try {
            $statement = $this->db->prepare('SELECT fingerprint,response FROM receipts WHERE operation_key=?');
            $statement->execute([$operationKey]); $receipt = $statement->fetch();
            if ($receipt) {
                if (!hash_equals($receipt['fingerprint'], $fingerprint)) { throw new RuntimeException('idempotency_key_reused'); }
                $this->db->exec('COMMIT');
                return ['replayed' => true, 'ticket' => json_decode($receipt['response'], true, flags: JSON_THROW_ON_ERROR)];
            }
            $row = $this->read($id);
            if ((int)$row['version'] !== $expectedVersion) { throw new RuntimeException('version_conflict'); }
            $ticket = new Ticket($id, $row['marking'], (int)$row['amount'], $role, $reason);
            (new Processes())->ticket()->apply($ticket, $transition, ['operation_key' => $operationKey]);
            $statement = $this->db->prepare('UPDATE tickets SET marking=?,version=version+1 WHERE id=? AND version=?');
            $statement->execute([$ticket->marking,$id,$expectedVersion]);
            if ($statement->rowCount() !== 1) { throw new RuntimeException('version_conflict'); }
            $result = $this->read($id); $json = json_encode($result, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
            $this->db->prepare('INSERT INTO outbox(operation_key,ticket_id,event,payload) VALUES(?,?,?,?)')->execute([$operationKey,$id,'ticket.'.$transition,$json]);
            $this->db->prepare('INSERT INTO receipts(operation_key,fingerprint,response) VALUES(?,?,?)')->execute([$operationKey,$fingerprint,$json]);
            $this->db->exec('COMMIT');
            return ['replayed' => false, 'ticket' => $result];
        } catch (Throwable $error) {
            $this->db->exec('ROLLBACK');
            throw $error;
        }
    }
}
