<?php

declare(strict_types=1);

namespace App\Framework\Database\Monitoring\Health;

use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\Driver\Optimization\MySQLOptimizer;
use App\Framework\Database\Driver\Optimization\PostgreSQLOptimizer;
use App\Framework\Database\Driver\Optimization\SQLiteOptimizer;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\DateTime\Clock;

/**
 * Service for checking database health
 */
final readonly class DatabaseHealthChecker
{
    public function __construct(
        private DatabaseManager $databaseManager,
        private Clock $clock
    ) {
    }

    /**
     * Run all health checks for a specific connection
     *
     * @param string $connection The connection name
     * @return HealthCheckResult The health check result
     */
    public function checkHealth(string $connection = 'default'): HealthCheckResult
    {
        $result = new HealthCheckResult($connection);
        $conn = $this->databaseManager->getConnection($connection);

        try {
            // Check connection
            $result->addCheck(
                'connection',
                'Database Connection',
                $this->checkConnection($conn),
                'Checks if the database connection is working'
            );

            // Check read/write
            $result->addCheck(
                'read_write',
                'Read/Write Access',
                $this->checkReadWrite($conn),
                'Checks if the database can be read from and written to'
            );

            // Check database size
            $result->addCheck(
                'database_size',
                'Database Size',
                $this->checkDatabaseSize($conn),
                'Checks if the database size is within acceptable limits'
            );

            // Check table count
            $result->addCheck(
                'table_count',
                'Table Count',
                $this->checkTableCount($conn),
                'Checks if the number of tables is within acceptable limits'
            );

            // Check for long-running queries
            $result->addCheck(
                'long_running_queries',
                'Long-Running Queries',
                $this->checkLongRunningQueries($conn),
                'Checks for queries that have been running for a long time'
            );

            // Check for database-specific issues
            $driver = $conn->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);

            switch ($driver) {
                case 'mysql':
                    $this->checkMySQLHealth($conn, $result);

                    break;

                case 'pgsql':
                    $this->checkPostgreSQLHealth($conn, $result);

                    break;

                case 'sqlite':
                    $this->checkSQLiteHealth($conn, $result);

                    break;
            }

        } catch (\Throwable $e) {
            $result->addError('general', 'General Error', $e->getMessage());
        }

        return $result;
    }

    /**
     * Check if the database connection is working
     */
    private function checkConnection(ConnectionInterface $connection): HealthCheckStatus
    {
        try {
            $connection->queryScalar(SqlQuery::create('SELECT 1'));

            return HealthCheckStatus::ok('Connection successful');
        } catch (\Throwable $e) {
            return HealthCheckStatus::error('Connection failed: ' . $e->getMessage());
        }
    }

    /**
     * Check if the database can be read from and written to
     */
    private function checkReadWrite(ConnectionInterface $connection): HealthCheckStatus
    {
        try {
            // Create a temporary test table
            $timestamp = $this->clock->now()->getTimestamp();
            $tableName = "health_check_{$timestamp}";

            // Create table
            $connection->execute(SqlQuery::create("CREATE TEMPORARY TABLE {$tableName} (id INT, value VARCHAR(50))"));

            // Write data
            $connection->execute(SqlQuery::create("INSERT INTO {$tableName} (id, value) VALUES (1, 'test')"));

            // Read data
            $result = $connection->queryScalar(SqlQuery::create("SELECT value FROM {$tableName} WHERE id = 1"));

            // Drop table
            $connection->execute(SqlQuery::create("DROP TABLE {$tableName}"));

            if ($result === 'test') {
                return HealthCheckStatus::ok('Read/write operations successful');
            } else {
                return HealthCheckStatus::warning('Read operation returned unexpected result: ' . $result);
            }
        } catch (\Throwable $e) {
            return HealthCheckStatus::error('Read/write check failed: ' . $e->getMessage());
        }
    }

    /**
     * Check if the database size is within acceptable limits
     */
    private function checkDatabaseSize(ConnectionInterface $connection): HealthCheckStatus
    {
        try {
            $driver = $connection->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
            $size = 0;
            $sizeFormatted = '';

            switch ($driver) {
                case 'mysql':
                    $result = $connection->queryOne(SqlQuery::create("
                        SELECT
                            SUM(data_length + index_length) as size
                        FROM
                            information_schema.tables
                        WHERE
                            table_schema = DATABASE()
                    "));
                    $size = (int)($result['size'] ?? 0);
                    $sizeFormatted = $this->formatBytes($size);

                    break;

                case 'pgsql':
                    $result = $connection->queryOne("
                        SELECT pg_database_size(current_database()) as size
                    ");
                    $size = (int)($result['size'] ?? 0);
                    $sizeFormatted = $this->formatBytes($size);

                    break;

                case 'sqlite':
                    $optimizer = new SQLiteOptimizer($connection);
                    $stats = $optimizer->getDatabaseStatistics();
                    $size = $stats['database_size'] ?? 0;
                    $sizeFormatted = $stats['database_size_human'] ?? $this->formatBytes($size);

                    break;

                default:
                    return HealthCheckStatus::warning('Unable to determine database size for driver: ' . $driver);
            }

            // Check size thresholds
            $warningThreshold = 1024 * 1024 * 1024; // 1 GB
            $errorThreshold = 5 * 1024 * 1024 * 1024; // 5 GB

            if ($size > $errorThreshold) {
                return HealthCheckStatus::error("Database size ({$sizeFormatted}) exceeds error threshold (5 GB)");
            } elseif ($size > $warningThreshold) {
                return HealthCheckStatus::warning("Database size ({$sizeFormatted}) exceeds warning threshold (1 GB)");
            } else {
                return HealthCheckStatus::ok("Database size: {$sizeFormatted}");
            }
        } catch (\Throwable $e) {
            return HealthCheckStatus::warning('Unable to check database size: ' . $e->getMessage());
        }
    }

    /**
     * Check if the number of tables is within acceptable limits
     */
    private function checkTableCount(ConnectionInterface $connection): HealthCheckStatus
    {
        try {
            $driver = $connection->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
            $count = 0;

            switch ($driver) {
                case 'mysql':
                    $result = $connection->queryScalar(SqlQuery::create("
                        SELECT COUNT(*)
                        FROM information_schema.tables
                        WHERE table_schema = DATABASE()
                    "));
                    $count = (int)$result;

                    break;

                case 'pgsql':
                    $result = $connection->queryScalar("
                        SELECT COUNT(*)
                        FROM pg_tables
                        WHERE schemaname = 'public'
                    ");
                    $count = (int)$result;

                    break;

                case 'sqlite':
                    $result = $connection->queryScalar("
                        SELECT COUNT(*)
                        FROM sqlite_master
                        WHERE type = 'table'
                        AND name NOT LIKE 'sqlite_%'
                    ");
                    $count = (int)$result;

                    break;

                default:
                    return HealthCheckStatus::warning('Unable to determine table count for driver: ' . $driver);
            }

            // Check count thresholds
            $warningThreshold = 100;
            $errorThreshold = 500;

            if ($count > $errorThreshold) {
                return HealthCheckStatus::error("Table count ({$count}) exceeds error threshold ({$errorThreshold})");
            } elseif ($count > $warningThreshold) {
                return HealthCheckStatus::warning("Table count ({$count}) exceeds warning threshold ({$warningThreshold})");
            } else {
                return HealthCheckStatus::ok("Table count: {$count}");
            }
        } catch (\Throwable $e) {
            return HealthCheckStatus::warning('Unable to check table count: ' . $e->getMessage());
        }
    }

    /**
     * Check for long-running queries
     */
    private function checkLongRunningQueries(ConnectionInterface $connection): HealthCheckStatus
    {
        try {
            $driver = $connection->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
            $longQueries = [];

            switch ($driver) {
                case 'mysql':
                    $result = $connection->query("
                        SELECT
                            id,
                            user,
                            host,
                            db,
                            command,
                            time,
                            state,
                            info
                        FROM
                            information_schema.processlist
                        WHERE
                            command != 'Sleep'
                            AND time > 10
                        ORDER BY
                            time DESC
                    ")->fetchAll();

                    foreach ($result as $row) {
                        $longQueries[] = [
                            'id' => $row['id'],
                            'user' => $row['user'],
                            'database' => $row['db'],
                            'time' => $row['time'],
                            'state' => $row['state'],
                            'query' => $row['info'],
                        ];
                    }

                    break;

                case 'pgsql':
                    $optimizer = new PostgreSQLOptimizer($connection);
                    $longQueries = $optimizer->getLongRunningQueries(10);

                    break;

                case 'sqlite':
                    // SQLite doesn't support process list or long-running query detection
                    return HealthCheckStatus::ok('Long-running query detection not supported for SQLite');

                default:
                    return HealthCheckStatus::warning('Unable to check long-running queries for driver: ' . $driver);
            }

            $count = count($longQueries);

            if ($count > 5) {
                return HealthCheckStatus::error("Found {$count} long-running queries");
            } elseif ($count > 0) {
                return HealthCheckStatus::warning("Found {$count} long-running queries");
            } else {
                return HealthCheckStatus::ok('No long-running queries found');
            }
        } catch (\Throwable $e) {
            return HealthCheckStatus::warning('Unable to check long-running queries: ' . $e->getMessage());
        }
    }

    /**
     * Check MySQL-specific health issues
     */
    private function checkMySQLHealth(ConnectionInterface $connection, HealthCheckResult $result): void
    {
        try {
            $optimizer = new MySQLOptimizer($connection);

            // Check InnoDB buffer pool hit ratio
            $bufferPoolStats = $optimizer->getBufferPoolStatistics();
            $reads = (int)($bufferPoolStats['Innodb_buffer_pool_reads'] ?? 0);
            $readRequests = (int)($bufferPoolStats['Innodb_buffer_pool_read_requests'] ?? 0);

            if ($readRequests > 0) {
                $hitRatio = 100 - (($reads / $readRequests) * 100);

                $status = $hitRatio < 95
                    ? HealthCheckStatus::warning("Buffer pool hit ratio is low: {$hitRatio}%")
                    : HealthCheckStatus::ok("Buffer pool hit ratio: {$hitRatio}%");

                $result->addCheck(
                    'mysql_buffer_pool',
                    'InnoDB Buffer Pool Hit Ratio',
                    $status,
                    'Checks if the InnoDB buffer pool hit ratio is acceptable'
                );
            }

            // Check for tables that need optimization
            $tableStatus = $optimizer->getTableStatus();
            $tablesNeedingOptimization = [];

            foreach ($tableStatus as $tableName => $status) {
                if (isset($status['Engine']) && $status['Engine'] === 'InnoDB') {
                    continue; // InnoDB tables don't need optimization
                }

                if (isset($status['Data_free']) && $status['Data_free'] > 0) {
                    $dataFree = $status['Data_free'];
                    $dataLength = $status['Data_length'] ?? 0;

                    if ($dataLength > 0 && ($dataFree / $dataLength) > 0.1) {
                        $tablesNeedingOptimization[] = $tableName;
                    }
                }
            }

            $count = count($tablesNeedingOptimization);

            $status = $count > 0
                ? HealthCheckStatus::warning("Found {$count} tables that need optimization")
                : HealthCheckStatus::ok('No tables need optimization');

            $result->addCheck(
                'mysql_tables_optimization',
                'Tables Needing Optimization',
                $status,
                'Checks if there are tables that need optimization'
            );

            // Check max connections usage
            $maxConnections = (int)$connection->queryScalar("SHOW VARIABLES LIKE 'max_connections'");
            $currentConnections = (int)$connection->queryScalar("SHOW STATUS LIKE 'Threads_connected'");

            if ($maxConnections > 0) {
                $connectionUsage = ($currentConnections / $maxConnections) * 100;

                $status = $connectionUsage > 80
                    ? HealthCheckStatus::warning("Connection usage is high: {$connectionUsage}%")
                    : HealthCheckStatus::ok("Connection usage: {$connectionUsage}%");

                $result->addCheck(
                    'mysql_connections',
                    'Connection Usage',
                    $status,
                    'Checks if the connection usage is within acceptable limits'
                );
            }

        } catch (\Throwable $e) {
            $result->addError('mysql_health', 'MySQL Health Check', $e->getMessage());
        }
    }

    /**
     * Check PostgreSQL-specific health issues
     */
    private function checkPostgreSQLHealth(ConnectionInterface $connection, HealthCheckResult $result): void
    {
        try {
            $optimizer = new PostgreSQLOptimizer($connection);

            // Check cache hit ratio
            $cacheStats = $optimizer->getCacheStatistics();
            $heapHitRatio = $cacheStats['heap_hit_ratio'] ?? 0;
            $idxHitRatio = $cacheStats['idx_hit_ratio'] ?? 0;

            $status = $heapHitRatio < 90
                ? HealthCheckStatus::warning("Heap hit ratio is low: {$heapHitRatio}%")
                : HealthCheckStatus::ok("Heap hit ratio: {$heapHitRatio}%");

            $result->addCheck(
                'pgsql_heap_hit_ratio',
                'Heap Hit Ratio',
                $status,
                'Checks if the heap hit ratio is acceptable'
            );

            $status = $idxHitRatio < 90
                ? HealthCheckStatus::warning("Index hit ratio is low: {$idxHitRatio}%")
                : HealthCheckStatus::ok("Index hit ratio: {$idxHitRatio}%");

            $result->addCheck(
                'pgsql_idx_hit_ratio',
                'Index Hit Ratio',
                $status,
                'Checks if the index hit ratio is acceptable'
            );

            // Check for tables that need vacuuming
            $deadTupleThreshold = 10000;
            $deadTupleRatioThreshold = 0.2; // 20%

            $tablesNeedingVacuum = [];

            $tables = $connection->query("
                SELECT
                    relname as table_name,
                    n_live_tup as live_tuples,
                    n_dead_tup as dead_tuples
                FROM
                    pg_stat_user_tables
                WHERE
                    schemaname = 'public'
            ")->fetchAll();

            foreach ($tables as $table) {
                $deadTuples = (int)$table['dead_tuples'];
                $liveTuples = (int)$table['live_tuples'];

                if ($deadTuples > $deadTupleThreshold) {
                    $tablesNeedingVacuum[] = $table['table_name'];
                } elseif ($liveTuples > 0 && ($deadTuples / $liveTuples) > $deadTupleRatioThreshold) {
                    $tablesNeedingVacuum[] = $table['table_name'];
                }
            }

            $count = count($tablesNeedingVacuum);

            $status = $count > 0
                ? HealthCheckStatus::warning("Found {$count} tables that need vacuuming")
                : HealthCheckStatus::ok('No tables need vacuuming');

            $result->addCheck(
                'pgsql_vacuum',
                'Tables Needing Vacuum',
                $status,
                'Checks if there are tables that need vacuuming'
            );

            // Check for bloated indexes
            $bloatedIndexes = [];

            $indexes = $connection->query("
                SELECT
                    indexrelname as index_name,
                    relname as table_name,
                    pg_size_pretty(pg_relation_size(indexrelid)) as index_size,
                    idx_scan as index_scans
                FROM
                    pg_stat_user_indexes
                WHERE
                    schemaname = 'public'
                    AND idx_scan < 50
                    AND pg_relation_size(indexrelid) > 5 * 1024 * 1024
                ORDER BY
                    pg_relation_size(indexrelid) DESC
            ")->fetchAll();

            foreach ($indexes as $index) {
                $bloatedIndexes[] = $index['index_name'] . ' on ' . $index['table_name'] . ' (' . $index['index_size'] . ')';
            }

            $count = count($bloatedIndexes);

            $status = $count > 0
                ? HealthCheckStatus::warning("Found {$count} potentially bloated indexes")
                : HealthCheckStatus::ok('No bloated indexes found');

            $result->addCheck(
                'pgsql_bloated_indexes',
                'Bloated Indexes',
                $status,
                'Checks for potentially bloated indexes'
            );

        } catch (\Throwable $e) {
            $result->addError('pgsql_health', 'PostgreSQL Health Check', $e->getMessage());
        }
    }

    /**
     * Check SQLite-specific health issues
     */
    private function checkSQLiteHealth(ConnectionInterface $connection, HealthCheckResult $result): void
    {
        try {
            $optimizer = new SQLiteOptimizer($connection);

            // Check database integrity
            $integrityCheck = $connection->query("PRAGMA integrity_check")->fetchAll(\PDO::FETCH_COLUMN);

            $status = (count($integrityCheck) === 1 && $integrityCheck[0] === 'ok')
                ? HealthCheckStatus::ok('Database integrity check passed')
                : HealthCheckStatus::error('Database integrity check failed: ' . implode(', ', $integrityCheck));

            $result->addCheck(
                'sqlite_integrity',
                'Database Integrity',
                $status,
                'Checks if the database passes the integrity check'
            );

            // Check for fragmentation
            $stats = $optimizer->getDatabaseStatistics();
            $pageCount = $stats['page_count'] ?? 0;
            $freelistCount = $stats['freelist_count'] ?? 0;

            if ($pageCount > 0) {
                $fragmentation = ($freelistCount / $pageCount) * 100;

                $status = $fragmentation > 20
                    ? HealthCheckStatus::warning("Database fragmentation is high: {$fragmentation}%")
                    : HealthCheckStatus::ok("Database fragmentation: {$fragmentation}%");

                $result->addCheck(
                    'sqlite_fragmentation',
                    'Database Fragmentation',
                    $status,
                    'Checks if the database fragmentation is within acceptable limits'
                );
            }

            // Check journal mode
            $journalMode = $connection->queryScalar("PRAGMA journal_mode");

            $status = $journalMode === 'wal'
                ? HealthCheckStatus::ok('Journal mode: WAL')
                : HealthCheckStatus::warning("Journal mode is not WAL: {$journalMode}");

            $result->addCheck(
                'sqlite_journal_mode',
                'Journal Mode',
                $status,
                'Checks if the journal mode is set to WAL for better performance'
            );

            // Check synchronous setting
            $synchronous = $connection->queryScalar("PRAGMA synchronous");

            $status = $synchronous < 2
                ? HealthCheckStatus::ok('Synchronous setting: ' . $synchronous)
                : HealthCheckStatus::warning("Synchronous setting is high: {$synchronous}");

            $result->addCheck(
                'sqlite_synchronous',
                'Synchronous Setting',
                $status,
                'Checks if the synchronous setting is optimized for performance'
            );

        } catch (\Throwable $e) {
            $result->addError('sqlite_health', 'SQLite Health Check', $e->getMessage());
        }
    }

    /**
     * Format bytes to a human-readable string
     */
    private function formatBytes(int $bytes, int $precision = 2): string
    {
        $units = ['B', 'KB', 'MB', 'GB', 'TB'];

        $bytes = max($bytes, 0);
        $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
        $pow = min($pow, count($units) - 1);

        $bytes /= (1 << (10 * $pow));

        return round($bytes, $precision) . ' ' . $units[$pow];
    }
}
