<?php
declare(strict_types=1);

namespace App\Core;

class Database
{
    private \PDO $pdo;
    private array $afterInsertHooks = [];
    private array $beforeUpdateHooks = [];
    private array $afterUpdateHooks = [];
    private array $afterDeleteHooks = [];
    private array $tableCache = [];

    public function __construct(string $host = '127.0.0.1', int $port = 3306, string $dbName = '', string $user = 'root', string $pass = '', string $charset = 'utf8mb4')
    {
        $dsn = "mysql:host={$host};port={$port};dbname={$dbName};charset={$charset}";

        $this->pdo = new \PDO($dsn, $user, $pass, [
            \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,
            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
            \PDO::ATTR_EMULATE_PREPARES   => false,
            \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES {$charset}",
        ]);
    }

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

    // ═══════════════════════════════════════════
    // CORE QUERY
    // ═══════════════════════════════════════════

    public function query(string $sql, array $params = []): \PDOStatement
    {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt;
    }

    public function raw(string $sql): void
    {
        $this->pdo->exec($sql);
    }

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

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

    // ═══════════════════════════════════════════
    // INSERT
    // ═══════════════════════════════════════════

    public function insert(string $table, array $data): int
    {
        $columns = array_keys($data);
        $placeholders = array_fill(0, count($columns), '?');

        $sql = sprintf(
            'INSERT INTO `%s` (%s) VALUES (%s)',
            $table,
            implode(', ', array_map(fn($c) => "`{$c}`", $columns)),
            implode(', ', $placeholders)
        );

        $this->query($sql, array_values($data));
        $id = (int) $this->pdo->lastInsertId();

        // Fire after-insert hooks
        foreach ($this->afterInsertHooks as $hook) {
            try {
                $hook($table, $data, $id);
            } catch (\Throwable $e) {
                // Hooks must never break the main operation
            }
        }

        return $id;
    }

    // ═══════════════════════════════════════════
    // UPDATE — with before/after audit support
    // ═══════════════════════════════════════════

    public function update(string $table, array $data, string $where, array $whereParams = []): int
    {
        // ── 1. Try to fetch old record(s) BEFORE updating ──
        $oldRecord = null;
        $entityId = null;

        if (!empty($this->afterUpdateHooks)) {
            try {
                $entityId = $this->extractIdFromWhere($where, $whereParams);

                if ($entityId !== null) {
                    $oldRecord = $this->selectOne("SELECT * FROM `{$table}` WHERE `id` = ?", [$entityId]);
                } else {
                    $oldRecord = $this->selectOne("SELECT * FROM `{$table}` WHERE {$where} LIMIT 1", $whereParams);
                    if ($oldRecord && isset($oldRecord['id'])) {
                        $entityId = (int) $oldRecord['id'];
                    }
                }
            } catch (\Throwable $e) {
                // Silently continue — audit is best-effort
            }
        }

        // ── 2. Fire before-update hooks ──
        foreach ($this->beforeUpdateHooks as $hook) {
            try {
                $hook($table, $data, $entityId, $oldRecord);
            } catch (\Throwable $e) {}
        }

        // ── 3. Execute the UPDATE ──
        $setParts = [];
        foreach (array_keys($data) as $col) {
            $setParts[] = "`{$col}` = ?";
        }

        $sql = sprintf('UPDATE `%s` SET %s WHERE %s', $table, implode(', ', $setParts), $where);
        $params = array_merge(array_values($data), $whereParams);

        $stmt = $this->query($sql, $params);
        $affected = $stmt->rowCount();

        // ── 4. Fire after-update hooks with before/after data ──
        if ($affected > 0) {
            $newRecord = null;
            if (!empty($this->afterUpdateHooks)) {
                try {
                    if ($entityId !== null) {
                        $newRecord = $this->selectOne("SELECT * FROM `{$table}` WHERE `id` = ?", [$entityId]);
                    } elseif ($oldRecord) {
                        $newRecord = array_merge($oldRecord, $data);
                    }
                } catch (\Throwable $e) {
                    $newRecord = $data;
                }
            }

            foreach ($this->afterUpdateHooks as $hook) {
                try {
                    $hook($table, $data, $entityId, $oldRecord, $newRecord);
                } catch (\Throwable $e) {}
            }
        }

        return $affected;
    }

    // ═══════════════════════════════════════════
    // DELETE
    // ═══════════════════════════════════════════

    public function delete(string $table, string $where, array $whereParams = []): int
    {
        $oldRecord = null;
        $entityId = null;

        try {
            $entityId = $this->extractIdFromWhere($where, $whereParams);
            if ($entityId !== null) {
                $oldRecord = $this->selectOne("SELECT * FROM `{$table}` WHERE `id` = ?", [$entityId]);
            } else {
                $oldRecord = $this->selectOne("SELECT * FROM `{$table}` WHERE {$where} LIMIT 1", $whereParams);
                if ($oldRecord && isset($oldRecord['id'])) {
                    $entityId = (int) $oldRecord['id'];
                }
            }
        } catch (\Throwable $e) {}

        $sql = sprintf('DELETE FROM `%s` WHERE %s', $table, $where);
        $stmt = $this->query($sql, $whereParams);
        $affected = $stmt->rowCount();

        if ($affected > 0) {
            foreach ($this->afterDeleteHooks as $hook) {
                try {
                    $hook($table, $oldRecord ?? [], $entityId);
                } catch (\Throwable $e) {}
            }
        }

        return $affected;
    }

    // ═══════════════════════════════════════════
    // TRANSACTIONS
    // ═══════════════════════════════════════════

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

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

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

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

    // ═══════════════════════════════════════════
    // TABLE EXISTS CHECK
    // ═══════════════════════════════════════════

    public function tableExists(string $table): bool
    {
        if (isset($this->tableCache[$table])) {
            return $this->tableCache[$table];
        }

        try {
            $this->query("SELECT 1 FROM `{$table}` LIMIT 1");
            $this->tableCache[$table] = true;
        } catch (\Throwable $e) {
            $this->tableCache[$table] = false;
        }

        return $this->tableCache[$table];
    }

    // ═══════════════════════════════════════════
    // HOOK REGISTRATION
    // ═══════════════════════════════════════════

    public function onAfterInsert(callable $callback): void
    {
        $this->afterInsertHooks[] = $callback;
    }

    public function onBeforeUpdate(callable $callback): void
    {
        $this->beforeUpdateHooks[] = $callback;
    }

    public function onAfterUpdate(callable $callback): void
    {
        $this->afterUpdateHooks[] = $callback;
    }

    public function onAfterDelete(callable $callback): void
    {
        $this->afterDeleteHooks[] = $callback;
    }

    // ═══════════════════════════════════════════
    // INTERNAL HELPERS
    // ═══════════════════════════════════════════

    private function extractIdFromWhere(string $where, array $params): ?int
    {
        if (preg_match('/^`?id`?\s*=\s*\?/', trim($where)) && !empty($params)) {
            $val = $params[0];
            if (is_numeric($val)) {
                return (int) $val;
            }
        }
        return null;
    }

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