<?php

declare(strict_types=1);

namespace App\Support\Settings;

use App\Models\Finding;
use App\Services\Localization\LocaleResolver;
use App\Support\Ai\AiPolicyMode;
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
use App\Services\Entitlements\WorkspacePlanProfileCatalog;

final class SettingsRegistry
{
    /**
     * @var array<string, SettingDefinition>
     */
    private array $definitions;

    public function __construct()
    {
        $this->definitions = [];

        $this->register(new SettingDefinition(
            domain: 'ai',
            key: 'policy_mode',
            type: 'string',
            systemDefault: AiPolicyMode::Disabled->value,
            rules: ['required', 'string', 'in:disabled,private_only'],
            normalizer: static fn (mixed $value): string => strtolower(trim((string) $value)),
        ));

        $this->register(new SettingDefinition(
            domain: LocaleResolver::SETTING_DOMAIN,
            key: LocaleResolver::SETTING_DEFAULT_LOCALE,
            type: 'string',
            systemDefault: null,
            rules: [
                'nullable',
                'string',
                'in:'.implode(',', LocaleResolver::supportedLocales()),
            ],
            normalizer: static function (mixed $value): ?string {
                if ($value === null) {
                    return null;
                }

                return LocaleResolver::normalize($value);
            },
        ));

        $this->register(new SettingDefinition(
            domain: 'backup',
            key: 'retention_keep_last_default',
            type: 'int',
            systemDefault: 30,
            rules: ['required', 'integer', 'min:1', 'max:365'],
            normalizer: static fn (mixed $value): int => (int) $value,
        ));

        $this->register(new SettingDefinition(
            domain: 'backup',
            key: 'retention_min_floor',
            type: 'int',
            systemDefault: 1,
            rules: ['required', 'integer', 'min:1', 'max:365'],
            normalizer: static fn (mixed $value): int => (int) $value,
        ));

        $this->register(new SettingDefinition(
            domain: 'drift',
            key: 'severity_mapping',
            type: 'json',
            systemDefault: [],
            rules: [
                'required',
                'array',
                static function (string $attribute, mixed $value, \Closure $fail): void {
                    if (! is_array($value)) {
                        $fail('The severity mapping must be a JSON object.');

                        return;
                    }

                    foreach ($value as $findingType => $severity) {
                        if (! is_string($findingType) || trim($findingType) === '') {
                            $fail('Each severity mapping key must be a non-empty string.');

                            return;
                        }

                        if (! is_string($severity)) {
                            $fail(sprintf('Severity for "%s" must be a string.', $findingType));

                            return;
                        }

                        $normalizedSeverity = strtolower($severity);

                        if (! in_array($normalizedSeverity, self::supportedFindingSeverities(), true)) {
                            $fail(sprintf(
                                'Severity for "%s" must be one of: %s.',
                                $findingType,
                                implode(', ', self::supportedFindingSeverities()),
                            ));

                            return;
                        }
                    }
                },
            ],
            normalizer: static fn (mixed $value): array => self::normalizeSeverityMapping($value),
        ));

        $this->register(new SettingDefinition(
            domain: 'baseline',
            key: 'severity_mapping',
            type: 'json',
            systemDefault: self::defaultBaselineSeverityMapping(),
            rules: [
                'required',
                'array',
                static function (string $attribute, mixed $value, \Closure $fail): void {
                    if (! is_array($value)) {
                        $fail('The baseline severity mapping must be a JSON object.');

                        return;
                    }

                    $supportedKeys = array_fill_keys(self::supportedBaselineChangeTypes(), true);

                    foreach ($value as $changeType => $severity) {
                        if (! is_string($changeType) || ! isset($supportedKeys[$changeType])) {
                            $fail(sprintf(
                                'Baseline severity mapping keys must be one of: %s.',
                                implode(', ', self::supportedBaselineChangeTypes()),
                            ));

                            return;
                        }

                        if (! is_string($severity)) {
                            $fail(sprintf('Severity for "%s" must be a string.', $changeType));

                            return;
                        }

                        $normalizedSeverity = strtolower($severity);

                        if (! in_array($normalizedSeverity, self::supportedFindingSeverities(), true)) {
                            $fail(sprintf(
                                'Severity for "%s" must be one of: %s.',
                                $changeType,
                                implode(', ', self::supportedFindingSeverities()),
                            ));

                            return;
                        }
                    }
                },
            ],
            normalizer: static fn (mixed $value): array => self::normalizeBaselineSeverityMapping($value),
        ));

        $this->register(new SettingDefinition(
            domain: 'baseline',
            key: 'alert_min_severity',
            type: 'string',
            systemDefault: Finding::SEVERITY_HIGH,
            rules: ['required', 'string', 'in:low,medium,high,critical'],
            normalizer: static fn (mixed $value): string => strtolower(trim((string) $value)),
        ));

        $this->register(new SettingDefinition(
            domain: 'baseline',
            key: 'auto_close_enabled',
            type: 'bool',
            systemDefault: true,
            rules: ['required', 'boolean'],
            normalizer: static fn (mixed $value): bool => filter_var($value, FILTER_VALIDATE_BOOL),
        ));

        $this->register(new SettingDefinition(
            domain: 'findings',
            key: 'sla_days',
            type: 'json',
            systemDefault: self::defaultFindingsSlaDays(),
            rules: [
                'required',
                'array',
                static function (string $attribute, mixed $value, \Closure $fail): void {
                    if (! is_array($value)) {
                        $fail('The findings SLA days setting must be a JSON object.');

                        return;
                    }

                    $supportedSeverities = self::supportedFindingSeverities();
                    $supportedMap = array_fill_keys($supportedSeverities, true);

                    foreach ($value as $severity => $days) {
                        if (! is_string($severity)) {
                            $fail('Each findings SLA key must be a severity string.');

                            return;
                        }

                        $normalizedSeverity = strtolower($severity);

                        if (! isset($supportedMap[$normalizedSeverity])) {
                            $fail(sprintf(
                                'Unsupported findings SLA severity "%s". Expected only: %s.',
                                $severity,
                                implode(', ', $supportedSeverities),
                            ));

                            return;
                        }

                        $normalizedDays = filter_var($days, FILTER_VALIDATE_INT);

                        if ($normalizedDays === false || $normalizedDays < 1 || $normalizedDays > 3650) {
                            $fail(sprintf(
                                'Findings SLA days for "%s" must be an integer between 1 and 3650.',
                                $normalizedSeverity,
                            ));

                            return;
                        }
                    }
                },
            ],
            normalizer: static fn (mixed $value): array => self::normalizeFindingsSlaDays($value),
        ));

        $this->register(new SettingDefinition(
            domain: 'operations',
            key: 'operation_run_retention_days',
            type: 'int',
            systemDefault: 90,
            rules: ['required', 'integer', 'min:7', 'max:3650'],
            normalizer: static fn (mixed $value): int => (int) $value,
        ));

        $this->register(new SettingDefinition(
            domain: 'operations',
            key: 'stuck_run_threshold_minutes',
            type: 'int',
            systemDefault: 0,
            rules: ['required', 'integer', 'min:0', 'max:10080'],
            normalizer: static fn (mixed $value): int => (int) $value,
        ));

        $this->register(new SettingDefinition(
            domain: 'entitlements',
            key: 'plan_profile',
            type: 'string',
            systemDefault: WorkspacePlanProfileCatalog::defaultProfileId(),
            rules: [
                'nullable',
                'string',
                'in:'.implode(',', WorkspacePlanProfileCatalog::profileIds()),
            ],
            normalizer: static function (mixed $value): ?string {
                if ($value === null) {
                    return null;
                }

                $normalized = trim((string) $value);

                return $normalized === '' ? null : $normalized;
            },
        ));

        $this->register(new SettingDefinition(
            domain: 'entitlements',
            key: 'managed_tenant_limit_override_value',
            type: 'int',
            systemDefault: null,
            rules: ['nullable', 'integer', 'min:0'],
            normalizer: static function (mixed $value): ?int {
                if ($value === null || $value === '') {
                    return null;
                }

                return (int) $value;
            },
        ));

        $this->register(new SettingDefinition(
            domain: 'entitlements',
            key: 'managed_tenant_limit_override_reason',
            type: 'string',
            systemDefault: null,
            rules: ['nullable', 'string', 'max:500'],
            normalizer: static function (mixed $value): ?string {
                if ($value === null) {
                    return null;
                }

                $normalized = trim((string) $value);

                return $normalized === '' ? null : $normalized;
            },
        ));

        $this->register(new SettingDefinition(
            domain: 'entitlements',
            key: 'review_pack_generation_override_value',
            type: 'bool',
            systemDefault: null,
            rules: ['nullable', 'boolean'],
            normalizer: static function (mixed $value): ?bool {
                if ($value === null || $value === '') {
                    return null;
                }

                return filter_var($value, FILTER_VALIDATE_BOOL);
            },
        ));

        $this->register(new SettingDefinition(
            domain: 'entitlements',
            key: 'review_pack_generation_override_reason',
            type: 'string',
            systemDefault: null,
            rules: ['nullable', 'string', 'max:500'],
            normalizer: static function (mixed $value): ?string {
                if ($value === null) {
                    return null;
                }

                $normalized = trim((string) $value);

                return $normalized === '' ? null : $normalized;
            },
        ));

        $this->register(new SettingDefinition(
            domain: 'entitlements',
            key: WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_STATE,
            type: 'string',
            systemDefault: null,
            rules: [
                'nullable',
                'string',
                'in:'.implode(',', WorkspaceCommercialLifecycleResolver::stateIds()),
            ],
            normalizer: static function (mixed $value): ?string {
                if ($value === null) {
                    return null;
                }

                $normalized = strtolower(trim((string) $value));

                return $normalized === '' ? null : $normalized;
            },
        ));

        $this->register(new SettingDefinition(
            domain: 'entitlements',
            key: WorkspaceCommercialLifecycleResolver::SETTING_COMMERCIAL_LIFECYCLE_REASON,
            type: 'string',
            systemDefault: null,
            rules: ['nullable', 'string', 'max:500'],
            normalizer: static function (mixed $value): ?string {
                if ($value === null) {
                    return null;
                }

                $normalized = trim((string) $value);

                return $normalized === '' ? null : $normalized;
            },
        ));
    }

