<?php

declare(strict_types=1);

namespace App\Framework\Database\ReadWrite;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\DateTime\Clock;
use App\Framework\DateTime\Timer;

/**
 * Detects replication lag between master and slave databases
 */
final class ReplicationLagDetector
{
    public function __construct(
        private readonly Clock $clock,
        private readonly Timer $timer
    ) {
    }

    /**
     * Detect replication lag using heartbeat method
     */
    public function detectLag(ConnectionInterface $master, ConnectionInterface $replica): Duration
    {
        try {
            // Method 1: Use heartbeat table if available
            $lag = $this->detectLagUsingHeartbeat($master, $replica);
            if ($lag !== null) {
                return $lag;
            }

            // Method 2: Use MySQL/PostgreSQL specific commands
            $lag = $this->detectLagUsingBuiltInCommands($master, $replica);
            if ($lag !== null) {
                return $lag;
            }

            // Method 3: Fallback - create temporary record and check
            return $this->detectLagUsingTemporaryRecord($master, $replica);

        } catch (\Throwable $e) {
            // If we can't detect lag, assume it's too high
            return Duration::fromSeconds(999);
        }
    }

    /**
     * Detect lag using heartbeat table
     */
    private function detectLagUsingHeartbeat(ConnectionInterface $master, ConnectionInterface $replica): ?Duration
    {
        try {
            // Check if heartbeat table exists
            $tableExists = $replica->queryScalar(
                "SELECT COUNT(*) FROM information_schema.tables 
                 WHERE table_schema = DATABASE() AND table_name = 'replication_heartbeat'"
            );

            if (! $tableExists) {
                return null;
            }

            // Get latest heartbeat from replica
            $replicaTime = $replica->queryScalar(
                'SELECT UNIX_TIMESTAMP(heartbeat_time) FROM replication_heartbeat ORDER BY heartbeat_time DESC LIMIT 1'
            );

            if ($replicaTime === null) {
                return null;
            }

            $currentTime = $this->clock->now()->getTimestamp();
            $lagSeconds = $currentTime - (float) $replicaTime;

            return Duration::fromSeconds(max(0, $lagSeconds));

        } catch (\Throwable) {
            return null;
        }
    }

    /**
     * Detect lag using database-specific commands
     */
    private function detectLagUsingBuiltInCommands(ConnectionInterface $master, ConnectionInterface $replica): ?Duration
    {
        try {
            // Try MySQL SHOW SLAVE STATUS
            $slaveStatus = $replica->queryOne('SHOW SLAVE STATUS');
            if ($slaveStatus && isset($slaveStatus['Seconds_Behind_Master'])) {
                $lagSeconds = (float) $slaveStatus['Seconds_Behind_Master'];

                return Duration::fromSeconds($lagSeconds);
            }

            // Try PostgreSQL specific lag detection
            $pgLag = $replica->queryScalar(
                "SELECT EXTRACT(EPOCH FROM (now() - pg_last_xact_replay_timestamp()))"
            );

            if ($pgLag !== null) {
                return Duration::fromSeconds((float) $pgLag);
            }

        } catch (\Throwable) {
            // Database-specific commands failed, continue to fallback
        }

        return null;
    }

    /**
     * Detect lag using temporary record method
     */
    private function detectLagUsingTemporaryRecord(ConnectionInterface $master, ConnectionInterface $replica): Duration
    {
        $testId = uniqid('lag_test_', true);
        $insertTime = $this->clock->time();

        try {
            // Create test table if it doesn't exist
            $this->createLagTestTable($master);

            // Insert test record on master
            $master->execute(
                'INSERT INTO lag_test_table (test_id, test_time) VALUES (?, ?)',
                [$testId, $insertTime]
            );

            // Try to read from replica with timeout
            $maxWaitSeconds = 10;
            $checkInterval = 0.1; // 100ms
            $totalWaitTime = 0;

            while ($totalWaitTime < $maxWaitSeconds) {
                try {
                    $replicaTime = $replica->queryScalar(
                        'SELECT test_time FROM lag_test_table WHERE test_id = ?',
                        [$testId]
                    );

                    if ($replicaTime !== null) {
                        $lagSeconds = $this->clock->time()->diff($insertTime)->toSeconds();

                        // Cleanup
                        $this->cleanupLagTest($master, $testId);

                        return Duration::fromSeconds($lagSeconds);
                    }

                } catch (\Throwable) {
                    // Continue waiting
                }

                $this->timer->sleep(Duration::fromSeconds($checkInterval));
                $totalWaitTime += $checkInterval;
            }

            // Cleanup failed test
            $this->cleanupLagTest($master, $testId);

            // If we couldn't detect within timeout, assume high lag
            return Duration::fromSeconds($maxWaitSeconds);

        } catch (\Throwable $e) {
            // Cleanup on error
            try {
                $this->cleanupLagTest($master, $testId);
            } catch (\Throwable) {
                // Ignore cleanup errors
            }

            throw new DatabaseException(
                'Failed to detect replication lag: ' . $e->getMessage(),
                previous: $e
            );
        }
    }

    /**
     * Create lag test table
     */
    private function createLagTestTable(ConnectionInterface $connection): void
    {
        try {
            $connection->execute('
                CREATE TABLE IF NOT EXISTS lag_test_table (
                    test_id VARCHAR(50) PRIMARY KEY,
                    test_time DOUBLE NOT NULL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                )
            ');
        } catch (\Throwable $e) {
            throw new DatabaseException(
                'Failed to create lag test table: ' . $e->getMessage(),
                previous: $e
            );
        }
    }

    /**
     * Cleanup lag test record
     */
    private function cleanupLagTest(ConnectionInterface $connection, string $testId): void
    {
        try {
            $connection->execute('DELETE FROM lag_test_table WHERE test_id = ?', [$testId]);

            // Also cleanup old test records (older than 1 minute)
            $connection->execute(
                'DELETE FROM lag_test_table WHERE created_at < DATE_SUB(NOW(), INTERVAL 1 MINUTE)'
            );
        } catch (\Throwable) {
            // Ignore cleanup errors
        }
    }

    /**
     * Initialize heartbeat system (for production use)
     */
    public function initializeHeartbeat(ConnectionInterface $master): void
    {
        try {
            // Create heartbeat table
            $master->execute('
                CREATE TABLE IF NOT EXISTS replication_heartbeat (
                    id INT PRIMARY KEY AUTO_INCREMENT,
                    heartbeat_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
                )
            ');

            // Insert initial heartbeat
            $master->execute('INSERT INTO replication_heartbeat () VALUES ()');

        } catch (\Throwable $e) {
            throw new DatabaseException(
                'Failed to initialize heartbeat system: ' . $e->getMessage(),
                previous: $e
            );
        }
    }

    /**
     * Update heartbeat (should be called periodically)
     */
    public function updateHeartbeat(ConnectionInterface $master): void
    {
        try {
            $master->execute('UPDATE replication_heartbeat SET heartbeat_time = NOW() WHERE id = 1');
        } catch (\Throwable) {
            // If update fails, try to insert
            try {
                $master->execute('INSERT INTO replication_heartbeat () VALUES ()');
            } catch (\Throwable) {
                // Ignore heartbeat update errors
            }
        }
    }
}
