<?php
/**
 * A SupabaseClient-shaped adapter over a real Postgres connection.
 *
 * The engine speaks PostgREST filter syntax ("eq.<v>", "in.(a,b)", "neq.<v>",
 * "is.true", "lt.<ts>"). This translates that to SQL and mimics PostgREST's
 * return shapes precisely — including the two behaviours the engine's
 * correctness depends on:
 *
 *   - an UPDATE that matches zero rows returns [] (that is the conditional-write
 *     guard that stops two players both finalising a match)
 *   - any failure returns ['error' => ...] (that is the duplicate-key signal
 *     that makes tournament match creation race-free)
 */
require_once dirname(__DIR__) . '/config/database.php';

class PgShim extends SupabaseClient {
    private PDO $pdo;
    public array $log = [];

    public function __construct(string $dsn = 'pgsql:host=127.0.0.1;port=54329;dbname=postgres', string $user = 'postgres') {
        parent::__construct(null, false);
        $this->pdo = new PDO($dsn, $user, '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    }

    public function pdo(): PDO { return $this->pdo; }

    /** PostgREST filter -> [sql fragment, bound values] */
    private function where(array $params, array &$vals): string {
        $clauses = [];
        foreach ($params as $col => $expr) {
            if (in_array($col, ['select', 'order', 'limit', 'offset', 'on_conflict'], true)) continue;
            if (!is_string($expr)) continue;

            if (preg_match('/^eq\.(.*)$/s', $expr, $m)) {
                $clauses[] = "\"$col\" = ?"; $vals[] = $m[1];
            } elseif (preg_match('/^neq\.(.*)$/s', $expr, $m)) {
                // PostgREST's neq excludes NULLs too, matching `IS DISTINCT FROM`
                // only for non-null columns; for status columns this is equivalent.
                $clauses[] = "\"$col\" IS DISTINCT FROM ?"; $vals[] = $m[1];
            } elseif (preg_match('/^not\.in\.\((.*)\)$/s', $expr, $m)) {
                $items = $m[1] === '' ? [] : explode(',', $m[1]);
                if ($items) {
                    $clauses[] = "\"$col\" NOT IN (" . implode(',', array_fill(0, count($items), '?')) . ')';
                    foreach ($items as $i) $vals[] = trim($i, '"');
                }
            } elseif (preg_match('/^in\.\((.*)\)$/s', $expr, $m)) {
                $items = $m[1] === '' ? [] : explode(',', $m[1]);
                if (!$items) { $clauses[] = 'false'; continue; }
                $clauses[] = "\"$col\" IN (" . implode(',', array_fill(0, count($items), '?')) . ')';
                foreach ($items as $i) $vals[] = trim($i, '"');
            } elseif (preg_match('/^lte\.(.*)$/s', $expr, $m)) {
                $clauses[] = "\"$col\" <= ?"; $vals[] = $m[1];
            } elseif (preg_match('/^gte\.(.*)$/s', $expr, $m)) {
                $clauses[] = "\"$col\" >= ?"; $vals[] = $m[1];
            } elseif (preg_match('/^lt\.(.*)$/s', $expr, $m)) {
                $clauses[] = "\"$col\" < ?"; $vals[] = $m[1];
            } elseif (preg_match('/^gt\.(.*)$/s', $expr, $m)) {
                $clauses[] = "\"$col\" > ?"; $vals[] = $m[1];
            } elseif ($expr === 'is.true') {
                $clauses[] = "\"$col\" IS TRUE";
            } elseif ($expr === 'is.null') {
                $clauses[] = "\"$col\" IS NULL";
            } elseif ($col === 'or') {
                // "(a.eq.X,b.eq.Y)"
                $inner = trim($expr, '()');
                $parts = [];
                foreach (explode(',', $inner) as $p) {
                    if (preg_match('/^(\w+)\.eq\.(.*)$/s', $p, $mm)) {
                        $parts[] = "\"{$mm[1]}\" = ?"; $vals[] = $mm[2];
                    }
                }
                if ($parts) $clauses[] = '(' . implode(' OR ', $parts) . ')';
            } else {
                // Never ignore a filter we do not understand. Silently dropping
                // one makes a query match far more rows than production would,
                // and the test then passes while the real behaviour differs.
                throw new RuntimeException("PgShim: unsupported PostgREST filter \"$col=$expr\" — add it rather than letting the query match everything");
            }
        }
        return $clauses ? ('WHERE ' . implode(' AND ', $clauses)) : '';
    }

    private function decodeRow(array $row): array {
        foreach ($row as $k => $v) {
            // Mirror PostgREST: jsonb comes back decoded, not as a string.
            if (is_string($v) && in_array($k, ['pairings', 'results', 'moves', 'metadata', 'game_state', 'tiebreak_rules', 'payload'], true)) {
                $d = json_decode($v, true);
                $row[$k] = $d === null ? $v : $d;
            }
        }
        return $row;
    }

    private function encodeValue($v) {
        if (is_array($v) || is_object($v)) return json_encode($v);
        if (is_bool($v)) return $v ? 'true' : 'false';
        return $v;
    }

    public function get(string $table, array $params = []): array {
        $vals = [];
        $where = $this->where($params, $vals);
        $select = $params['select'] ?? '*';
        // Keep it simple: fetch whole rows, then project.
        $sql = "SELECT * FROM \"$table\" $where";
        if (!empty($params['order'])) {
            [$c, $d] = array_pad(explode('.', $params['order']), 2, 'asc');
            $sql .= ' ORDER BY "' . $c . '" ' . (strtolower($d) === 'desc' ? 'DESC' : 'ASC');
        }
        if (!empty($params['limit'])) $sql .= ' LIMIT ' . (int)$params['limit'];

        try {
            $st = $this->pdo->prepare($sql);
            $st->execute($vals);
            $rows = array_map([$this, 'decodeRow'], $st->fetchAll(PDO::FETCH_ASSOC));
        } catch (PDOException $e) {
            return ['error' => $e->getMessage(), 'code' => 400];
        }

        if ($select !== '*') {
            $cols = array_map('trim', explode(',', $select));
            $rows = array_map(fn($r) => array_intersect_key($r, array_flip($cols)), $rows);
        }
        return $rows;
    }

    public function getOne(string $table, array $params = []): ?array {
        $params['limit'] = 1;
        $r = $this->get($table, $params);
        if (isset($r['error'])) return $r;
        return $r[0] ?? null;
    }

    public function insert(string $table, array $data): array {
        $cols = array_keys($data);
        $sql = "INSERT INTO \"$table\" (" . implode(',', array_map(fn($c) => "\"$c\"", $cols)) . ')'
             . ' VALUES (' . implode(',', array_fill(0, count($cols), '?')) . ') RETURNING *';
        try {
            $st = $this->pdo->prepare($sql);
            $st->execute(array_map([$this, 'encodeValue'], array_values($data)));
            return array_map([$this, 'decodeRow'], $st->fetchAll(PDO::FETCH_ASSOC));
        } catch (PDOException $e) {
            return ['error' => $e->getMessage(), 'code' => 409];
        }
    }

    public function update(string $table, array $data, array $params = []): array {
        $vals = [];
        $sets = [];
        foreach ($data as $c => $v) { $sets[] = "\"$c\" = ?"; $vals[] = $this->encodeValue($v); }
        $whereVals = [];
        $where = $this->where($params, $whereVals);
        $sql = "UPDATE \"$table\" SET " . implode(',', $sets) . " $where RETURNING *";
        try {
            $st = $this->pdo->prepare($sql);
            $st->execute(array_merge($vals, $whereVals));
            return array_map([$this, 'decodeRow'], $st->fetchAll(PDO::FETCH_ASSOC));
        } catch (PDOException $e) {
            return ['error' => $e->getMessage(), 'code' => 400];
        }
    }

    public function delete(string $table, array $params = []): array {
        $vals = [];
        $where = $this->where($params, $vals);
        try {
            $st = $this->pdo->prepare("DELETE FROM \"$table\" $where RETURNING *");
            $st->execute($vals);
            return $st->fetchAll(PDO::FETCH_ASSOC);
        } catch (PDOException $e) {
            return ['error' => $e->getMessage(), 'code' => 400];
        }
    }

    public function rpc(string $fn, array $data = []): array { return []; }
}