    /**
     * @return array<string, SettingDefinition>
     */
    public function all(): array
    {
        return $this->definitions;
    }

    public function find(string $domain, string $key): ?SettingDefinition
    {
        return $this->definitions[$this->cacheKey($domain, $key)] ?? null;
    }

    public function require(string $domain, string $key): SettingDefinition
    {
        $definition = $this->find($domain, $key);

        if ($definition instanceof SettingDefinition) {
            return $definition;
        }

        throw new \InvalidArgumentException(sprintf('Unknown setting key: %s.%s', $domain, $key));
    }

    private function register(SettingDefinition $definition): void
    {
        $this->definitions[$this->cacheKey($definition->domain, $definition->key)] = $definition;
    }

    private function cacheKey(string $domain, string $key): string
    {
        return $domain.'.'.$key;
    }

    /**
     * @return array<int, string>
     */
    private static function supportedFindingSeverities(): array
    {
        return [
            Finding::SEVERITY_LOW,
            Finding::SEVERITY_MEDIUM,
            Finding::SEVERITY_HIGH,
            Finding::SEVERITY_CRITICAL,
        ];
    }

    /**
     * @return array<string, string>
     */
    private static function normalizeSeverityMapping(mixed $value): array
    {
        if (! is_array($value)) {
            return [];
        }

        $normalized = [];

        foreach ($value as $findingType => $severity) {
            if (! is_string($findingType) || trim($findingType) === '' || ! is_string($severity)) {
                continue;
            }

            $normalized[$findingType] = strtolower($severity);
        }

        ksort($normalized);

        return $normalized;
    }

