<?php
class Database
{
    private static ?Database $instance = null;
    private PDO $pdo;
    private array $config;
    private bool $inTransaction = false;

    private function __construct()
    {
        $this->config = require ROOT_PATH . '/config/database.php';
        $this->connect();
    }

    private function connect(): void
    {
        $dsn = sprintf(
            'mysql:host=%s;port=%s;dbname=%s;charset=%s',
            $this->config['host'],
            $this->config['port'],
            $this->config['database'],
            $this->config['charset']
        );

        /**
         * BUG 3 FIX: emulate_prepares = true
         * With emulate_prepares = false, PDO::execute(array) binds ALL params
         * as PDO::PARAM_STR. MySQL rejects LIMIT '25' — it needs an integer.
         * With emulate_prepares = true, PDO handles escaping itself and
         * stringified integers in LIMIT/OFFSET work correctly.
         * Combined with utf8mb4 charset set via DSN, this is safe.
         */
        $options = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => true,
            PDO::ATTR_STRINGIFY_FETCHES  => false,
            PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES {$this->config['charset']} COLLATE {$this->config['collation']}",
        ];

        $this->pdo = new PDO($dsn, $this->config['username'], $this->config['password'], $options);
    }

    public static function getInstance(): Database
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

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

    public function fetchAll(string $sql, array $params = []): array
    {
        $stmt = $this->executeQuery($sql, $params);
        return $stmt->fetchAll();
    }

    public function fetchOne(string $sql, array $params = []): ?array
    {
        $stmt = $this->executeQuery($sql, $params);
        $row = $stmt->fetch();
        return $row ?: null;
    }

    public function fetchColumn(string $sql, array $params = [])
    {
        $stmt = $this->executeQuery($sql, $params);
        $val = $stmt->fetchColumn();
        return $val === false ? null : $val;
    }

    public function insert(string $table, array $data): int
    {
        if (empty($data)) {
            throw new \InvalidArgumentException("Cannot insert empty data into {$table}");
        }

        $columns = array_keys($data);
        $placeholders = array_fill(0, count($columns), '?');

        $sql = sprintf(
            'INSERT INTO `%s` (`%s`) VALUES (%s)',
            $table,
            implode('`, `', $columns),
            implode(', ', $placeholders)
        );

        $this->executeQuery($sql, array_values($data));
        return (int) $this->pdo->lastInsertId();
    }

    public function update(string $table, array $data, string $where, array $whereParams = []): int
    {
        if (empty($data)) {
            return 0;
        }

        $setClauses = [];
        $values = [];
        foreach ($data as $col => $val) {
            $setClauses[] = "`{$col}` = ?";
            $values[] = $val;
        }

        $sql = sprintf(
            'UPDATE `%s` SET %s WHERE %s',
            $table,
            implode(', ', $setClauses),
            $where
        );

        $stmt = $this->executeQuery($sql, array_merge($values, $whereParams));
        return $stmt->rowCount();
    }

    public function delete(string $table, string $where, array $params = []): int
    {
        $sql = sprintf('DELETE FROM `%s` WHERE %s', $table, $where);
        $stmt = $this->executeQuery($sql, $params);
        return $stmt->rowCount();
    }

    public function execute(string $sql, array $params = []): PDOStatement
    {
        return $this->executeQuery($sql, $params);
    }

    public function beginTransaction(): void
    {
        if (!$this->inTransaction) {
            $this->pdo->beginTransaction();
            $this->inTransaction = true;
        }
    }

    public function commit(): void
    {
        if ($this->inTransaction) {
            $this->pdo->commit();
            $this->inTransaction = false;
        }
    }

    public function rollBack(): void
    {
        if ($this->inTransaction) {
            $this->pdo->rollBack();
            $this->inTransaction = false;
        }
    }

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

    public function table(string $table): QueryBuilder
    {
        return new QueryBuilder($this, $table);
    }

    private function executeQuery(string $sql, array $params = []): PDOStatement
    {
        try {
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            return $stmt;
        } catch (PDOException $e) {
            if (stripos($e->getMessage(), 'gone away') !== false || $e->getCode() == 'HY000') {
                $this->connect();
                $stmt = $this->pdo->prepare($sql);
                $stmt->execute($params);
                return $stmt;
            }
            Logger::error('Database query failed: ' . $e->getMessage(), [
                'sql' => $sql,
                'params' => $params,
            ]);
            throw $e;
        }
    }
}

