<?php

declare(strict_types=1);

namespace App\Framework\Database\Driver\Optimization;

use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Exception\DatabaseException;

/**
 * PostgreSQL-specific database optimizations
 */
final readonly class PostgreSQLOptimizer implements DatabaseOptimizer
{
    public function __construct(
        private ConnectionInterface $connection,
        private ?string $schema = 'public'
    ) {
        // Verify this is a PostgreSQL connection
        $driver = $this->connection->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
        if ($driver !== 'pgsql') {
            throw new DatabaseException("PostgreSQLOptimizer requires a PostgreSQL connection, got {$driver}");
        }
    }

    /**
     * Optimize a table or all tables in the database
     * In PostgreSQL, this is equivalent to VACUUM FULL ANALYZE
     *
     * @param string|null $table The table to optimize, or null for all tables
     * @return array<string, string> Optimization results by table
     */
    public function optimizeTables(?string $table = null): array
    {
        $results = [];

        if ($table !== null) {
            // Optimize a specific table
            $sql = "VACUUM FULL ANALYZE " . $this->quoteIdentifier($table);
            $this->connection->execute($sql);
            $results[$table] = 'OK';
        } else {
            // Get all tables
            $tables = $this->getAllTables();

            // Optimize each table
            foreach ($tables as $tableName) {
                $sql = "VACUUM FULL ANALYZE " . $this->quoteIdentifier($tableName);
                $this->connection->execute($sql);
                $results[$tableName] = 'OK';
            }
        }

        return $results;
    }

    /**
     * Analyze a table or all tables in the database
     *
     * @param string|null $table The table to analyze, or null for all tables
     * @return array<string, string> Analysis results by table
     */
    public function analyzeTables(?string $table = null): array
    {
        $results = [];

        if ($table !== null) {
            // Analyze a specific table
            $sql = "ANALYZE " . $this->quoteIdentifier($table);
            $this->connection->execute($sql);
            $results[$table] = 'OK';
        } else {
            // Get all tables
            $tables = $this->getAllTables();

            // Analyze each table
            foreach ($tables as $tableName) {
                $sql = "ANALYZE " . $this->quoteIdentifier($tableName);
                $this->connection->execute($sql);
                $results[$tableName] = 'OK';
            }
        }

        return $results;
    }

    /**
     * Check a table or all tables in the database
     * In PostgreSQL, we use pg_stat_user_tables to get table statistics
     *
     * @param string|null $table The table to check, or null for all tables
     * @param bool $extended Whether to perform an extended check (not used in PostgreSQL)
     * @return array<string, string> Check results by table
     */
    public function checkTables(?string $table = null, bool $extended = false): array
    {
        $results = [];
        $schema = $this->schema ?? 'public';

        $sql = "SELECT relname as table_name,
                       n_live_tup as row_count,
                       n_dead_tup as dead_rows,
                       last_vacuum,
                       last_analyze
                FROM pg_stat_user_tables
                WHERE schemaname = ?";

        $params = [$schema];

        if ($table !== null) {
            $sql .= " AND relname = ?";
            $params[] = $table;
        }

        $rows = $this->connection->query($sql, $params)->fetchAll();

        foreach ($rows as $row) {
            $tableName = $row['table_name'];
            $deadRowPercentage = $row['row_count'] > 0
                ? round(($row['dead_rows'] / $row['row_count']) * 100, 2)
                : 0;

            $status = 'OK';

            // If more than 20% dead rows, suggest VACUUM
            if ($deadRowPercentage > 20) {
                $status = "Warning: {$deadRowPercentage}% dead rows, consider running VACUUM";
            }

            $results[$tableName] = $status;
        }

        return $results;
    }

    /**
     * Repair tables - not directly supported in PostgreSQL
     * This is a no-op method that returns a message indicating that repair is not needed
     *
     * @param string|null $table The table to repair, or null for all tables
     * @return array<string, string> Repair results by table
     */
    public function repairTables(?string $table = null): array
    {
        $results = [];

        if ($table !== null) {
            $results[$table] = 'PostgreSQL tables do not need repair; use VACUUM for maintenance';
        } else {
            // Get all tables
            $tables = $this->getAllTables();

            // Add message for each table
            foreach ($tables as $tableName) {
                $results[$tableName] = 'PostgreSQL tables do not need repair; use VACUUM for maintenance';
            }
        }

        return $results;
    }

    /**
     * Get table status information
     *
     * @param string|null $table The table to get status for, or null for all tables
     * @return array<string, array<string, mixed>> Table status by table
     */
    public function getTableStatus(?string $table = null): array
    {
        $results = [];
        $schema = $this->schema ?? 'public';

        $sql = "SELECT t.relname as table_name,
                       pg_size_pretty(pg_total_relation_size(t.oid)) as total_size,
                       pg_size_pretty(pg_relation_size(t.oid)) as table_size,
                       pg_size_pretty(pg_total_relation_size(t.oid) - pg_relation_size(t.oid)) as index_size,
                       s.n_live_tup as row_count,
                       s.n_dead_tup as dead_rows,
                       s.last_vacuum,
                       s.last_analyze,
                       s.vacuum_count,
                       s.analyze_count
                FROM pg_class t
                JOIN pg_stat_user_tables s ON t.relname = s.relname
                WHERE t.relkind = 'r'
                AND s.schemaname = ?";

        $params = [$schema];

        if ($table !== null) {
            $sql .= " AND t.relname = ?";
            $params[] = $table;
        }

        $rows = $this->connection->query($sql, $params)->fetchAll();

        foreach ($rows as $row) {
            $tableName = $row['table_name'];
            $results[$tableName] = $row;
        }

        return $results;
    }

    /**
     * Get index statistics
     *
     * @param string|null $table The table to get index statistics for, or null for all tables
     * @return array<string, array<string, array<string, mixed>>> Index statistics by table and index
     */
    public function getIndexStatistics(?string $table = null): array
    {
        $results = [];
        $schema = $this->schema ?? 'public';

        if ($table !== null) {
            // Get index statistics for a specific table
            $results[$table] = $this->getTableIndexStatistics($table);
        } else {
            // Get all tables
            $sql = "SELECT tablename FROM pg_tables WHERE schemaname = ?";
            $tables = $this->connection->query($sql, [$schema])->fetchAll(\PDO::FETCH_COLUMN);

            // Get index statistics for each table
            foreach ($tables as $tableName) {
                $results[$tableName] = $this->getTableIndexStatistics($tableName);
            }
        }

        return $results;
    }

    /**
     * Get database cache statistics
     *
     * @return array<string, mixed> Cache statistics
     */
    public function getCacheStatistics(): array
    {
        $sql = "SELECT
                    sum(heap_blks_read) as heap_read,
                    sum(heap_blks_hit) as heap_hit,
                    sum(idx_blks_read) as idx_read,
                    sum(idx_blks_hit) as idx_hit,
                    sum(toast_blks_read) as toast_read,
                    sum(toast_blks_hit) as toast_hit,
                    sum(tidx_blks_read) as tidx_read,
                    sum(tidx_blks_hit) as tidx_hit
                FROM pg_statio_user_tables";

        $row = $this->connection->queryOne($sql);

        if (! $row) {
            return [];
        }

        // Calculate hit ratios
        $heapHitRatio = ($row['heap_hit'] + $row['heap_read']) > 0
            ? round($row['heap_hit'] * 100 / ($row['heap_hit'] + $row['heap_read']), 2)
            : 0;

        $idxHitRatio = ($row['idx_hit'] + $row['idx_read']) > 0
            ? round($row['idx_hit'] * 100 / ($row['idx_hit'] + $row['idx_read']), 2)
            : 0;

        $toastHitRatio = ($row['toast_hit'] + $row['toast_read']) > 0
            ? round($row['toast_hit'] * 100 / ($row['toast_hit'] + $row['toast_read']), 2)
            : 0;

        $tidxHitRatio = ($row['tidx_hit'] + $row['tidx_read']) > 0
            ? round($row['tidx_hit'] * 100 / ($row['tidx_hit'] + $row['tidx_read']), 2)
            : 0;

        return [
            'heap_read' => $row['heap_read'],
            'heap_hit' => $row['heap_hit'],
            'heap_hit_ratio' => $heapHitRatio,
            'idx_read' => $row['idx_read'],
            'idx_hit' => $row['idx_hit'],
            'idx_hit_ratio' => $idxHitRatio,
            'toast_read' => $row['toast_read'],
            'toast_hit' => $row['toast_hit'],
            'toast_hit_ratio' => $toastHitRatio,
            'tidx_read' => $row['tidx_read'],
            'tidx_hit' => $row['tidx_hit'],
            'tidx_hit_ratio' => $tidxHitRatio,
        ];
    }

    /**
     * Get database settings
     *
     * @param string|null $pattern Pattern to filter settings
     * @return array<string, mixed> Database settings
     */
    public function getDatabaseSettings(?string $pattern = null): array
    {
        $sql = "SELECT name, setting, unit, context, vartype, short_desc
                FROM pg_settings";

        $params = [];

        if ($pattern !== null) {
            $sql .= " WHERE name LIKE ?";
            $params[] = "%{$pattern}%";
        }

        $sql .= " ORDER BY name";

        $rows = $this->connection->query($sql, $params)->fetchAll();

        $results = [];
        foreach ($rows as $row) {
            $results[$row['name']] = [
                'value' => $row['setting'],
                'unit' => $row['unit'],
                'context' => $row['context'],
                'type' => $row['vartype'],
                'description' => $row['short_desc'],
            ];
        }

        return $results;
    }

    /**
     * Get long-running queries
     *
     * @param int $seconds Minimum query duration in seconds
     * @return array<int, array<string, mixed>> Long-running queries
     */
    public function getLongRunningQueries(int $seconds = 5): array
    {
        $sql = "SELECT pid,
                       usename,
                       application_name,
                       client_addr,
                       state,
                       wait_event_type,
                       wait_event,
                       query_start,
                       EXTRACT(EPOCH FROM (now() - query_start)) as duration,
                       query
                FROM pg_stat_activity
                WHERE state = 'active'
                AND EXTRACT(EPOCH FROM (now() - query_start)) > ?
                AND query NOT LIKE '%pg_stat_activity%'
                ORDER BY duration DESC";

        $rows = $this->connection->query($sql, [$seconds])->fetchAll();

        return $rows;
    }

    /**
     * Get all tables in the current schema
     *
     * @return array<string> List of table names
     */
    private function getAllTables(): array
    {
        $schema = $this->schema ?? 'public';
        $sql = "SELECT tablename FROM pg_tables WHERE schemaname = ?";

        return $this->connection->query($sql, [$schema])->fetchAll(\PDO::FETCH_COLUMN);
    }

    /**
     * Get index statistics for a specific table
     *
     * @param string $table The table to get index statistics for
     * @return array<string, array<string, mixed>> Index statistics by index
     */
    private function getTableIndexStatistics(string $table): array
    {
        $results = [];
        $schema = $this->schema ?? 'public';

        $sql = "SELECT
                    i.relname as index_name,
                    a.attname as column_name,
                    ix.indisunique as is_unique,
                    ix.indisprimary as is_primary,
                    am.amname as index_type,
                    pg_size_pretty(pg_relation_size(i.oid)) as index_size,
                    s.idx_scan as index_scans,
                    s.idx_tup_read as tuples_read,
                    s.idx_tup_fetch as tuples_fetched
                FROM pg_index ix
                JOIN pg_class i ON i.oid = ix.indexrelid
                JOIN pg_class t ON t.oid = ix.indrelid
                JOIN pg_am am ON i.relam = am.oid
                JOIN pg_namespace n ON n.oid = i.relnamespace
                JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
                LEFT JOIN pg_stat_user_indexes s ON s.indexrelid = i.oid
                WHERE t.relname = ?
                AND n.nspname = ?
                ORDER BY i.relname, a.attnum";

        $rows = $this->connection->query($sql, [$table, $schema])->fetchAll();

        // Group by index name
        $indexGroups = [];
        foreach ($rows as $row) {
            $indexName = $row['index_name'];
            if (! isset($indexGroups[$indexName])) {
                $indexGroups[$indexName] = [
                    'columns' => [],
                    'is_unique' => $row['is_unique'],
                    'is_primary' => $row['is_primary'],
                    'index_type' => $row['index_type'],
                    'index_size' => $row['index_size'],
                    'index_scans' => $row['index_scans'],
                    'tuples_read' => $row['tuples_read'],
                    'tuples_fetched' => $row['tuples_fetched'],
                ];
            }
            $indexGroups[$indexName]['columns'][] = $row['column_name'];
        }

        return $indexGroups;
    }

    /**
     * Quote an identifier (table or column name)
     *
     * @param string $identifier The identifier to quote
     * @return string The quoted identifier
     */
    private function quoteIdentifier(string $identifier): string
    {
        return '"' . str_replace('"', '""', $identifier) . '"';
    }
}
