<?php

declare(strict_types=1);

namespace Tests\Support\Api;

use mysqli;
use Predis\Client as PredisClient;
use RuntimeException;
use Throwable;

final class ApiTestRuntime
{
    private const DEFAULT_BASE_URL = 'http://127.0.0.1:18080';

    private static ?self $instance = null;

    private ?mysqli $db = null;
    private ?PredisClient $redis = null;
    private ?ApiServer $server = null;
    private ?ApiFixtures $fixtures = null;
    private ?ApiCleanup $cleanup = null;
    private bool $bootstrapped = false;
    private bool $schemaBootstrapped = false;
    private bool $shutdownRegistered = false;
    private string $baseUrl = self::DEFAULT_BASE_URL;
    private bool $usesExternalBaseUrl = false;
    private ?int $internalServerPort = null;

    public static function instance(): self
    {
        return self::$instance ??= new self();
    }

    public function skipReason(): ?string
    {
        if (!api_tests_enabled()) {
            return 'API tests are disabled. Run with RUN_API_TESTS=1.';
        }

        $this->assertApiDatabaseTargetIsSafe();

        try {
            $this->bootstrapEnvironment();
            $this->bootstrapSchemaIfRequested();
        } catch (Throwable $throwable) {
            return $throwable->getMessage();
        }

        $missingTables = $this->missingTables();
        if ($missingTables !== []) {
            return 'API tests require an initialized schema. Missing tables: ' . implode(', ', $missingTables);
        }

        return null;
    }

    public function beginTest(): void
    {
        $this->bootstrapEnvironment();
        $this->bootstrapSchemaIfRequested();
        $this->cleanup = new ApiCleanup();
        $this->fixtures = new ApiFixtures($this->db(), $this->redis(), $this->cleanup);
    }

    public function endTest(): void
    {
        if ($this->cleanup !== null) {
            $this->cleanup->run();
            $this->cleanup = null;
        }

        $this->fixtures = null;
    }

    public function client(): ApiClient
    {
        $this->bootstrapEnvironment();
        $this->ensureServerIsRunning();

        return new ApiClient($this->baseUrl);
    }

    public function restartServer(): void
    {
        if ($this->usesExternalBaseUrl) {
            return;
        }

        if ($this->server !== null) {
            $this->server->stop();
            $this->server = null;
        }

        $this->baseUrl = self::DEFAULT_BASE_URL;
        $this->internalServerPort = null;
    }

    public function fixtures(): ApiFixtures
    {
        if ($this->fixtures === null) {
            throw new RuntimeException('API fixtures are only available during an active API test.');
        }

        return $this->fixtures;
    }

    public function db(): mysqli
    {
        if ($this->db === null) {
            throw new RuntimeException('The API test database connection has not been initialized.');
        }

        return $this->db;
    }

    public function redis(): ?PredisClient
    {
        return $this->redis;
    }

    public function queryOne(string $sql): ?array
    {
        $result = $this->db()->query($sql);
        if ($result === false) {
            throw new RuntimeException('Query failed: ' . $sql);
        }

        $row = $result->fetch_assoc();
        $result->free();

        return $row ?: null;
    }

    public function shutdown(): void
    {
        if ($this->server !== null) {
            $this->server->stop();
            $this->server = null;
        }

        if ($this->db !== null) {
            $this->db->close();
            $this->db = null;
        }

        if ($this->redis !== null) {
            try {
                $this->redis->disconnect();
            } catch (Throwable) {
            }
            $this->redis = null;
        }

        $this->bootstrapped = false;
        $this->schemaBootstrapped = false;
        $this->internalServerPort = null;
    }

