<?php

declare(strict_types=1);

namespace App\Filament\Pages\Settings;

use App\Models\User;
use App\Models\Workspace;
use App\Models\WorkspaceSetting;
use App\Services\Auth\WorkspaceCapabilityResolver;
use App\Services\Settings\SettingsResolver;
use App\Services\Settings\SettingsWriter;
use App\Support\Auth\Capabilities;
use App\Support\Settings\SettingDefinition;
use App\Support\Settings\SettingsRegistry;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Workspaces\WorkspaceContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use UnitEnum;

class WorkspaceSettings extends Page
{
    protected static bool $isDiscovered = false;

    protected static bool $shouldRegisterNavigation = false;

    protected static ?string $slug = 'settings/workspace';

    protected static ?string $title = 'Workspace settings';

    protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-cog-6-tooth';

    protected static string|UnitEnum|null $navigationGroup = 'Settings';

    protected static ?int $navigationSort = 20;

    /**
     * @var array<string, array{domain: string, key: string, type: 'int'|'json'|'string'|'bool'}>
     */
    private const SETTING_FIELDS = [
        'backup_retention_keep_last_default' => ['domain' => 'backup', 'key' => 'retention_keep_last_default', 'type' => 'int'],
        'backup_retention_min_floor' => ['domain' => 'backup', 'key' => 'retention_min_floor', 'type' => 'int'],
        'drift_severity_mapping' => ['domain' => 'drift', 'key' => 'severity_mapping', 'type' => 'json'],
        'baseline_severity_mapping' => ['domain' => 'baseline', 'key' => 'severity_mapping', 'type' => 'json'],
        'baseline_alert_min_severity' => ['domain' => 'baseline', 'key' => 'alert_min_severity', 'type' => 'string'],
        'baseline_auto_close_enabled' => ['domain' => 'baseline', 'key' => 'auto_close_enabled', 'type' => 'bool'],
        'findings_sla_days' => ['domain' => 'findings', 'key' => 'sla_days', 'type' => 'json'],
        'operations_operation_run_retention_days' => ['domain' => 'operations', 'key' => 'operation_run_retention_days', 'type' => 'int'],
        'operations_stuck_run_threshold_minutes' => ['domain' => 'operations', 'key' => 'stuck_run_threshold_minutes', 'type' => 'int'],
    ];

    /**
     * Fields rendered as Filament KeyValue components (array state, not JSON string).
     *
     * @var array<int, string>
     */
    private const KEYVALUE_FIELDS = [
        'drift_severity_mapping',
    ];

    /**
     * Findings SLA days are decomposed into individual form fields per severity.
     *
     * @var array<string, string>
     */
    private const SLA_SUB_FIELDS = [
        'findings_sla_critical' => 'critical',
        'findings_sla_high' => 'high',
        'findings_sla_medium' => 'medium',
        'findings_sla_low' => 'low',
    ];

    /**
     * Baseline severity mapping is edited as explicit fields per change type.
     *
     * @var array<string, string>
     */
    private const BASELINE_MAPPING_SUB_FIELDS = [
        'baseline_severity_missing_policy' => 'missing_policy',
        'baseline_severity_different_version' => 'different_version',
        'baseline_severity_unexpected_policy' => 'unexpected_policy',
    ];

    public Workspace $workspace;

    /**
     * @var array<string, mixed>
     */
    public array $data = [];

    /**
     * @var array<string, mixed>
     */
    public array $workspaceOverrides = [];

    /**
     * @var array<string, array{source: string, value: mixed, system_default: mixed}>
     */
    public array $resolvedSettings = [];

    /**
     * Per-domain "last modified" metadata: domain => {user_name, updated_at}.
     *
     * @var array<string, array{user_name: string, updated_at: Carbon}>
     */
    public array $domainLastModified = [];

    /**
     * @return array<Action>
     */
    protected function getHeaderActions(): array
    {
        return [
            Action::make('save')
                ->label('Save')
                ->action(function (): void {
                    $this->save();
                })
                ->disabled(fn (): bool => ! $this->currentUserCanManage())
                ->tooltip(fn (): ?string => $this->currentUserCanManage()
                    ? null
                    : 'You do not have permission to manage workspace settings.'),
        ];
    }

