<?php

declare(strict_types=1);

namespace WhmcsMigrate;

use PDO;
use PDOStatement;

final class Database
{
    private PDO $pdo;

    public function __construct(Config $config)
    {
        $host = $config->getRequired('EZSCALE_DB_HOST');
        $port = $config->get('EZSCALE_DB_PORT', '3306');
        $database = $config->getRequired('EZSCALE_DB_DATABASE');
        $username = $config->getRequired('EZSCALE_DB_USERNAME');
        $password = $config->get('EZSCALE_DB_PASSWORD', '');

        $dsn = "mysql:host={$host};port={$port};dbname={$database};charset=utf8mb4";

        $this->pdo = new PDO($dsn, $username, (string) $password, [
            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 utf8mb4 COLLATE utf8mb4_unicode_ci",
        ]);
    }

    /**
     * Get the underlying PDO instance.
     */
    public function getPdo(): PDO
    {
        return $this->pdo;
    }

    /**
     * Insert a row and return the last insert ID.
     */
    public function insert(string $table, array $data): int
    {
        $data = $this->ensureTimestamps($data);

        $columns = implode(', ', array_map(fn (string $col): string => "`{$col}`", array_keys($data)));
        $placeholders = implode(', ', array_map(fn (string $col): string => ":{$col}", array_keys($data)));

        $sql = "INSERT INTO `{$table}` ({$columns}) VALUES ({$placeholders})";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($data);

        return (int) $this->pdo->lastInsertId();
    }

    /**
     * Insert a row using INSERT IGNORE. Returns last insert ID (0 if ignored).
     */
    public function insertIgnore(string $table, array $data): int
    {
        $data = $this->ensureTimestamps($data);

        $columns = implode(', ', array_map(fn (string $col): string => "`{$col}`", array_keys($data)));
        $placeholders = implode(', ', array_map(fn (string $col): string => ":{$col}", array_keys($data)));

        $sql = "INSERT IGNORE INTO `{$table}` ({$columns}) VALUES ({$placeholders})";

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($data);

        return (int) $this->pdo->lastInsertId();
    }

    /**
     * Update rows matching the where clause. Returns affected row count.
     */
    public function update(string $table, array $data, array $where): int
    {
        $setParts = [];
        $params = [];

        foreach ($data as $column => $value) {
            $paramKey = "set_{$column}";
            $setParts[] = "`{$column}` = :{$paramKey}";
            $params[$paramKey] = $value;
        }

        $whereParts = [];
        foreach ($where as $column => $value) {
            $paramKey = "where_{$column}";
            $whereParts[] = "`{$column}` = :{$paramKey}";
            $params[$paramKey] = $value;
        }

        $sql = sprintf(
            'UPDATE `%s` SET %s WHERE %s',
            $table,
            implode(', ', $setParts),
            implode(' AND ', $whereParts),
        );

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);

        return $stmt->rowCount();
    }

    /**
     * Execute a SELECT query and return all rows.
     */
    public function query(string $sql, array $params = []): array
    {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);

        return $stmt->fetchAll();
    }

    /**
     * Execute a SELECT query and return a single row or null.
     */
    public function queryOne(string $sql, array $params = []): ?array
    {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);

        $row = $stmt->fetch();

        return $row !== false ? $row : null;
    }

    /**
     * Execute a raw SQL statement (UPDATE, DELETE, etc.) and return affected rows.
     */
    public function execute(string $sql, array $params = []): int
    {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);

        return $stmt->rowCount();
    }

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

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

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

    /**
     * Check if a row exists matching the given conditions.
     */
    public function tableHasRow(string $table, array $where): bool
    {
        $whereParts = [];
        $params = [];

        foreach ($where as $column => $value) {
            $whereParts[] = "`{$column}` = :{$column}";
            $params[$column] = $value;
        }

        $sql = sprintf(
            'SELECT 1 FROM `%s` WHERE %s LIMIT 1',
            $table,
            implode(' AND ', $whereParts),
        );

        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);

        return $stmt->fetch() !== false;
    }

    /**
     * Ensure created_at and updated_at are set if not already present.
     */
    private function ensureTimestamps(array $data): array
    {
        $now = date('Y-m-d H:i:s');

        if (! array_key_exists('created_at', $data)) {
            $data['created_at'] = $now;
        }

        if (! array_key_exists('updated_at', $data)) {
            $data['updated_at'] = $now;
        }

        return $data;
    }
}
