<?php

declare(strict_types=1);

namespace App\Framework\Database\Platform;

use App\Framework\Database\Platform\Enums\ColumnType;
use App\Framework\Database\Platform\Enums\DatabaseFeature;
use App\Framework\Database\Platform\Enums\IndexType;
use App\Framework\Database\Platform\ValueObjects\ColumnDefinition;
use App\Framework\Database\Platform\ValueObjects\IndexDefinition;
use App\Framework\Database\Platform\ValueObjects\TableOptions;

/**
 * MySQL/MariaDB platform implementation
 */
final readonly class MySQLPlatform implements DatabasePlatform
{
    private array $supportedFeatures;

    public function __construct()
    {
        $this->supportedFeatures = [
            DatabaseFeature::AUTO_INCREMENT->value => true,
            DatabaseFeature::FOREIGN_KEYS->value => true,
            DatabaseFeature::TRANSACTIONS->value => true,
            DatabaseFeature::SAVEPOINTS->value => true,
            DatabaseFeature::JSON_COLUMNS->value => true,
            DatabaseFeature::FULLTEXT_SEARCH->value => true,
            DatabaseFeature::SPATIAL_INDEXES->value => true,
            DatabaseFeature::PARTITIONING->value => true,
            DatabaseFeature::VIEWS->value => true,
            DatabaseFeature::STORED_PROCEDURES->value => true,
            DatabaseFeature::TRIGGERS->value => true,
            DatabaseFeature::UUID_GENERATION->value => false,
            DatabaseFeature::ULID_GENERATION->value => false,
            DatabaseFeature::CONCURRENT_INDEXES->value => false,
            DatabaseFeature::PARTIAL_INDEXES->value => false,
            DatabaseFeature::EXPRESSION_INDEXES->value => true,
            DatabaseFeature::RECURSIVE_CTE->value => true,
            DatabaseFeature::WINDOW_FUNCTIONS->value => true,
            DatabaseFeature::UPSERT->value => true,
            DatabaseFeature::RETURNING_CLAUSE->value => false,
        ];
    }

    public function getName(): string
    {
        return 'MySQL';
    }

    public function supportsFeature(string $feature): bool
    {
        return $this->supportedFeatures[$feature] ?? false;
    }

    public function getAutoIncrementSQL(): string
    {
        return 'AUTO_INCREMENT';
    }

    public function getColumnTypeSQL(string $type, array $options = []): string
    {
        $columnType = ColumnType::tryFrom($type);

        if (! $columnType) {
            throw new \InvalidArgumentException("Unknown column type: {$type}");
        }

        return match($columnType) {
            ColumnType::TINY_INTEGER => $this->buildNumericType('TINYINT', $options),
            ColumnType::SMALL_INTEGER => $this->buildNumericType('SMALLINT', $options),
            ColumnType::INTEGER => $this->buildNumericType('INT', $options),
            ColumnType::BIG_INTEGER => $this->buildNumericType('BIGINT', $options),
            ColumnType::DECIMAL => $this->buildDecimalType('DECIMAL', $options),
            ColumnType::FLOAT => $this->buildDecimalType('FLOAT', $options),
            ColumnType::DOUBLE => $this->buildDecimalType('DOUBLE', $options),
            ColumnType::BOOLEAN => 'BOOLEAN',

            ColumnType::CHAR => $this->buildStringType('CHAR', $options),
            ColumnType::VARCHAR => $this->buildStringType('VARCHAR', $options),
            ColumnType::TEXT => 'TEXT',
            ColumnType::MEDIUM_TEXT => 'MEDIUMTEXT',
            ColumnType::LONG_TEXT => 'LONGTEXT',

            ColumnType::BINARY => $this->buildBinaryType('BINARY', $options),
            ColumnType::VARBINARY => $this->buildBinaryType('VARBINARY', $options),
            ColumnType::BLOB => 'BLOB',
            ColumnType::MEDIUM_BLOB => 'MEDIUMBLOB',
            ColumnType::LONG_BLOB => 'LONGBLOB',

            ColumnType::DATE => 'DATE',
            ColumnType::TIME => 'TIME',
            ColumnType::DATETIME => 'DATETIME',
            ColumnType::TIMESTAMP => 'TIMESTAMP',
            ColumnType::YEAR => 'YEAR',

            ColumnType::JSON => 'JSON',
            ColumnType::UUID => 'BINARY(16)',
            ColumnType::ULID => 'BINARY(16)',
        };
    }

    public function getCreateTableSQL(string $table, array $columns, array|TableOptions $options = []): string
    {
        $tableOptions = $options instanceof TableOptions ? $options : TableOptions::default();

        $sql = [];

        // Table creation start
        if ($tableOptions->temporary) {
            $sql[] = 'CREATE TEMPORARY TABLE';
        } else {
            $sql[] = 'CREATE TABLE';
        }

        if ($tableOptions->ifNotExists) {
            $sql[] = 'IF NOT EXISTS';
        }

        $sql[] = $this->quoteIdentifier($table);

        // Column definitions
        $columnDefinitions = [];
        $indexes = [];

        foreach ($columns as $column) {
            if ($column instanceof ColumnDefinition) {
                $columnDefinitions[] = $this->buildColumnDefinition($column);

                // Auto-create primary key index
                if ($column->isPrimaryKey()) {
                    $indexes[] = IndexDefinition::primary('PRIMARY', [$column->name]);
                }

                // Auto-create unique index for columns with unique option
                $options = $column->getOptions();
                if ($options->unique) {
                    $indexName = 'UNQ_' . $table . '_' . $column->name;
                    $indexes[] = IndexDefinition::unique($indexName, [$column->name], $table);
                }
            } else {
                throw new \InvalidArgumentException('All columns must be ColumnDefinition instances');
            }
        }

        // Index definitions
        $indexDefinitions = [];
        foreach ($indexes as $index) {
            if ($index instanceof IndexDefinition) {
                $indexDefinitions[] = $this->buildIndexDefinition($index);
            }
        }

        // Combine column and index definitions
        $allDefinitions = array_merge($columnDefinitions, $indexDefinitions);
        $sql[] = '(' . implode(', ', $allDefinitions) . ')';

        // Table options
        $tableOptionsClauses = [];

        if ($tableOptions->engine) {
            $tableOptionsClauses[] = "ENGINE = {$tableOptions->engine}";
        }

        if ($tableOptions->charset) {
            $tableOptionsClauses[] = "DEFAULT CHARACTER SET = {$tableOptions->charset}";
        }

        if ($tableOptions->collation) {
            $tableOptionsClauses[] = "COLLATE = {$tableOptions->collation}";
        }

        if ($tableOptions->comment) {
            $tableOptionsClauses[] = "COMMENT = " . $this->escapeString($tableOptions->comment);
        }

        if ($tableOptions->autoIncrementStart !== null) {
            $tableOptionsClauses[] = "AUTO_INCREMENT = {$tableOptions->autoIncrementStart}";
        }

        if (! empty($tableOptionsClauses)) {
            $sql[] = implode(' ', $tableOptionsClauses);
        }

        return implode(' ', $sql);
    }

    public function getDropTableSQL(string $table, bool $ifExists = false): string
    {
        $sql = 'DROP TABLE';

        if ($ifExists) {
            $sql .= ' IF EXISTS';
        }

        $sql .= ' ' . $this->quoteIdentifier($table);

        return $sql;
    }

    public function getCreateIndexSQL(string $table, string $indexName, array $columns, array $options = []): string
    {
        $indexType = $options['type'] ?? IndexType::INDEX;
        if (is_string($indexType)) {
            $indexType = IndexType::from($indexType);
        }

        $index = new IndexDefinition($indexName, $indexType, $columns, $table, $options);

        return "CREATE {$indexType->getKeyword()} {$this->quoteIdentifier($indexName)} ON {$this->quoteIdentifier($table)} ({$this->quoteColumnList($columns)})";
    }

    public function getTableExistsSQL(string $table): string
    {
        return "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = " . $this->escapeString($table);
    }

    public function getListTablesSQL(): string
    {
        return "SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name";
    }

    public function quoteIdentifier(string $identifier): string
    {
        return '`' . str_replace('`', '``', $identifier) . '`';
    }

    public function getCurrentTimestampSQL(): string
    {
        return 'CURRENT_TIMESTAMP';
    }

    public function getBinaryUuidSQL(): string
    {
        // MySQL doesn't have built-in UUID generation, use application-level UUID generation
        return 'UNHEX(REPLACE(UUID(), \'-\', \'\'))';
    }

    private function buildColumnDefinition(ColumnDefinition $column): string
    {
        $parts = [];

        // Column name and type
        $parts[] = $this->quoteIdentifier($column->name);
        $parts[] = $this->getColumnTypeSQL($column->type->value, [
            'length' => $column->length,
            'precision' => $column->precision,
            'scale' => $column->scale,
            'unsigned' => $column->unsigned,
        ]);

        // Character set and collation for text columns
        if ($column->type->isTextual() && $column->type !== ColumnType::JSON) {
            if ($column->charset !== 'utf8mb4') {
                $parts[] = "CHARACTER SET {$column->charset}";
            }
            if ($column->collation !== 'utf8mb4_unicode_ci') {
                $parts[] = "COLLATE {$column->collation}";
            }
        }

        // Nullability
        $parts[] = $column->nullable ? 'NULL' : 'NOT NULL';

        // Auto increment
        if ($column->autoIncrement) {
            $parts[] = 'AUTO_INCREMENT';
        }

        // Default value
        if ($column->hasDefault()) {
            if (is_string($column->default) && ! in_array($column->default, ['CURRENT_TIMESTAMP', 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'])) {
                $parts[] = 'DEFAULT ' . $this->escapeString($column->default);
            } else {
                $parts[] = "DEFAULT {$column->default}";
            }
        }

        // Comment
        if ($column->comment) {
            $parts[] = 'COMMENT ' . $this->escapeString($column->comment);
        }

        return implode(' ', $parts);
    }

    private function buildIndexDefinition(IndexDefinition $index): string
    {
        $parts = [];

        // Index type keyword
        $parts[] = $index->type->getKeyword();

        // Index name (except for PRIMARY KEY)
        if ($index->type !== IndexType::PRIMARY) {
            $parts[] = $this->quoteIdentifier($index->name);
        }

        // Column list
        $parts[] = '(' . $this->quoteColumnList($index->columns) . ')';

        return implode(' ', $parts);
    }

    private function buildNumericType(string $baseType, array $options): string
    {
        $type = $baseType;

        if (! empty($options['length'])) {
            $type .= "({$options['length']})";
        }

        if (! empty($options['unsigned'])) {
            $type .= ' UNSIGNED';
        }

        return $type;
    }

    private function buildDecimalType(string $baseType, array $options): string
    {
        $type = $baseType;

        if (! empty($options['precision']) && ! empty($options['scale'])) {
            $type .= "({$options['precision']},{$options['scale']})";
        } elseif (! empty($options['precision'])) {
            $type .= "({$options['precision']})";
        }

        if (! empty($options['unsigned'])) {
            $type .= ' UNSIGNED';
        }

        return $type;
    }

    private function buildStringType(string $baseType, array $options): string
    {
        $type = $baseType;

        if (! empty($options['length'])) {
            $type .= "({$options['length']})";
        } else {
            $type .= '(255)'; // Default length
        }

        return $type;
    }

    private function buildBinaryType(string $baseType, array $options): string
    {
        $type = $baseType;

        if (! empty($options['length'])) {
            $type .= "({$options['length']})";
        }

        return $type;
    }

    private function quoteColumnList(array $columns): string
    {
        return implode(', ', array_map([$this, 'quoteIdentifier'], $columns));
    }

    private function escapeString(string $value): string
    {
        return "'" . str_replace("'", "''", $value) . "'";
    }
}