    public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
    {
        return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
            ->satisfy(ActionSurfaceSlot::ListHeader, 'Header action saves settings; each setting includes a confirmed reset action.')
            ->exempt(ActionSurfaceSlot::InspectAffordance, 'Workspace settings are edited as a singleton form without a record inspect action.')
            ->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The page does not render table rows with secondary actions.')
            ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The page has no bulk actions because it manages a single settings scope.')
            ->exempt(ActionSurfaceSlot::ListEmptyState, 'The settings form is always rendered and has no list empty state.');
    }

    public function mount(): void
    {
        $user = auth()->user();

        if (! $user instanceof User) {
            abort(403);
        }

        $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());

        if ($workspaceId === null) {
            $this->redirect('/admin/choose-workspace');

            return;
        }

        $workspace = Workspace::query()->whereKey($workspaceId)->first();

        if (! $workspace instanceof Workspace) {
            abort(404);
        }

        $this->workspace = $workspace;

        $this->authorizeWorkspaceView($user);

        $this->loadFormState();
    }

    public function content(Schema $schema): Schema
    {
        return $schema
            ->statePath('data')
            ->schema([
                Section::make('Backup settings')
                    ->description($this->sectionDescription('backup', 'Workspace defaults used when a schedule has no explicit value.'))
                    ->schema([
                        TextInput::make('backup_retention_keep_last_default')
                            ->label('Default retention keep-last')
                            ->placeholder('Unset (uses default)')
                            ->suffix('versions')
                            ->hint('1 – 365')
                            ->numeric()
                            ->integer()
                            ->minValue(1)
                            ->maxValue(365)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->helperTextFor('backup_retention_keep_last_default'))
                            ->hintAction($this->makeResetAction('backup_retention_keep_last_default')),
                        TextInput::make('backup_retention_min_floor')
                            ->label('Minimum retention floor')
                            ->placeholder('Unset (uses default)')
                            ->suffix('versions')
                            ->hint('1 – 365')
                            ->numeric()
                            ->integer()
                            ->minValue(1)
                            ->maxValue(365)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->helperTextFor('backup_retention_min_floor'))
                            ->hintAction($this->makeResetAction('backup_retention_min_floor')),
                    ]),
                Section::make('Drift settings')
                    ->description($this->sectionDescription('drift', 'Map finding types to severity levels. Allowed severities: critical, high, medium, low.'))
                    ->schema([
                        KeyValue::make('drift_severity_mapping')
                            ->label('Severity mapping')
                            ->keyLabel('Finding type')
                            ->valueLabel('Severity')
                            ->keyPlaceholder('e.g. drift')
                            ->valuePlaceholder('critical, high, medium, or low')
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->helperTextFor('drift_severity_mapping'))
                            ->hintAction($this->makeResetAction('drift_severity_mapping')),
                    ]),
                Section::make('Baseline settings')
                    ->key('baseline_section')
                    ->description($this->sectionDescription('baseline', 'Tune baseline drift severity mapping, alert threshold, and stale-finding auto-close behavior.'))
                    ->columns(2)
                    ->afterHeader([
                        $this->makeResetAction('baseline_severity_mapping')->label('Reset mapping')->size('sm'),
                        $this->makeResetAction('baseline_alert_min_severity')->label('Reset threshold')->size('sm'),
                        $this->makeResetAction('baseline_auto_close_enabled')->label('Reset auto-close')->size('sm'),
                    ])
                    ->schema([
                        Select::make('baseline_severity_missing_policy')
                            ->label('Missing policy severity')
                            ->options(self::severityOptions())
                            ->placeholder('Unset (uses default)')
                            ->native(false)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->baselineSeverityFieldHelperText('missing_policy')),
                        Select::make('baseline_severity_different_version')
                            ->label('Different version severity')
                            ->options(self::severityOptions())
                            ->placeholder('Unset (uses default)')
                            ->native(false)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->baselineSeverityFieldHelperText('different_version')),
                        Select::make('baseline_severity_unexpected_policy')
                            ->label('Unexpected policy severity')
                            ->options(self::severityOptions())
                            ->placeholder('Unset (uses default)')
                            ->native(false)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->baselineSeverityFieldHelperText('unexpected_policy')),
                        Select::make('baseline_alert_min_severity')
                            ->label('Minimum alert severity')
                            ->options(self::severityOptions())
                            ->placeholder('Unset (uses default)')
                            ->native(false)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->helperTextFor('baseline_alert_min_severity')),
                        Select::make('baseline_auto_close_enabled')
                            ->label('Auto-close stale drift')
                            ->options(self::booleanOptions())
                            ->placeholder('Unset (uses default)')
                            ->native(false)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->helperTextFor('baseline_auto_close_enabled')),
                    ]),
                Section::make('Findings settings')
                    ->key('findings_section')
                    ->description($this->sectionDescription('findings', 'Configure workspace-wide SLA days by severity. Set one or more, or leave all empty to use the system default. Unset severities use their default.'))
                    ->columns(2)
                    ->afterHeader([
                        $this->makeResetAction('findings_sla_days')->label('Reset all SLA')->size('sm'),
                    ])
                    ->schema([
                        TextInput::make('findings_sla_critical')
                            ->label('Critical severity')
                            ->placeholder('Unset (uses default)')
                            ->suffix('days')
                            ->hint('1 – 3,650')
                            ->numeric()
                            ->integer()
                            ->minValue(1)
                            ->maxValue(3650)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->slaFieldHelperText('critical')),
                        TextInput::make('findings_sla_high')
                            ->label('High severity')
                            ->placeholder('Unset (uses default)')
                            ->suffix('days')
                            ->hint('1 – 3,650')
                            ->numeric()
                            ->integer()
                            ->minValue(1)
                            ->maxValue(3650)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->slaFieldHelperText('high')),
                        TextInput::make('findings_sla_medium')
                            ->label('Medium severity')
                            ->placeholder('Unset (uses default)')
                            ->suffix('days')
                            ->hint('1 – 3,650')
                            ->numeric()
                            ->integer()
                            ->minValue(1)
                            ->maxValue(3650)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->slaFieldHelperText('medium')),
                        TextInput::make('findings_sla_low')
                            ->label('Low severity')
                            ->placeholder('Unset (uses default)')
                            ->suffix('days')
                            ->hint('1 – 3,650')
                            ->numeric()
                            ->integer()
                            ->minValue(1)
                            ->maxValue(3650)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->slaFieldHelperText('low')),
                    ]),
                Section::make('Operations settings')
                    ->description($this->sectionDescription('operations', 'Workspace controls for operations retention and thresholds.'))
                    ->schema([
                        TextInput::make('operations_operation_run_retention_days')
                            ->label('Operation run retention')
                            ->placeholder('Unset (uses default)')
                            ->suffix('days')
                            ->hint('7 – 3,650')
                            ->numeric()
                            ->integer()
                            ->minValue(7)
                            ->maxValue(3650)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->helperTextFor('operations_operation_run_retention_days'))
                            ->hintAction($this->makeResetAction('operations_operation_run_retention_days')),
                        TextInput::make('operations_stuck_run_threshold_minutes')
                            ->label('Stuck run threshold')
                            ->placeholder('Unset (uses default)')
                            ->suffix('minutes')
                            ->hint('0 – 10,080')
                            ->numeric()
                            ->integer()
                            ->minValue(0)
                            ->maxValue(10080)
                            ->disabled(fn (): bool => ! $this->currentUserCanManage())
                            ->helperText(fn (): string => $this->helperTextFor('operations_stuck_run_threshold_minutes'))
                            ->hintAction($this->makeResetAction('operations_stuck_run_threshold_minutes')),
                    ]),
            ]);
    }

    public function save(): void
    {
        $user = auth()->user();

        if (! $user instanceof User) {
            abort(403);
        }

        $this->authorizeWorkspaceManage($user);

        $this->resetValidation();

        $this->composeBaselineSeveritySubFieldsIntoData();
        $this->composeSlaSubFieldsIntoData();

        [$normalizedValues, $validationErrors] = $this->normalizedInputValues();

        if ($validationErrors !== []) {
            throw ValidationException::withMessages($validationErrors);
        }

        $writer = app(SettingsWriter::class);
        $changedSettingsCount = 0;

        foreach (self::SETTING_FIELDS as $field => $setting) {
            $incomingValue = $normalizedValues[$field] ?? null;
            $currentOverride = $this->workspaceOverrideForField($field);

            if ($incomingValue === null) {
                if ($currentOverride === null) {
                    continue;
                }

                $writer->resetWorkspaceSetting(
                    actor: $user,
                    workspace: $this->workspace,
                    domain: $setting['domain'],
                    key: $setting['key'],
                );

                $changedSettingsCount++;

                continue;
            }

            if ($this->valuesEqual($incomingValue, $currentOverride)) {
                continue;
            }

            $writer->updateWorkspaceSetting(
                actor: $user,
                workspace: $this->workspace,
                domain: $setting['domain'],
                key: $setting['key'],
                value: $incomingValue,
            );

            $changedSettingsCount++;
        }

        $this->loadFormState();

        Notification::make()
            ->title($changedSettingsCount > 0 ? 'Workspace settings saved' : 'No settings changes to save')
            ->success()
            ->send();
    }

    public function resetSetting(string $field): void
    {
        $user = auth()->user();

        if (! $user instanceof User) {
            abort(403);
        }

        $this->authorizeWorkspaceManage($user);

        $setting = $this->settingForField($field);

        if ($this->workspaceOverrideForField($field) === null) {
            Notification::make()
                ->title('Setting already uses default')
                ->success()
                ->send();

            return;
        }

        app(SettingsWriter::class)->resetWorkspaceSetting(
            actor: $user,
            workspace: $this->workspace,
            domain: $setting['domain'],
            key: $setting['key'],
        );

        $this->loadFormState();

        Notification::make()
            ->title('Workspace setting reset to default')
            ->success()
            ->send();
    }

    private function loadFormState(): void
    {
        $resolver = app(SettingsResolver::class);

        $data = [];
        $workspaceOverrides = [];
        $resolvedSettings = [];

        foreach (self::SETTING_FIELDS as $field => $setting) {
            $resolved = $resolver->resolveDetailed(
                workspace: $this->workspace,
                domain: $setting['domain'],
                key: $setting['key'],
            );

            $workspaceValue = $resolved['workspace_value'];

            $workspaceOverrides[$field] = $workspaceValue;
            $resolvedSettings[$field] = [
                'source' => $resolved['source'],
                'value' => $resolved['value'],
                'system_default' => $resolved['system_default'],
            ];

            $data[$field] = $workspaceValue === null
                ? (in_array($field, self::KEYVALUE_FIELDS, true) ? [] : null)
                : $this->formatValueForInput($field, $workspaceValue);
        }

        $this->decomposeBaselineSeveritySubFields($data, $workspaceOverrides);
        $this->decomposeSlaSubFields($data, $workspaceOverrides, $resolvedSettings);

        $this->data = $data;
        $this->workspaceOverrides = $workspaceOverrides;
        $this->resolvedSettings = $resolvedSettings;

        $this->loadDomainLastModified();
    }

    /**
     * Load per-domain "last modified" metadata from workspace_settings.
     */
    private function loadDomainLastModified(): void
    {
        $domains = array_unique(array_column(self::SETTING_FIELDS, 'domain'));

        $records = WorkspaceSetting::query()
            ->where('workspace_id', (int) $this->workspace->getKey())
            ->whereIn('domain', $domains)
            ->whereNotNull('updated_by_user_id')
            ->with('updatedByUser:id,name')
            ->get();

        $domainInfo = [];

        foreach ($records as $record) {
            /** @var WorkspaceSetting $record */
            $domain = $record->domain;
            $updatedAt = $record->updated_at;

            if (! $updatedAt instanceof Carbon) {
                continue;
            }

            if (isset($domainInfo[$domain]) && $domainInfo[$domain]['updated_at']->gte($updatedAt)) {
                continue;
            }

            $user = $record->updatedByUser;

            $domainInfo[$domain] = [
                'user_name' => $user instanceof User ? $user->name : 'Unknown',
                'updated_at' => $updatedAt,
            ];
        }

        $this->domainLastModified = $domainInfo;
    }

    /**
     * Build a section description that appends "last modified" info when available.
     */
    private function sectionDescription(string $domain, string $baseDescription): string
    {
        $meta = $this->domainLastModified[$domain] ?? null;

        if (! is_array($meta)) {
            return $baseDescription;
        }

        /** @var Carbon $updatedAt */
        $updatedAt = $meta['updated_at'];

        return sprintf(
            '%s — Last modified by %s, %s.',
            $baseDescription,
            $meta['user_name'],
            $updatedAt->diffForHumans(),
        );
    }

    private function makeResetAction(string $field): Action
    {
        return Action::make('reset_'.$field)
            ->label('Reset')
            ->color('danger')
            ->requiresConfirmation()
            ->action(function () use ($field): void {
                $this->resetSetting($field);
            })
            ->disabled(fn (): bool => ! $this->currentUserCanManage() || ! $this->hasWorkspaceOverride($field))
            ->tooltip(function () use ($field): ?string {
                if (! $this->currentUserCanManage()) {
                    return 'You do not have permission to manage workspace settings.';
                }

                if (! $this->hasWorkspaceOverride($field)) {
                    return 'No workspace override to reset.';
                }

                return null;
            });
    }

    private function helperTextFor(string $field): string
    {
        $resolved = $this->resolvedSettings[$field] ?? null;

        if (! is_array($resolved)) {
            return '';
        }

        $effectiveValue = $this->formatValueForDisplay($field, $resolved['value'] ?? null);

        if (! $this->hasWorkspaceOverride($field)) {
            return sprintf(
                'Unset. Effective value: %s (%s).',
                $effectiveValue,
                $this->sourceLabel((string) ($resolved['source'] ?? 'system_default')),
            );
        }

        return sprintf('Effective value: %s.', $effectiveValue);
    }

    private function slaFieldHelperText(string $severity): string
    {
        $resolved = $this->resolvedSettings['findings_sla_days'] ?? null;

        if (! is_array($resolved)) {
            return '';
        }

        $effectiveValue = is_array($resolved['value'] ?? null)
            ? (int) ($resolved['value'][$severity] ?? 0)
            : 0;

        $systemDefault = is_array($resolved['system_default'] ?? null)
            ? (int) ($resolved['system_default'][$severity] ?? 0)
            : 0;

        if (! $this->hasWorkspaceOverride('findings_sla_days')) {
            return sprintf('Default: %d days.', $systemDefault);
        }

        return sprintf('Effective: %d days.', $effectiveValue);
    }

    private function baselineSeverityFieldHelperText(string $changeType): string
    {
        $resolved = $this->resolvedSettings['baseline_severity_mapping'] ?? null;

        if (! is_array($resolved)) {
            return '';
        }

        $effectiveValue = is_array($resolved['value'] ?? null)
            ? (string) ($resolved['value'][$changeType] ?? '')
            : '';

        $systemDefault = is_array($resolved['system_default'] ?? null)
            ? (string) ($resolved['system_default'][$changeType] ?? '')
            : '';

        if (! $this->hasWorkspaceOverride('baseline_severity_mapping')) {
            return sprintf('Default: %s.', $systemDefault);
        }

        if (
            is_array($this->workspaceOverrideForField('baseline_severity_mapping'))
            && array_key_exists($changeType, $this->workspaceOverrideForField('baseline_severity_mapping'))
        ) {
            return sprintf('Effective: %s.', $effectiveValue);
        }

        return sprintf('Unset. Effective value: %s (system default).', $effectiveValue);
    }

    /**
     * @return array{0: array<string, mixed>, 1: array<string, array<int, string>>}
     */
    private function normalizedInputValues(): array
    {
        $normalizedValues = [];
        $validationErrors = [];

        foreach (self::SETTING_FIELDS as $field => $_setting) {
            try {
                $normalizedValues[$field] = $this->normalizeFieldInput(
                    field: $field,
                    value: $this->data[$field] ?? null,
                );
            } catch (ValidationException $exception) {
                $messages = [];

                foreach ($exception->errors() as $errorMessages) {
                    foreach ((array) $errorMessages as $message) {
                        $messages[] = (string) $message;
                    }
                }

                if ($field === 'findings_sla_days') {
                    $severityToField = array_flip(self::SLA_SUB_FIELDS);

                    $targeted = false;

                    foreach ($messages as $message) {
                        if (preg_match('/include "(?<severity>critical|high|medium|low)"/i', $message, $matches) === 1) {
                            $severity = strtolower((string) $matches['severity']);
                            $subField = $severityToField[$severity] ?? null;

                            if (is_string($subField)) {
                                $validationErrors['data.'.$subField] ??= [];
                                $validationErrors['data.'.$subField][] = $message;
                                $targeted = true;
                            }
                        }
                    }

                    if (! $targeted) {
                        foreach (self::SLA_SUB_FIELDS as $subField => $_severity) {
                            $validationErrors['data.'.$subField] = $messages !== []
                                ? $messages
                                : ['Invalid value.'];
                        }
                    }

                    continue;
                }

                if ($field === 'baseline_severity_mapping') {
                    foreach (array_keys(self::BASELINE_MAPPING_SUB_FIELDS) as $subField) {
                        $validationErrors['data.'.$subField] = $messages !== []
                            ? $messages
                            : ['Invalid value.'];
                    }

                    continue;
                }

                $validationErrors['data.'.$field] = $messages !== []
                    ? $messages
                    : ['Invalid value.'];
            }
        }

        return [$normalizedValues, $validationErrors];
    }

    private function normalizeFieldInput(string $field, mixed $value): mixed
    {
        $setting = $this->settingForField($field);

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

        if (is_string($value) && trim($value) === '') {
            return null;
        }

        if (is_array($value) && $value === []) {
            return null;
        }

        if ($setting['type'] === 'json') {
            $value = $this->normalizeJsonInput($value);

            if (in_array($field, self::KEYVALUE_FIELDS, true)) {
                $value = $this->normalizeKeyValueInput($value);

                if ($value === []) {
                    return null;
                }
            }
        }

        $definition = $this->settingDefinition($field);

        $validator = Validator::make(
            data: ['value' => $value],
            rules: ['value' => $definition->rules],
        );

        if ($validator->fails()) {
            throw ValidationException::withMessages($validator->errors()->toArray());
        }

        return $definition->normalize($validator->validated()['value']);
    }

    /**
     * Normalize KeyValue component state.
     *
     * Filament's KeyValue UI keeps an empty row by default, which can submit as
     * ['' => ''] and would otherwise fail validation. We treat empty rows as unset.
     *
     * @param  array<mixed>  $value
     * @return array<string, mixed>
     */
    private function normalizeKeyValueInput(array $value): array
    {
        $normalized = [];

        foreach ($value as $key => $item) {
            if (is_array($item) && array_key_exists('key', $item)) {
                $rowKey = $item['key'];
                $rowValue = $item['value'] ?? null;

                if (! is_string($rowKey)) {
                    continue;
                }

                $trimmedKey = trim($rowKey);

                if ($trimmedKey === '') {
                    continue;
                }

                if (is_string($rowValue)) {
                    $trimmedValue = trim($rowValue);

                    if ($trimmedValue === '') {
                        continue;
                    }

                    $normalized[$trimmedKey] = $trimmedValue;

                    continue;
                }

                if ($rowValue === null) {
                    continue;
                }

                $normalized[$trimmedKey] = $rowValue;

                continue;
            }

            if (! is_string($key)) {
                continue;
            }

            $trimmedKey = trim($key);

            if ($trimmedKey === '') {
                continue;
            }

            if (is_string($item)) {
                $trimmedValue = trim($item);

                if ($trimmedValue === '') {
                    continue;
                }

                $normalized[$trimmedKey] = $trimmedValue;

                continue;
            }

            if ($item === null) {
                continue;
            }

            $normalized[$trimmedKey] = $item;
        }

        return $normalized;
    }

    private function normalizeJsonInput(mixed $value): array
    {
        if (is_array($value)) {
            return $value;
        }

        if (! is_string($value)) {
            throw ValidationException::withMessages([
                'value' => ['The value must be valid JSON.'],
            ]);
        }

        $decoded = json_decode($value, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            throw ValidationException::withMessages([
                'value' => ['The value must be valid JSON.'],
            ]);
        }

        if (! is_array($decoded)) {
            throw ValidationException::withMessages([
                'value' => ['The value must be a JSON object.'],
            ]);
        }

        return $decoded;
    }

    private function valuesEqual(mixed $left, mixed $right): bool
    {
        if ($left === null || $right === null) {
            return $left === $right;
        }

        if (is_array($left) && is_array($right)) {
            return $this->encodeCanonicalArray($left) === $this->encodeCanonicalArray($right);
        }

        if (is_numeric($left) && is_numeric($right)) {
            return (int) $left === (int) $right;
        }

        return $left === $right;
    }

    private function encodeCanonicalArray(array $value): string
    {
        $encoded = json_encode($this->sortNestedArray($value));

        return is_string($encoded) ? $encoded : '';
    }

    /**
     * @param  array<mixed>  $value
     * @return array<mixed>
     */
    private function sortNestedArray(array $value): array
    {
        foreach ($value as $key => $item) {
            if (! is_array($item)) {
                continue;
            }

            $value[$key] = $this->sortNestedArray($item);
        }

        ksort($value);

        return $value;
    }

    private function formatValueForInput(string $field, mixed $value): mixed
    {
        $setting = $this->settingForField($field);

        if ($setting['type'] === 'json') {
            if (! is_array($value)) {
                return null;
            }

            if (in_array($field, self::KEYVALUE_FIELDS, true)) {
                return $value;
            }

            $encoded = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

            return is_string($encoded) ? $encoded : null;
        }

        if ($setting['type'] === 'string') {
            return is_string($value) ? $value : null;
        }

        if ($setting['type'] === 'bool') {
            if (is_bool($value)) {
                return $value ? '1' : '0';
            }

            return is_numeric($value) ? (string) (int) $value : null;
        }

        return is_numeric($value) ? (int) $value : null;
    }

    private function formatValueForDisplay(string $field, mixed $value): string
    {
        $setting = $this->settingForField($field);

        if ($setting['type'] === 'json') {
            if (! is_array($value) || $value === []) {
                return '{}';
            }

            $encoded = json_encode($value, JSON_UNESCAPED_SLASHES);

            return is_string($encoded) ? $encoded : '{}';
        }

        if ($setting['type'] === 'string') {
            return is_string($value) && $value !== '' ? $value : 'null';
        }

        if ($setting['type'] === 'bool') {
            if (is_bool($value)) {
                return $value ? 'enabled' : 'disabled';
            }

            return 'null';
        }

        return is_numeric($value) ? (string) (int) $value : 'null';
    }

    private function sourceLabel(string $source): string
    {
        return match ($source) {
            'workspace_override' => 'workspace override',
            'tenant_override' => 'tenant override',
            default => 'system default',
        };
    }

    /**
     * @return array{domain: string, key: string, type: 'int'|'json'|'string'|'bool'}
     */
    private function settingForField(string $field): array
    {
        if (! isset(self::SETTING_FIELDS[$field])) {
            throw ValidationException::withMessages([
                'data' => [sprintf('Unknown settings field: %s', $field)],
            ]);
        }

        return self::SETTING_FIELDS[$field];
    }

    private function settingDefinition(string $field): SettingDefinition
    {
        $setting = $this->settingForField($field);

        return app(SettingsRegistry::class)->require($setting['domain'], $setting['key']);
    }

    private function hasWorkspaceOverride(string $field): bool
    {
        return $this->workspaceOverrideForField($field) !== null;
    }

    private function workspaceOverrideForField(string $field): mixed
    {
        return $this->workspaceOverrides[$field] ?? null;
    }

    /**
     * Decompose the baseline severity mapping JSON setting into explicit change-type sub-fields.
     *
     * @param  array<string, mixed>  $data
     * @param  array<string, mixed>  $workspaceOverrides
     */
    private function decomposeBaselineSeveritySubFields(array &$data, array &$workspaceOverrides): void
    {
        $mappingOverride = $workspaceOverrides['baseline_severity_mapping'] ?? null;

        foreach (self::BASELINE_MAPPING_SUB_FIELDS as $subField => $changeType) {
            $data[$subField] = is_array($mappingOverride) && isset($mappingOverride[$changeType])
                ? (string) $mappingOverride[$changeType]
                : null;
        }
    }

    /**
     * Decompose the findings_sla_days JSON setting into individual SLA sub-fields.
     *
     * @param  array<string, mixed>  $data
     * @param  array<string, mixed>  $workspaceOverrides
     * @param  array<string, array{source: string, value: mixed, system_default: mixed}>  $resolvedSettings
     */
    private function decomposeSlaSubFields(array &$data, array &$workspaceOverrides, array &$resolvedSettings): void
    {
        $slaOverride = $workspaceOverrides['findings_sla_days'] ?? null;
        $slaResolved = $resolvedSettings['findings_sla_days'] ?? null;

        foreach (self::SLA_SUB_FIELDS as $subField => $severity) {
            $data[$subField] = is_array($slaOverride) && isset($slaOverride[$severity])
                ? (int) $slaOverride[$severity]
                : null;
        }
    }

    /**
     * Re-compose baseline severity mapping sub-fields back into the baseline_severity_mapping data key before save.
     */
    private function composeBaselineSeveritySubFieldsIntoData(): void
    {
        $values = [];

        foreach (self::BASELINE_MAPPING_SUB_FIELDS as $subField => $changeType) {
            $value = $this->data[$subField] ?? null;

            if (! is_string($value) || trim($value) === '') {
                continue;
            }

            $values[$changeType] = strtolower(trim($value));
        }

        $this->data['baseline_severity_mapping'] = $values !== [] ? $values : null;
    }

    /**
     * Re-compose individual SLA sub-fields back into the findings_sla_days data key before save.
     */
    private function composeSlaSubFieldsIntoData(): void
    {
        $values = [];
        $hasAnyValue = false;

        foreach (self::SLA_SUB_FIELDS as $subField => $severity) {
            $val = $this->data[$subField] ?? null;

            if ($val !== null && (is_string($val) ? trim($val) !== '' : true)) {
                $values[$severity] = (int) $val;
                $hasAnyValue = true;
            }
        }

        $this->data['findings_sla_days'] = $hasAnyValue ? $values : null;
    }

    /**
     * @return array<string, string>
     */
    private static function severityOptions(): array
    {
        return [
            'low' => 'Low',
            'medium' => 'Medium',
            'high' => 'High',
            'critical' => 'Critical',
        ];
    }

    /**
     * @return array<string, string>
     */
    private static function booleanOptions(): array
    {
        return [
            '1' => 'Enabled',
            '0' => 'Disabled',
        ];
    }

    private function currentUserCanManage(): bool
    {
        $user = auth()->user();

        if (! $user instanceof User || ! $this->workspace instanceof Workspace) {
            return false;
        }

        /** @var WorkspaceCapabilityResolver $resolver */
        $resolver = app(WorkspaceCapabilityResolver::class);

        return $resolver->isMember($user, $this->workspace)
            && $resolver->can($user, $this->workspace, Capabilities::WORKSPACE_SETTINGS_MANAGE);
    }

    private function authorizeWorkspaceView(User $user): void
    {
        /** @var WorkspaceCapabilityResolver $resolver */
        $resolver = app(WorkspaceCapabilityResolver::class);

        if (! $resolver->isMember($user, $this->workspace)) {
            abort(404);
        }

        if (! $resolver->can($user, $this->workspace, Capabilities::WORKSPACE_SETTINGS_VIEW)) {
            abort(403);
        }
    }

    private function authorizeWorkspaceManage(User $user): void
    {
        /** @var WorkspaceCapabilityResolver $resolver */
        $resolver = app(WorkspaceCapabilityResolver::class);

        if (! $resolver->isMember($user, $this->workspace)) {
            abort(404);
        }

        if (! $resolver->can($user, $this->workspace, Capabilities::WORKSPACE_SETTINGS_MANAGE)) {
            abort(403);
        }
    }
}