    private function bootstrapEnvironment(): void
    {
        if ($this->bootstrapped) {
            return;
        }

        $dbConfig = $this->readDbConfig();
        mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

        $this->db = new mysqli(
            $dbConfig['host'],
            $dbConfig['user'],
            $dbConfig['password'],
            $dbConfig['database'],
            $dbConfig['port']
        );
        $this->db->set_charset('utf8mb4');

        $redisConfig = $this->readRedisConfig();
        if ($redisConfig !== null) {
            $parameters = [
                'scheme' => 'tcp',
                'host' => $redisConfig['host'],
                'port' => $redisConfig['port'],
                'database' => $redisConfig['database'],
                'password' => $redisConfig['password'],
            ];
            if ($redisConfig['user'] !== '') {
                $parameters['username'] = $redisConfig['user'];
            }
            $this->redis = new PredisClient($parameters);
        }

        $configuredBaseUrl = trim((string)getenv('API_TEST_BASE_URL'));
        if ($configuredBaseUrl !== '') {
            $this->baseUrl = rtrim($configuredBaseUrl, '/');
            $this->usesExternalBaseUrl = true;
        }

        if (!$this->shutdownRegistered) {
            register_shutdown_function([$this, 'shutdown']);
            $this->shutdownRegistered = true;
        }

        $this->bootstrapped = true;
    }

    private function bootstrapSchemaIfRequested(): void
    {
        if ($this->schemaBootstrapped) {
            return;
        }

        if (getenv('API_TEST_BOOTSTRAP_SCHEMA') !== '1') {
            return;
        }

        (new ApiSchemaBootstrap($this->db()))->ensureSchema();
        $this->schemaBootstrapped = true;
    }

    private function ensureServerIsRunning(): void
    {
        if ($this->usesExternalBaseUrl) {
            return;
        }

        if ($this->server !== null && $this->server->isRunning()) {
            return;
        }

        $url = parse_url($this->baseUrl);
        $host = (string)($url['host'] ?? '127.0.0.1');
        $port = $this->internalServerPort ?? ApiServer::findAvailablePort($host);
        $this->internalServerPort = $port;
        $this->baseUrl = sprintf('http://%s:%d', $host, $port);

        $this->server = new ApiServer(app_path(), $host, $port);
        $this->server->start();
        $this->waitForPing();
    }

    private function waitForPing(): void
    {
        $deadline = microtime(true) + 10;
        $client = new ApiClient($this->baseUrl);
        $lastError = 'Timed out waiting for /ping.';

        while (microtime(true) < $deadline) {
            try {
                $response = $client->get('/ping');
                if ($response->status === 200) {
                    return;
                }
                $lastError = 'Unexpected /ping status: ' . $response->status;
            } catch (Throwable $throwable) {
                $lastError = $throwable->getMessage();
            }

            usleep(200000);
        }

        throw new RuntimeException($this->server?->describeFailure($lastError) ?? $lastError);
    }

    /**
     * @return array<int, string>
     */
    private function missingTables(): array
    {
        $requiredTables = [
            'users',
            'groups',
            'groups_permissions',
            'departments',
            'department_categories',
            'categories',
            'orders',
            'collected_order_invoices',
            'tokens',
            'subusers',
            'subuser_grants',
            'module_config',
            'customer_attributes',
        ];

        $missing = [];
        foreach ($requiredTables as $table) {
            $escaped = $this->db()->real_escape_string($table);
            $result = $this->db()->query("SHOW TABLES LIKE '{$escaped}'");
            if ($result === false || $result->num_rows === 0) {
                $missing[] = $table;
            }
            if ($result !== false) {
                $result->free();
            }
        }

        return $missing;
    }

    /**
     * @return array{host:string,user:string,password:string,database:string,port:int}
     */
    private function readDbConfig(): array
    {
        $target = strtolower(trim((string)(getenv('CONFIG_DB_TARGET') ?: 'live')));
        if ($target !== 'debug') {
            $target = 'live';
        }

        $host = $this->readConfigValue('CONFIG_DB_HOST', 'CONFIG_DB_DEBUG_HOST', $target);
        $user = $this->readConfigValue('CONFIG_DB_USER', 'CONFIG_DB_DEBUG_USER', $target);
        $password = $this->readConfigValue('CONFIG_DB_PASSWORD', 'CONFIG_DB_DEBUG_PASSWORD', $target);
        $database = $this->readConfigValue('CONFIG_DB_DATABASE', 'CONFIG_DB_DEBUG_DATABASE', $target);
        $port = (int)($this->readConfigValue('CONFIG_DB_PORT', 'CONFIG_DB_DEBUG_PORT', $target) ?: '3306');

        $this->assertApiDatabaseTargetIsSafe($target, $host, $user, $database, $port);

        if ($host === '' || $user === '' || $database === '') {
            throw new RuntimeException('API tests require CONFIG_DB_HOST, CONFIG_DB_USER and CONFIG_DB_DATABASE to be set.');
        }

        return [
            'host' => $host,
            'user' => $user,
            'password' => $password,
            'database' => $database,
            'port' => $port > 0 ? $port : 3306,
        ];
    }