class QueryBuilder
{
    private Database $db;
    private string $table;
    private array $wheres = [];
    private array $whereParams = [];
    private string $orderBy = '';
    private ?int $limitVal = null;
    private ?int $offsetVal = null;
    private string $selectColumns = '*';

    public function __construct(Database $db, string $table)
    {
        $this->db = $db;
        $this->table = $table;
    }

    public function select(string $columns): self
    {
        $this->selectColumns = $columns;
        return $this;
    }

    public function where(string $column, string $operator, $value): self
    {
        $this->wheres[] = "`{$column}` {$operator} ?";
        $this->whereParams[] = $value;
        return $this;
    }

    public function whereRaw(string $condition, array $params = []): self
    {
        $this->wheres[] = $condition;
        $this->whereParams = array_merge($this->whereParams, $params);
        return $this;
    }

    public function whereIn(string $column, array $values): self
    {
        if (empty($values)) {
            $this->wheres[] = '1=0'; // always false
            return $this;
        }
        $placeholders = implode(',', array_fill(0, count($values), '?'));
        $this->wheres[] = "`{$column}` IN ({$placeholders})";
        $this->whereParams = array_merge($this->whereParams, $values);
        return $this;
    }

    public function whereNull(string $column): self
    {
        $this->wheres[] = "`{$column}` IS NULL";
        return $this;
    }

    public function whereNotNull(string $column): self
    {
        $this->wheres[] = "`{$column}` IS NOT NULL";
        return $this;
    }

    public function orderBy(string $column, string $direction = 'ASC'): self
    {
        $direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC';
        if ($this->orderBy) {
            $this->orderBy .= ", `{$column}` {$direction}";
        } else {
            $this->orderBy = "ORDER BY `{$column}` {$direction}";
        }
        return $this;
    }

    public function orderByRaw(string $raw): self
    {
        $this->orderBy = "ORDER BY {$raw}";
        return $this;
    }

    public function limit(int $limit): self
    {
        $this->limitVal = $limit;
        return $this;
    }

    public function offset(int $offset): self
    {
        $this->offsetVal = $offset;
        return $this;
    }

    public function get(): array
    {
        return $this->db->fetchAll($this->buildQuery(), $this->whereParams);
    }

    public function first(): ?array
    {
        $this->limitVal = 1;
        return $this->db->fetchOne($this->buildQuery(), $this->whereParams);
    }

    public function count(): int
    {
        $sql = "SELECT COUNT(*) FROM `{$this->table}`";
        if (!empty($this->wheres)) {
            $sql .= ' WHERE ' . implode(' AND ', $this->wheres);
        }
        return (int) $this->db->fetchColumn($sql, $this->whereParams);
    }

    public function exists(): bool
    {
        return $this->count() > 0;
    }

    /**
     * BUG 3 FIX: Inline LIMIT/OFFSET as integers directly in SQL.
     * Even though we fixed emulate_prepares, this is defense-in-depth.
     * These values are already validated as int by the setter methods.
     */
    private function buildQuery(): string
    {
        $sql = "SELECT {$this->selectColumns} FROM `{$this->table}`";
        if (!empty($this->wheres)) {
            $sql .= ' WHERE ' . implode(' AND ', $this->wheres);
        }
        if ($this->orderBy) {
            $sql .= ' ' . $this->orderBy;
        }
        if ($this->limitVal !== null) {
            $sql .= ' LIMIT ' . (int)$this->limitVal;
        }
        if ($this->offsetVal !== null) {
            $sql .= ' OFFSET ' . (int)$this->offsetVal;
        }
        return $sql;
    }
}