<?php

declare(strict_types=1);

namespace App\Framework\Config;

use App\Framework\Config\Exceptions\RequiredEnvironmentVariableException;
use App\Framework\Filesystem\ValueObjects\FilePath;
use BackedEnum;

final readonly class Environment
{
    /**
     * @param array<string, mixed> $variables
     */
    public function __construct(
        private array $variables = [],
        private DockerSecretsResolver $secretsResolver = new DockerSecretsResolver()
    ) {
    }

    public function get(EnvKey|string $key, mixed $default = null): mixed
    {
        $key = $this->keyToString($key);

        // 1. Check if direct env var exists in internal array and is not empty
        // Empty strings are treated as "not set" to allow Docker Secrets resolution
        if (isset($this->variables[$key])) {
            $value = $this->variables[$key];
            // If value is not empty, return it (non-empty values take precedence)
            if ($value !== '' && $value !== null) {
                return $value;
            }
            // If value is empty, continue to check Docker Secrets as fallback
        }

        // 2. Docker Secrets support: Check for *_FILE pattern
        // This allows Docker Secrets to override empty values
        $secretValue = $this->secretsResolver->resolve($key, $this->variables);
        if ($secretValue !== null) {
            return $secretValue;
        }

        // 3. Fallback: Check system environment dynamically
        // This handles cases where environment variables are set after Environment initialization
        // (common in PHP-FPM where vars may be set during request processing)
        $systemValue = $this->getFromSystemEnvironment($key);
        if ($systemValue !== null && $systemValue !== '') {
            return $systemValue;
        }

        // 4. If internal variable was set (even if empty), return it (to distinguish between "not set" and "empty")
        // This preserves the original behavior: if variable is explicitly set to empty string, return it
        if (isset($this->variables[$key])) {
            return $this->variables[$key];
        }

        // 5. Return default
        return $default;
    }

    /**
     * Get variable from system environment as fallback
     *
     * This ensures we can access environment variables that were set
     * after Environment initialization (e.g., by PHP-FPM/FastCGI)
     */
    private function getFromSystemEnvironment(string $key): ?string
    {
        // Priority: $_ENV > $_SERVER > getenv()
        // $_ENV and $_SERVER may contain dynamically set vars in PHP-FPM

        if (isset($_ENV[$key]) && is_string($_ENV[$key])) {
            return $_ENV[$key];
        }

        if (isset($_SERVER[$key]) && is_string($_SERVER[$key]) && !str_starts_with($key, 'HTTP_')) {
            return $_SERVER[$key];
        }

        $value = getenv($key);
        if ($value !== false && is_string($value)) {
            return $value;
        }

        return null;
    }

    public function getRequired(EnvKey|string $key): mixed
    {
        $key = $this->keyToString($key);
        $value = $this->get($key);
        if ($value === null) {
            throw new RequiredEnvironmentVariableException($key);
        }

        return $value;
    }

    public function getInt(EnvKey|string $key, int $default = 0): int
    {
        $key = $this->keyToString($key);

        return (int) $this->get($key, $default);
    }

    public function getFloat(EnvKey|string $key, float $default = 0.0): float
    {
        $key = $this->keyToString($key);

        return (float) $this->get($key, $default);
    }

    public function getBool(EnvKey|string $key, bool $default = false): bool
    {
        $key = $this->keyToString($key);
        $value = $this->get($key, $default);
        if (is_string($value)) {
            return match (strtolower($value)) {
                'true', '1', 'yes', 'on' => true,
                'false', '0', 'no', 'off' => false,
                default => $default
            };
        }

        return (bool) $value;
    }

    public function getString(EnvKey|string $key, string $default = ''): string
    {
        $key = $this->keyToString($key);

        return (string) $this->get($key, $default);
    }

    /** @param class-string<BackedEnum> $enumClass */
    public function getEnum(EnvKey|string $key, string $enumClass, BackedEnum $default): BackedEnum
    {
        $key = $this->keyToString($key);

        if (! $default instanceof $enumClass) {
            throw new \InvalidArgumentException('Default value must be an instance of the enum class');
        }

        $value = $this->get($key);

        if ($value === null) {
            return $default;
        }

        return $enumClass::tryFrom($value) ?? $default;
    }

    public function has(EnvKey|string $key): bool
    {
        $key = $this->keyToString($key);

        return $this->get($key) !== null;
    }

    /**
     * @return array<string, mixed>
     */
    public function all(bool $sorted = false): array
    {
        // Merge internal variables with system environment variables
        // This ensures all available environment variables are returned,
        // including those that became available after Environment initialization
        // (e.g., set by PHP-FPM/FastCGI during request processing)
        $systemVariables = $this->getSystemEnvironment();

        // Merge: internal variables take precedence over system variables
        // This ensures variables loaded from .env files or set during initialization
        // take precedence over system environment variables
        $all = array_merge($systemVariables, $this->variables);

        // Resolve Docker Secrets for variables that are empty or not set
        // This ensures that variables like DB_PASSWORD are resolved from their *_FILE counterparts
        $resolved = [];
        
        // First pass: Process existing variables
        foreach ($all as $key => $value) {
            // Skip *_FILE variables (we'll resolve them below)
            if (str_ends_with($key, '_FILE')) {
                continue;
            }
            
            // If variable is empty or not set, check for Docker Secret
            if (empty($value) || $value === '' || $value === null) {
                $secretValue = $this->secretsResolver->resolve($key, $all);
                if ($secretValue !== null) {
                    $resolved[$key] = $secretValue;
                    continue;
                }
                // If secret resolution failed, don't add empty value yet
                // We'll check again in second pass with *_FILE pattern
                continue;
            }
            // Include non-empty values
            $resolved[$key] = $value;
        }
        
        // Second pass: Check for *_FILE variables that don't have corresponding resolved values
        // This handles cases where a *_FILE exists but the variable itself is not in the array
        foreach ($all as $key => $value) {
            if (str_ends_with($key, '_FILE')) {
                // Extract the base key name (e.g., APP_KEY from APP_KEY_FILE)
                $baseKey = substr($key, 0, -5); // Remove '_FILE' suffix
                
                // If base key is not set or empty, try to resolve it
                if (!isset($resolved[$baseKey]) || empty($resolved[$baseKey])) {
                    $secretValue = $this->secretsResolver->resolve($baseKey, $all);
                    if ($secretValue !== null) {
                        $resolved[$baseKey] = $secretValue;
                    }
                }
                
                // Always include *_FILE variables in output
                $resolved[$key] = $value;
            }
        }

        if ($sorted) {
            ksort($resolved);
        }

        return $resolved;
    }

    /**
     * Get all system environment variables dynamically
     *
     * @return array<string, mixed>
     */
    private function getSystemEnvironment(): array
    {
        $variables = [];

        // Load from $_ENV (contains dynamically set vars in PHP-FPM)
        foreach ($_ENV as $key => $value) {
            if (is_string($key) && is_string($value) && !str_starts_with($key, 'HTTP_')) {
                $variables[$key] = $value;
            }
        }

        // Load from $_SERVER (may contain additional vars from web server)
        foreach ($_SERVER as $key => $value) {
            if (!isset($variables[$key]) &&
                is_string($key) &&
                is_string($value) &&
                !str_starts_with($key, 'HTTP_') &&
                !in_array($key, ['GATEWAY_INTERFACE', 'SERVER_SOFTWARE', 'SERVER_NAME', 'SERVER_ADDR',
                                 'SERVER_PORT', 'REQUEST_URI', 'REQUEST_METHOD', 'QUERY_STRING',
                                 'CONTENT_TYPE', 'CONTENT_LENGTH', 'SCRIPT_NAME', 'SCRIPT_FILENAME',
                                 'PATH_INFO', 'FCGI_ROLE', 'REDIRECT_STATUS', 'REQUEST_TIME',
                                 'REQUEST_TIME_FLOAT', 'DOCUMENT_ROOT', 'DOCUMENT_URI',
                                 'REMOTE_ADDR', 'REMOTE_PORT', 'REMOTE_USER'], true)) {
                $variables[$key] = $value;
            }
        }

        // Load from getenv() as fallback
        $allEnvVars = getenv();
        if ($allEnvVars !== false) {
            foreach ($allEnvVars as $key => $value) {
                if (!isset($variables[$key])) {
                    $variables[$key] = $value;
                }
            }
        }

        return $variables;
    }

    /**
     * Factory method f?r .env file loading
     */
    public static function fromFile(FilePath|string $envPath): self
    {
        $parser = new EnvFileParser();
        $variables = $parser->parse($envPath);

        return new self($variables);
    }

    public function keyToString(EnvKey|string $key): string
    {
        if (is_string($key)) {
            return $key;
        }

        return $key->value;
    }

    /**
     * F?r Tests
     */
    public function withVariable(string $key, mixed $value): self
    {
        $variables = $this->variables;
        $variables[$key] = $value;

        return new self($variables);
    }

    /**
     * @param array<string, mixed> $variables
     */
    public function withVariables(array $variables): self
    {
        return new self(array_merge($this->variables, $variables));
    }
}
