<?php

declare(strict_types=1);

namespace Budget;

use PDO;
use PDOException;
use RuntimeException;

final class Database
{
    private PDO $pdo;
    private array $config;
    private string $configPath;

    public function __construct(string $configPath, string $rootPath)
    {
        $this->configPath = $configPath;
        $this->config = $this->readConfig($configPath);
        $sqliteOverride = getenv('BUDGET_SQLITE_PATH');
        if (is_string($sqliteOverride) && $sqliteOverride !== '') {
            $this->config['driver'] = 'sqlite';
            $this->config['sqlite_path'] = $sqliteOverride;
        }
        $driver = (string) ($this->config['driver'] ?? 'sqlite');

        if ($driver === 'mysql') {
            $mysql = $this->config['mysql'] ?? [];
            $dsn = sprintf(
                'mysql:host=%s;port=%d;dbname=%s;charset=%s',
                $mysql['host'] ?? '127.0.0.1',
                (int) ($mysql['port'] ?? 3306),
                $mysql['database'] ?? 'budget_planner',
                $mysql['charset'] ?? 'utf8mb4'
            );
            $this->pdo = new PDO($dsn, (string) ($mysql['username'] ?? ''), (string) ($mysql['password'] ?? ''), self::options());
        } else {
            $path = (string) ($this->config['sqlite_path'] ?? 'storage/database/budget.sqlite');
            $absolutePath = str_starts_with($path, '/') ? $path : $rootPath . '/' . $path;
            $directory = dirname($absolutePath);
            if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
                throw new RuntimeException('Unable to create SQLite storage directory.');
            }
            $this->pdo = new PDO('sqlite:' . $absolutePath, null, null, self::options());
            $this->pdo->exec('PRAGMA foreign_keys = ON');
            $this->pdo->exec('PRAGMA journal_mode = WAL');
        }

        $this->migrate();
    }

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

    public function driver(): string
    {
        return (string) ($this->config['driver'] ?? 'sqlite');
    }

    public function config(): array
    {
        return $this->config;
    }

    public function migrate(): void
    {
        foreach (Schema::statements($this->driver()) as $statement) {
            $this->pdo->exec($statement);
        }
    }

    public function migrateSqliteToMysql(array $mysqlConfig, string $rootPath): int
    {
        if ($this->driver() !== 'sqlite') {
            throw new RuntimeException('Automatic migration can only start while SQLite mode is active.');
        }

        $dsn = sprintf(
            'mysql:host=%s;port=%d;dbname=%s;charset=%s',
            $mysqlConfig['host'],
            (int) $mysqlConfig['port'],
            $mysqlConfig['database'],
            $mysqlConfig['charset'] ?? 'utf8mb4'
        );
        $target = new PDO($dsn, (string) $mysqlConfig['username'], (string) $mysqlConfig['password'], self::options());
        foreach (Schema::statements('mysql') as $statement) {
            $target->exec($statement);
        }

        $tables = [
            'settings', 'users', 'monthly_plans', 'income_entries', 'categories',
            'budget_items', 'transactions', 'dead_account_entries', 'calendar_events',
            'alert_logs', 'visits', 'audit_logs',
        ];
        $copied = 0;
        $target->beginTransaction();
        try {
            $target->exec('SET FOREIGN_KEY_CHECKS=0');
            foreach ($tables as $table) {
                $rows = $this->pdo->query('SELECT * FROM ' . $table)->fetchAll();
                if ($rows === []) {
                    continue;
                }
                $columns = array_keys($rows[0]);
                $quoted = array_map(static fn (string $column): string => '`' . $column . '`', $columns);
                $placeholders = array_map(static fn (string $column): string => ':' . $column, $columns);
                $sql = sprintf(
                    'REPLACE INTO `%s` (%s) VALUES (%s)',
                    $table,
                    implode(', ', $quoted),
                    implode(', ', $placeholders)
                );
                $insert = $target->prepare($sql);
                foreach ($rows as $row) {
                    $insert->execute($row);
                    $copied++;
                }
            }
            $target->exec('SET FOREIGN_KEY_CHECKS=1');
            $target->commit();
        } catch (\Throwable $exception) {
            if ($target->inTransaction()) {
                $target->rollBack();
            }
            throw $exception;
        }

        $newConfig = [
            'driver' => 'mysql',
            'sqlite_path' => $this->config['sqlite_path'] ?? 'storage/database/budget.sqlite',
            'mysql' => [
                'host' => $mysqlConfig['host'],
                'port' => (int) $mysqlConfig['port'],
                'database' => $mysqlConfig['database'],
                'username' => $mysqlConfig['username'],
                'password' => $mysqlConfig['password'],
                'charset' => $mysqlConfig['charset'] ?? 'utf8mb4',
            ],
        ];
        $encoded = json_encode($newConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        if ($encoded === false || file_put_contents($this->configPath, $encoded . PHP_EOL, LOCK_EX) === false) {
            throw new RuntimeException('Data copied, but config/database.json could not be updated.');
        }

        return $copied;
    }

    private static function options(): array
    {
        return [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ];
    }

    private function readConfig(string $path): array
    {
        $raw = @file_get_contents($path);
        $config = $raw === false ? null : json_decode($raw, true);
        if (!is_array($config)) {
            throw new RuntimeException('Database configuration is missing or invalid.');
        }
        return $config;
    }
}