    /**
     * @return array{host:string,user:string,password:string,database:int,port:int}|null
     */
    private function readRedisConfig(): ?array
    {
        $target = strtolower(trim((string)(getenv('CONFIG_DB_TARGET') ?: 'live')));
        if ($target !== 'debug') {
            $target = 'live';
        }

        $host = $this->readConfigValue('REDIS_CONFIG_HOST', 'REDIS_CONFIG_DEBUG_HOST', $target);
        if ($host === '') {
            return null;
        }

        return [
            'host' => $host,
            'user' => $this->readConfigValue('REDIS_CONFIG_USER', 'REDIS_CONFIG_DEBUG_USER', $target),
            'password' => $this->readConfigValue('REDIS_CONFIG_PASSWORD', 'REDIS_CONFIG_DEBUG_PASSWORD', $target),
            'database' => (int)($this->readConfigValue('REDIS_CONFIG_DATABASE', 'REDIS_CONFIG_DEBUG_DATABASE', $target) ?: '0'),
            'port' => (int)($this->readConfigValue('REDIS_CONFIG_PORT', 'REDIS_CONFIG_DEBUG_PORT', $target) ?: '6379'),
        ];
    }

    private function readConfigValue(string $liveKey, string $debugKey, string $target): string
    {
        $liveValue = trim((string)(getenv($liveKey) ?: ''));
        $debugValue = trim((string)(getenv($debugKey) ?: ''));

        if ($target === 'debug' && $debugValue !== '') {
            return $debugValue;
        }

        return $liveValue;
    }

    private function assertApiDatabaseTargetIsSafe(
        ?string $target = null,
        ?string $host = null,
        ?string $user = null,
        ?string $database = null,
        ?int $port = null
    ): void {
        if ((string)(getenv('API_TEST_ALLOW_LIVE_DB') ?: '') === '1') {
            return;
        }

        $target = strtolower(trim((string)($target ?? (getenv('CONFIG_DB_TARGET') ?: 'live'))));
        if ($target !== 'debug') {
            throw new RuntimeException(
                'Refusing to run API tests against CONFIG_DB_TARGET=live. Use CONFIG_DB_TARGET=debug, or set API_TEST_ALLOW_LIVE_DB=1 for an explicit override.'
            );
        }

        $host ??= $this->readConfigValue('CONFIG_DB_HOST', 'CONFIG_DB_DEBUG_HOST', $target);
        $user ??= $this->readConfigValue('CONFIG_DB_USER', 'CONFIG_DB_DEBUG_USER', $target);
        $database ??= $this->readConfigValue('CONFIG_DB_DATABASE', 'CONFIG_DB_DEBUG_DATABASE', $target);
        $port ??= (int)($this->readConfigValue('CONFIG_DB_PORT', 'CONFIG_DB_DEBUG_PORT', $target) ?: '3306');

        $liveHost = trim((string)(getenv('CONFIG_DB_HOST') ?: ''));
        $liveUser = trim((string)(getenv('CONFIG_DB_USER') ?: ''));
        $liveDatabase = trim((string)(getenv('CONFIG_DB_DATABASE') ?: ''));
        $livePort = (int)(trim((string)(getenv('CONFIG_DB_PORT') ?: '3306')) ?: '3306');

        if (
            $liveHost !== '' &&
            $liveUser !== '' &&
            $liveDatabase !== '' &&
            $host === $liveHost &&
            $user === $liveUser &&
            $database === $liveDatabase &&
            $port === $livePort
        ) {
            throw new RuntimeException(
                'Refusing to run API tests because CONFIG_DB_TARGET=debug resolves to the configured live database. Point CONFIG_DB_DEBUG_* at an isolated database, or set API_TEST_ALLOW_LIVE_DB=1 for an explicit override.'
            );
        }
    }
}
