<?php

declare(strict_types=1);

final class MyronDatabaseConfig
{
    public function __construct(
        public readonly string $driver,
        public readonly string $appEnv,
        public readonly string $host,
        public readonly string $port,
        public readonly string $socket,
        public readonly string $database,
        public readonly string $username,
        public readonly string $password,
        public readonly string $charset,
        public readonly string $sqlitePath,
    ) {
    }

    public static function fromEnvironment(string $appRoot): self
    {
        $driver = strtolower(trim((string) (getenv('MYRON_DB_DRIVER') ?: 'mysql')));
        if ($driver === '') {
            $driver = 'mysql';
        }

        return new self(
            $driver,
            (string) (getenv('MYRON_APP_ENV') ?: 'local'),
            (string) (getenv('MYRON_DB_HOST') ?: '127.0.0.1'),
            (string) (getenv('MYRON_DB_PORT') ?: '3306'),
            (string) (getenv('MYRON_DB_SOCKET') ?: ''),
            (string) (getenv('MYRON_DB_NAME') ?: 'myroncms'),
            (string) (getenv('MYRON_DB_USER') ?: ''),
            (string) (getenv('MYRON_DB_PASS') ?: ''),
            (string) (getenv('MYRON_DB_CHARSET') ?: 'utf8mb4'),
            (string) (getenv('MYRON_DB_SQLITE_PATH') ?: ($appRoot . '/storage/myroncms.sqlite')),
        );
    }

    public function isLocal(): bool
    {
        return $this->appEnv === 'local';
    }

    public function dsn(?string $databaseOverride = null): string
    {
        if ($this->driver === 'sqlite') {
            return 'sqlite:' . ($databaseOverride ?? $this->sqlitePath);
        }

        $database = $databaseOverride ?? $this->database;
        if ($this->socket !== '') {
            return sprintf(
                'mysql:unix_socket=%s;dbname=%s;charset=%s',
                $this->socket,
                $database,
                $this->charset
            );
        }

        return sprintf(
            'mysql:host=%s;port=%s;dbname=%s;charset=%s',
            $this->host,
            $this->port,
            $database,
            $this->charset
        );
    }

    public function serverDsn(): string
    {
        if ($this->driver === 'sqlite') {
            return $this->dsn();
        }

        if ($this->socket !== '') {
            return sprintf('mysql:unix_socket=%s;charset=%s', $this->socket, $this->charset);
        }

        return sprintf(
            'mysql:host=%s;port=%s;charset=%s',
            $this->host,
            $this->port,
            $this->charset
        );
    }
}

final class MyronDatabase
{
    private ?PDO $pdo = null;

    public function __construct(
        private readonly MyronDatabaseConfig $config,
    ) {
    }

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

        try {
            $this->pdo = $this->createConnection($this->config->dsn());
        } catch (PDOException $exception) {
            if ($this->shouldCreateDatabase($exception)) {
                $this->createDatabase();
                $this->pdo = $this->createConnection($this->config->dsn());
            } else {
                throw $exception;
            }
        }

        if ($this->config->driver === 'sqlite') {
            $statement = $this->pdo->prepare('PRAGMA foreign_keys = ON');
            if ($statement === false) {
                throw new RuntimeException('Failed to prepare SQLite foreign key pragma.');
            }

            $statement->execute();
        }

        return $this->pdo;
    }

    private function createConnection(string $dsn): PDO
    {
        if ($this->config->driver === 'sqlite') {
            $directory = dirname($this->config->sqlitePath);
            if (!is_dir($directory)) {
                mkdir($directory, 0775, true);
            }
        }

        return new PDO(
            $dsn,
            $this->config->driver === 'sqlite' ? null : $this->config->username,
            $this->config->driver === 'sqlite' ? null : $this->config->password,
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            ]
        );
    }

    private function shouldCreateDatabase(PDOException $exception): bool
    {
        if ($this->config->driver !== 'mysql' || !$this->config->isLocal()) {
            return false;
        }

        $info = $exception->errorInfo;
        if (is_array($info) && isset($info[1]) && (int) $info[1] === 1049) {
            return true;
        }

        return str_contains(strtolower($exception->getMessage()), 'unknown database');
    }

    private function createDatabase(): void
    {
        $pdo = new PDO(
            $this->config->serverDsn(),
            $this->config->username,
            $this->config->password,
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            ]
        );

        $database = str_replace('`', '``', $this->config->database);
        $statement = $pdo->prepare(sprintf(
            'CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET %s COLLATE %s_unicode_ci',
            $database,
            $this->config->charset,
            $this->config->charset
        ));

        if ($statement === false) {
            throw new RuntimeException('Failed to prepare database creation statement.');
        }

        $statement->execute();
    }
}
