<?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;
use App\Framework\DI\Attributes\DefaultImplementation;

/**
 * PostgreSQL platform implementation with native features
 *
 * Supports PostgreSQL-specific features:
 * - SERIAL/BIGSERIAL for auto-increment
 * - JSONB for optimized JSON storage
 * - Native UUID generation
 * - Advanced indexing (GiST, GIN, BRIN)
 * - RETURNING clause
 * - Partial and concurrent indexes
 */

final readonly class PostgreSQLPlatform implements DatabasePlatform
{
    private array $supportedFeatures;

    public function __construct()
    {
        $this->supportedFeatures = [
            DatabaseFeature::AUTO_INCREMENT->value => true,  // via SERIAL
            DatabaseFeature::FOREIGN_KEYS->value => true,
            DatabaseFeature::TRANSACTIONS->value => true,
            DatabaseFeature::SAVEPOINTS->value => true,
            DatabaseFeature::JSON_COLUMNS->value => true,     // JSONB
            DatabaseFeature::FULLTEXT_SEARCH->value => true,  // via tsvector
            DatabaseFeature::SPATIAL_INDEXES->value => true,  // PostGIS
            DatabaseFeature::PARTITIONING->value => true,     // Native partitioning
            DatabaseFeature::VIEWS->value => true,
            DatabaseFeature::STORED_PROCEDURES->value => true,
            DatabaseFeature::TRIGGERS->value => true,
            DatabaseFeature::UUID_GENERATION->value => true,  // uuid-ossp extension
            DatabaseFeature::ULID_GENERATION->value => false, // App-level
            DatabaseFeature::CONCURRENT_INDEXES->value => true,  // CREATE INDEX CONCURRENTLY
            DatabaseFeature::PARTIAL_INDEXES->value => true,  // WHERE clause
            DatabaseFeature::EXPRESSION_INDEXES->value => true,
            DatabaseFeature::RECURSIVE_CTE->value => true,
            DatabaseFeature::WINDOW_FUNCTIONS->value => true,
            DatabaseFeature::UPSERT->value => true,           // ON CONFLICT
            DatabaseFeature::RETURNING_CLAUSE->value => true, // RETURNING *
        ];
    }

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

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

    public function getAutoIncrementSQL(): string
    {
        // PostgreSQL uses SERIAL type instead of AUTO_INCREMENT
        return 'GENERATED BY DEFAULT AS IDENTITY';
    }

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

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

        return match($columnType) {
            // Integer types
            ColumnType::TINY_INTEGER => 'SMALLINT',      // PostgreSQL: no TINYINT
            ColumnType::SMALL_INTEGER => 'SMALLINT',
            ColumnType::INTEGER => 'INTEGER',
            ColumnType::BIG_INTEGER => 'BIGINT',

            // Decimal types
            ColumnType::DECIMAL => $this->buildDecimalType('NUMERIC', $options),
            ColumnType::FLOAT => 'REAL',
            ColumnType::DOUBLE => 'DOUBLE PRECISION',
            ColumnType::BOOLEAN => 'BOOLEAN',

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

            // Binary types
            ColumnType::BINARY => $this->buildBinaryType('BYTEA', $options),
            ColumnType::VARBINARY => 'BYTEA',
            ColumnType::BLOB => 'BYTEA',
            ColumnType::MEDIUM_BLOB => 'BYTEA',
            ColumnType::LONG_BLOB => 'BYTEA',

            // Date/Time types
            ColumnType::DATE => 'DATE',
            ColumnType::TIME => 'TIME',
            ColumnType::DATETIME => 'TIMESTAMP',         // PostgreSQL: TIMESTAMP instead of DATETIME
            ColumnType::TIMESTAMP => 'TIMESTAMP',
            ColumnType::YEAR => 'SMALLINT',              // PostgreSQL: no YEAR type

            // Special types
            ColumnType::JSON => 'JSONB',                 // JSONB for better performance
            ColumnType::UUID => 'UUID',                  // Native UUID type
            ColumnType::ULID => 'BYTEA',                 // Store as binary
        };
    }

    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 constraint
                if ($column->isPrimaryKey()) {
                    $indexes[] = IndexDefinition::primary('PRIMARY', [$column->name]);
                }

                // Auto-create unique constraint for columns with unique option
                $colOptions = $column->getOptions();
                if ($colOptions->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 (constraints in PostgreSQL)
        $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) . ')';

        // PostgreSQL-specific table options
        // Note: PostgreSQL doesn't support ENGINE or CHARSET table options like MySQL
        // Encoding is set at database level, not table level

        return implode(' ', $sql);
    }

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

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

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

        // PostgreSQL: CASCADE option to drop dependent objects
        // $sql .= ' CASCADE';  // Optional, use with caution

        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);
        }

        $concurrent = $options['concurrent'] ?? false;
        $unique = $indexType === IndexType::UNIQUE;

        $sql = 'CREATE';

        if ($unique) {
            $sql .= ' UNIQUE';
        }

        $sql .= ' INDEX';

        if ($concurrent) {
            $sql .= ' CONCURRENTLY';
        }

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

        // Index method (BTREE, HASH, GIN, GIST, BRIN)
        $method = $options['method'] ?? 'BTREE';
        $sql .= ' USING ' . $method;

        $sql .= ' (' . $this->quoteColumnList($columns) . ')';

        // WHERE clause for partial indexes
        if (isset($options['where'])) {
            $sql .= ' WHERE ' . $options['where'];
        }

        return $sql;
    }

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

    public function getListTablesSQL(): string
    {
        return "SELECT tablename FROM pg_catalog.pg_tables
                WHERE schemaname = 'public'
                ORDER BY tablename";
    }

    public function quoteIdentifier(string $identifier): string
    {
        // PostgreSQL uses double quotes for identifiers
        return '"' . str_replace('"', '""', $identifier) . '"';
    }

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

    public function getBinaryUuidSQL(): string
    {
        // PostgreSQL has native UUID generation via uuid-ossp extension
        return 'uuid_generate_v4()';
    }

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

        // Column name and type
        $parts[] = $this->quoteIdentifier($column->name);

        // Handle auto-increment with SERIAL types
        if ($column->autoIncrement) {
            $serialType = match($column->type) {
                ColumnType::SMALL_INTEGER => 'SMALLSERIAL',
                ColumnType::INTEGER => 'SERIAL',
                ColumnType::BIG_INTEGER => 'BIGSERIAL',
                default => throw new \InvalidArgumentException('Auto-increment only supported for integer types')
            };
            $parts[] = $serialType;
        } else {
            $parts[] = $this->getColumnTypeSQL($column->type->value, [
                'length' => $column->length,
                'precision' => $column->precision,
                'scale' => $column->scale,
            ]);
        }

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

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

        // PostgreSQL doesn't support column comments in CREATE TABLE directly
        // Use COMMENT ON COLUMN separately if needed

        return implode(' ', $parts);
    }

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

        // Constraint type
        if ($index->type === IndexType::PRIMARY) {
            $parts[] = 'PRIMARY KEY';
        } elseif ($index->type === IndexType::UNIQUE) {
            $parts[] = 'CONSTRAINT ' . $this->quoteIdentifier($index->name);
            $parts[] = 'UNIQUE';
        } else {
            // Regular indexes are created separately in PostgreSQL
            return '';
        }

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

        return implode(' ', $parts);
    }

    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']})";
        }

        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
    {
        // PostgreSQL BYTEA doesn't need length specification
        return $baseType;
    }

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

    private function escapeString(string $value): string
    {
        // PostgreSQL uses single quotes and doubles them for escaping
        return "'" . str_replace("'", "''", $value) . "'";
    }
}
