<?php

declare(strict_types=1);

namespace BlackRed\Bootstrap;

use Dotenv\Dotenv;
use RuntimeException;

/**
 * Centralised configuration loader.
 *
 * Loads .env once, then provides typed accessors. We never read $_ENV / getenv()
 * directly elsewhere in the codebase — everything goes through this class so
 * config access is uniform and testable.
 *
 * Required values fail loudly at boot, not silently at the moment of use.
 */
final class Config
{
    private array $values = [];
    private bool $loaded = false;

    public function __construct(private readonly string $basePath)
    {
    }

    /**
     * Load .env from the base path. Idempotent.
     *
     * @throws RuntimeException If .env is missing or required keys are absent.
     */
    public function load(): void
    {
        if ($this->loaded) {
            return;
        }

        $envFile = $this->basePath . '/.env';
        if (!is_file($envFile)) {
            throw new RuntimeException(
                'Missing .env file. Copy .env.example to .env and configure it.'
            );
        }

        // Read-only mutable: we use $_ENV via Dotenv, then snapshot into $this->values.
        $dotenv = Dotenv::createImmutable($this->basePath);
        $dotenv->load();

        // Snapshot the env into our own array so future getenv() calls in user code
        // can't accidentally override what we loaded.
        foreach ($_ENV as $k => $v) {
            $this->values[$k] = $v;
        }

        $this->validateRequired();
        $this->loaded = true;
    }

    /**
     * Required keys that must be present and non-empty for the app to start.
     */
    private function validateRequired(): void
    {
        $required = [
            'APP_ENV',
            'APP_SECRET',
            'DB_HOST',
            'DB_NAME',
            'DB_USER',
        ];

        $missing = [];
        foreach ($required as $key) {
            if (!isset($this->values[$key]) || $this->values[$key] === '') {
                $missing[] = $key;
            }
        }

        if (!empty($missing)) {
            throw new RuntimeException(
                'Missing required environment variables: ' . implode(', ', $missing)
            );
        }

        // APP_SECRET sanity: at least 32 hex chars.
        if (strlen((string)$this->values['APP_SECRET']) < 32) {
            throw new RuntimeException(
                'APP_SECRET must be at least 32 characters. ' .
                'Generate with: php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"'
            );
        }
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->values[$key] ?? $default;
    }

    public function string(string $key, ?string $default = null): string
    {
        $val = $this->values[$key] ?? $default;
        if ($val === null) {
            throw new RuntimeException("Config key '{$key}' is required and not set.");
        }
        return (string)$val;
    }

    public function int(string $key, ?int $default = null): int
    {
        $val = $this->values[$key] ?? $default;
        if ($val === null) {
            throw new RuntimeException("Config key '{$key}' is required and not set.");
        }
        return (int)$val;
    }

    public function bool(string $key, bool $default = false): bool
    {
        $val = $this->values[$key] ?? null;
        if ($val === null) {
            return $default;
        }
        $val = strtolower((string)$val);
        return in_array($val, ['1', 'true', 'yes', 'on'], true);
    }

    public function isProduction(): bool
    {
        return $this->string('APP_ENV', 'development') === 'production';
    }

    public function isDebug(): bool
    {
        return $this->bool('APP_DEBUG', false);
    }

    public function basePath(): string
    {
        return $this->basePath;
    }
}