    /**
     * @return array<int, string>
     */
    private static function supportedBaselineChangeTypes(): array
    {
        return [
            'different_version',
            'missing_policy',
            'unexpected_policy',
        ];
    }

    /**
     * @return array<string, string>
     */
    private static function defaultBaselineSeverityMapping(): array
    {
        return [
            'different_version' => Finding::SEVERITY_MEDIUM,
            'missing_policy' => Finding::SEVERITY_HIGH,
            'unexpected_policy' => Finding::SEVERITY_LOW,
        ];
    }

    /**
     * @return array<string, string>
     */
    private static function normalizeBaselineSeverityMapping(mixed $value): array
    {
        if (! is_array($value)) {
            return [];
        }

        $normalized = [];
        $supportedKeys = array_fill_keys(self::supportedBaselineChangeTypes(), true);

        foreach ($value as $changeType => $severity) {
            if (! is_string($changeType) || ! isset($supportedKeys[$changeType]) || ! is_string($severity)) {
                continue;
            }

            $normalized[$changeType] = strtolower($severity);
        }

        $ordered = [];

        foreach (self::defaultBaselineSeverityMapping() as $changeType => $_severity) {
            if (array_key_exists($changeType, $normalized)) {
                $ordered[$changeType] = $normalized[$changeType];
            }
        }

        return $ordered;
    }

    /**
     * @return array<string, int>
     */
    private static function defaultFindingsSlaDays(): array
    {
        return [
            Finding::SEVERITY_CRITICAL => 3,
            Finding::SEVERITY_HIGH => 7,
            Finding::SEVERITY_MEDIUM => 14,
            Finding::SEVERITY_LOW => 30,
        ];
    }

    /**
     * @return array<string, int>
     */
    private static function normalizeFindingsSlaDays(mixed $value): array
    {
        if (! is_array($value)) {
            return self::defaultFindingsSlaDays();
        }

        $normalized = [];

        foreach ($value as $severity => $days) {
            if (! is_string($severity)) {
                continue;
            }

            $normalizedSeverity = strtolower($severity);

            if (! in_array($normalizedSeverity, self::supportedFindingSeverities(), true)) {
                continue;
            }

            $normalizedDays = filter_var($days, FILTER_VALIDATE_INT);

            if ($normalizedDays === false) {
                continue;
            }

            $normalized[$normalizedSeverity] = (int) $normalizedDays;
        }

        $ordered = [];

        foreach (self::defaultFindingsSlaDays() as $severity => $_default) {
            if (array_key_exists($severity, $normalized)) {
                $ordered[$severity] = $normalized[$severity];
            }
        }

        return $ordered;
    }
}
