<?php

declare(strict_types=1);

namespace App\Support\Baselines;

use App\Support\Governance\GovernanceDomainKey;
use App\Support\Governance\GovernanceSubjectClass;
use App\Support\Governance\GovernanceSubjectTaxonomyRegistry;
use App\Support\Governance\PlatformSubjectDescriptorNormalizer;
use App\Support\Inventory\InventoryPolicyTypeMeta;
use InvalidArgumentException;

/**
 * Value object for baseline scope resolution.
 *
 * Canonical storage uses versioned Governance Scope V2 entries.
 * Presentation compatibility for the current Intune-first UI still projects
 * back to legacy policy and foundation buckets.
 */
final class BaselineScope
{
    /**
     * @param  list<string>  $policyTypes
     * @param  list<string>  $foundationTypes
     * @param  list<array{domain_key: string, subject_class: string, subject_type_keys: list<string>, filters: array<string, mixed>}>  $entries
     * @param  list<string>  $legacyKeysPresent
     */
    public function __construct(
        public readonly array $policyTypes = [],
        public readonly array $foundationTypes = [],
        public readonly array $entries = [],
        public readonly int $version = 2,
        public readonly string $sourceShape = 'canonical_v2',
        public readonly bool $normalizedOnRead = false,
        public readonly array $legacyKeysPresent = [],
        public readonly bool $saveForwardRequired = false,
    ) {}

    /**
     * Create from the scope_jsonb column value.
     *
     * @param  array<string, mixed>|null  $scopeJsonb
     */
    public static function fromJsonb(?array $scopeJsonb, bool $allowEmptyLegacyAsNoOverride = false): self
    {
        if ($scopeJsonb === null) {
            return self::fromLegacyPayload(
                policyTypes: [],
                foundationTypes: [],
                legacyKeysPresent: [],
                normalizedOnRead: true,
                saveForwardRequired: true,
            );
        }

        if (isset($scopeJsonb['canonical_scope']) && is_array($scopeJsonb['canonical_scope'])) {
            return self::fromJsonb($scopeJsonb['canonical_scope'], $allowEmptyLegacyAsNoOverride);
        }

        $hasLegacyKeys = array_key_exists('policy_types', $scopeJsonb) || array_key_exists('foundation_types', $scopeJsonb);
        $hasCanonicalKeys = array_key_exists('version', $scopeJsonb) || array_key_exists('entries', $scopeJsonb);

        if ($hasLegacyKeys && $hasCanonicalKeys) {
            throw new InvalidArgumentException('Baseline scope payload must not mix legacy buckets with canonical V2 keys.');
        }

        if ($hasCanonicalKeys) {
            return self::fromCanonicalPayload($scopeJsonb);
        }

        if (! $hasLegacyKeys) {
            throw new InvalidArgumentException('Baseline scope payload must contain either legacy buckets or canonical V2 keys.');
        }

        $legacyKeysPresent = array_values(array_filter([
            array_key_exists('policy_types', $scopeJsonb) ? 'policy_types' : null,
            array_key_exists('foundation_types', $scopeJsonb) ? 'foundation_types' : null,
        ]));

        $policyTypes = self::stringList($scopeJsonb['policy_types'] ?? []);
        $foundationTypes = self::stringList($scopeJsonb['foundation_types'] ?? []);

        if ($allowEmptyLegacyAsNoOverride && $policyTypes === [] && $foundationTypes === []) {
            return new self(
                policyTypes: [],
                foundationTypes: [],
                entries: [],
                sourceShape: 'legacy',
                normalizedOnRead: false,
                legacyKeysPresent: $legacyKeysPresent,
                saveForwardRequired: false,
            );
        }

        return self::fromLegacyPayload(
            policyTypes: $policyTypes,
            foundationTypes: $foundationTypes,
            legacyKeysPresent: $legacyKeysPresent,
            normalizedOnRead: true,
            saveForwardRequired: true,
        );
    }

    /**
     * Normalize the effective scope by intersecting profile scope with an optional override.
     *
     * Override can only narrow the profile scope (subset enforcement).
     * Empty override means "no override".
     */
    public static function effective(self $profileScope, ?self $overrideScope): self
    {
        $profileScope = $profileScope->expandDefaults();

        if ($overrideScope === null || $overrideScope->isEmpty()) {
            return $profileScope;
        }

        $overridePolicyTypes = self::normalizePolicyTypes($overrideScope->policyTypes);
        $overrideFoundationTypes = self::normalizeFoundationTypes($overrideScope->foundationTypes);
        $entries = [];

        foreach ($profileScope->entries as $entry) {
            $subjectTypeKeys = $entry['subject_type_keys'];

            if ($entry['domain_key'] === GovernanceDomainKey::Intune->value
                && $entry['subject_class'] === GovernanceSubjectClass::Policy->value
                && $overridePolicyTypes !== []) {
                $subjectTypeKeys = array_values(array_intersect($subjectTypeKeys, $overridePolicyTypes));
            }

            if ($entry['domain_key'] === GovernanceDomainKey::PlatformFoundation->value
                && $entry['subject_class'] === GovernanceSubjectClass::ConfigurationResource->value
                && $overrideFoundationTypes !== []) {
                $subjectTypeKeys = array_values(array_intersect($subjectTypeKeys, $overrideFoundationTypes));
            }

            $subjectTypeKeys = self::uniqueSorted($subjectTypeKeys);

            if ($subjectTypeKeys === []) {
                continue;
            }

            $entries[] = [
                'domain_key' => $entry['domain_key'],
                'subject_class' => $entry['subject_class'],
                'subject_type_keys' => $subjectTypeKeys,
                'filters' => $entry['filters'],
            ];
        }

        return self::fromCanonicalPayload([
            'version' => 2,
            'entries' => $entries,
        ]);
    }

    /**
     * An empty scope means "no override" (for override_scope semantics).
     */
    public function isEmpty(): bool
    {
        return $this->entries === [] && $this->policyTypes === [] && $this->foundationTypes === [];
    }

    /**
     * Apply Spec 116 defaults and filter to supported types.
     */
    public function expandDefaults(): self
    {
        if ($this->entries !== []) {
            return $this;
        }

        return self::fromLegacyPayload(
            policyTypes: $this->policyTypes,
            foundationTypes: $this->foundationTypes,
            legacyKeysPresent: $this->legacyKeysPresent,
            normalizedOnRead: $this->normalizedOnRead,
            saveForwardRequired: $this->saveForwardRequired,
        );
    }

    /**
     * @return list<string>
     */
    public function allTypes(): array
    {
        $expanded = $this->expandDefaults();

        $canonicalTypeKeys = [];

        foreach ($expanded->entries as $entry) {
            $canonicalTypeKeys = array_merge(
                $canonicalTypeKeys,
                is_array($entry['subject_type_keys'] ?? null) ? $entry['subject_type_keys'] : [],
            );
        }

        return self::uniqueSorted(array_merge(
            $expanded->policyTypes,
            $expanded->foundationTypes,
            $canonicalTypeKeys,
        ));
    }

    /**
     * @return list<string>
     */
    public function truthfulTypes(string $operation, ?BaselineSupportCapabilityGuard $guard = null): array
    {
        $guard ??= app(BaselineSupportCapabilityGuard::class);
        $guardResult = $guard->guardTypes($this->allTypes(), $operation);

        return $guardResult['allowed_types'];
    }

    /**
     * @return array<string, mixed>
     */
    public function toJsonb(): array
    {
        $supportedPolicyTypes = self::supportedPolicyTypes();
        $policyTypes = $this->policyTypes;

        if ($policyTypes === $supportedPolicyTypes) {
            $policyTypes = [];
        }

        return [
            'policy_types' => $policyTypes,
            'foundation_types' => $this->foundationTypes,
        ];
    }

    /**
     * @return array{version: 2, entries: list<array{domain_key: string, subject_class: string, subject_type_keys: list<string>, filters: array<string, mixed>}>}
     */
    public function toStoredJsonb(): array
    {
        return [
            'version' => 2,
            'entries' => $this->entries,
        ];
    }

    /**
     * @return array{source_shape: string, normalized_on_read: bool, legacy_keys_present: list<string>, save_forward_required: bool}
     */
    public function normalizationLineage(): array
    {
        return [
            'source_shape' => $this->sourceShape,
            'normalized_on_read' => $this->normalizedOnRead,
            'legacy_keys_present' => $this->legacyKeysPresent,
            'save_forward_required' => $this->saveForwardRequired,
        ];
    }

    /**
     * @return list<array{domain_key: string, subject_class: string, group_label: string, selected_subject_types: list<string>, capture_supported_count: int, compare_supported_count: int, inactive_count: int}>
     */
    public function summaryGroups(?GovernanceSubjectTaxonomyRegistry $registry = null): array
    {
        $registry ??= app(GovernanceSubjectTaxonomyRegistry::class);
        $groups = [];

        foreach ($this->entries as $entry) {
            $selectedSubjectTypes = [];
            $captureSupportedCount = 0;
            $compareSupportedCount = 0;
            $inactiveCount = 0;

            foreach ($entry['subject_type_keys'] as $subjectTypeKey) {
                $subjectType = $registry->find($entry['domain_key'], $subjectTypeKey);
                $selectedSubjectTypes[] = $subjectType?->label ?? $subjectTypeKey;

                if ($subjectType?->captureSupported) {
                    $captureSupportedCount++;
                }

                if ($subjectType?->compareSupported) {
                    $compareSupportedCount++;
                }

                if ($subjectType !== null && ! $subjectType->active) {
                    $inactiveCount++;
                }
            }

            sort($selectedSubjectTypes, SORT_STRING);

            $groups[] = [
                'domain_key' => $entry['domain_key'],
                'subject_class' => $entry['subject_class'],
                'group_label' => $registry->groupLabel($entry['domain_key'], $entry['subject_class']),
                'selected_subject_types' => $selectedSubjectTypes,
                'capture_supported_count' => $captureSupportedCount,
                'compare_supported_count' => $compareSupportedCount,
                'inactive_count' => $inactiveCount,
            ];
        }

        return $groups;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function subjectDescriptors(?PlatformSubjectDescriptorNormalizer $normalizer = null): array
    {
        $normalizer ??= app(PlatformSubjectDescriptorNormalizer::class);

        return $normalizer->descriptorsForScopeEntries($this->entries);
    }

    /**
     * Effective scope payload for OperationRun.context.
     *
     * @return array<string, mixed>
     */
    public function toEffectiveScopeContext(?BaselineSupportCapabilityGuard $guard = null, ?string $operation = null): array
    {
        $expanded = $this->expandDefaults();
        $allTypes = $expanded->allTypes();

        $context = [
            'canonical_scope' => $expanded->toStoredJsonb(),
            'legacy_projection' => $expanded->toJsonb(),
            'policy_types' => $expanded->policyTypes,
            'foundation_types' => $expanded->foundationTypes,
            'all_types' => $allTypes,
            'selected_type_keys' => $allTypes,
            'foundations_included' => $expanded->foundationTypes !== [],
            'governed_subjects' => $expanded->subjectDescriptors(),
        ];

        if (! is_string($operation) || $operation === '') {
            return $context;
        }

        $guard ??= app(BaselineSupportCapabilityGuard::class);
        $guardResult = $guard->guardTypes($allTypes, $operation);

        return array_merge($context, [
            'truthful_types' => $guardResult['allowed_types'],
            'limited_types' => $guardResult['limited_types'],
            'unsupported_types' => $guardResult['unsupported_types'],
            'invalid_support_types' => $guardResult['invalid_support_types'],
            'capabilities' => $guardResult['capabilities'],
            'allowed_type_keys' => $guardResult['allowed_types'],
            'limited_type_keys' => $guardResult['limited_types'],
            'unsupported_type_keys' => $guardResult['unsupported_types'],
            'capabilities_by_type' => $guardResult['capabilities'],
        ]);
    }

    /**
     * @return array{ok: bool, unsupported_types: list<string>, invalid_support_types: list<string>}
     */
    public function operationEligibility(string $operation, ?BaselineSupportCapabilityGuard $guard = null): array
    {
        $guard ??= app(BaselineSupportCapabilityGuard::class);
        $guardResult = $guard->guardTypes($this->allTypes(), $operation);

        return [
            'ok' => $guardResult['unsupported_types'] === [] && $guardResult['invalid_support_types'] === [],
            'unsupported_types' => $guardResult['unsupported_types'],
            'invalid_support_types' => $guardResult['invalid_support_types'],
        ];
    }

    /**
     * @return list<string>
     */
    private static function supportedPolicyTypes(): array
    {
        return app(GovernanceSubjectTaxonomyRegistry::class)->activeLegacyBucketKeys('policy_types');
    }

    /**
     * @return list<string>
     */
    private static function supportedFoundationTypes(): array
    {
        return app(GovernanceSubjectTaxonomyRegistry::class)->activeLegacyBucketKeys('foundation_types');
    }

    /**
     * @param  array<string>  $types
     * @return list<string>
     */
    private static function normalizePolicyTypes(array $types): array
    {
        $supported = self::supportedPolicyTypes();

        return self::uniqueSorted(array_values(array_intersect($types, $supported)));
    }

    /**
     * @param  array<string>  $types
     * @return list<string>
     */
    private static function normalizeFoundationTypes(array $types): array
    {
        $supported = self::supportedFoundationTypes();

        return self::uniqueSorted(array_values(array_intersect($types, $supported)));
    }

    /**
     * @param  array<int, string>  $types
     * @return list<string>
     */
    private static function uniqueSorted(array $types): array
    {
        $types = array_values(array_unique(array_filter($types, fn (mixed $type): bool => is_string($type) && $type !== '')));

        sort($types, SORT_STRING);

        return $types;
    }

    /**
     * @param  list<string>  $policyTypes
     * @param  list<string>  $foundationTypes
     * @param  list<string>  $legacyKeysPresent
     */
    private static function fromLegacyPayload(
        array $policyTypes,
        array $foundationTypes,
        array $legacyKeysPresent,
        bool $normalizedOnRead,
        bool $saveForwardRequired,
    ): self {
        $policyTypes = $policyTypes === []
            ? self::supportedPolicyTypes()
            : self::normalizePolicyTypes($policyTypes);

        $foundationTypes = self::normalizeFoundationTypes($foundationTypes);
        $entries = [];

        if ($policyTypes !== []) {
            $entries[] = [
                'domain_key' => GovernanceDomainKey::Intune->value,
                'subject_class' => GovernanceSubjectClass::Policy->value,
                'subject_type_keys' => $policyTypes,
                'filters' => [],
            ];
        }

        if ($foundationTypes !== []) {
            $entries[] = [
                'domain_key' => GovernanceDomainKey::PlatformFoundation->value,
                'subject_class' => GovernanceSubjectClass::ConfigurationResource->value,
                'subject_type_keys' => $foundationTypes,
                'filters' => [],
            ];
        }

        return new self(
            policyTypes: $policyTypes,
            foundationTypes: $foundationTypes,
            entries: $entries,
            sourceShape: 'legacy',
            normalizedOnRead: $normalizedOnRead,
            legacyKeysPresent: $legacyKeysPresent,
            saveForwardRequired: $saveForwardRequired,
        );
    }

    /**
     * @param  array<string, mixed>  $scopeJsonb
     */
    private static function fromCanonicalPayload(array $scopeJsonb): self
    {
        if (($scopeJsonb['version'] ?? null) !== 2) {
            throw new InvalidArgumentException('Baseline scope version must equal 2.');
        }

        $entries = $scopeJsonb['entries'] ?? null;

        if (! is_array($entries) || $entries === []) {
            throw new InvalidArgumentException('Baseline scope V2 entries must be a non-empty array.');
        }

        $normalizedEntries = self::normalizeEntries($entries);
        [$policyTypes, $foundationTypes] = self::legacyProjectionFromEntries($normalizedEntries);

        return new self(
            policyTypes: $policyTypes,
            foundationTypes: $foundationTypes,
            entries: $normalizedEntries,
            sourceShape: 'canonical_v2',
            normalizedOnRead: false,
            legacyKeysPresent: [],
            saveForwardRequired: false,
        );
    }

    /**
     * @param  list<mixed>  $entries
     * @return list<array{domain_key: string, subject_class: string, subject_type_keys: list<string>, filters: array<string, mixed>}>
     */
    private static function normalizeEntries(array $entries): array
    {
        $registry = app(GovernanceSubjectTaxonomyRegistry::class);
        $normalizedEntries = [];
        $subjectFilters = [];

        foreach ($entries as $entry) {
            if (! is_array($entry)) {
                throw new InvalidArgumentException('Each canonical baseline scope entry must be an array.');
            }

            $domainKey = trim((string) ($entry['domain_key'] ?? ''));
            $subjectClass = trim((string) ($entry['subject_class'] ?? ''));

            if (! $registry->isKnownDomain($domainKey)) {
                throw new InvalidArgumentException('Unknown governance domain ['.$domainKey.'].');
            }

            if (! $registry->allowsSubjectClass($domainKey, $subjectClass)) {
                throw new InvalidArgumentException('Subject class ['.$subjectClass.'] is not valid for domain ['.$domainKey.'].');
            }

            $subjectTypeKeys = self::stringList($entry['subject_type_keys'] ?? null);

            if ($subjectTypeKeys === []) {
                throw new InvalidArgumentException('Canonical baseline scope entries must include at least one subject type key.');
            }

            $filters = $entry['filters'] ?? [];

            if (! is_array($filters)) {
                throw new InvalidArgumentException('Baseline scope entry filters must be an object-shaped array.');
            }

            $filters = self::normalizeFilters($filters);

            if ($filters !== [] && ! $registry->supportsFilters($domainKey, $subjectClass)) {
                throw new InvalidArgumentException('Filters are not supported for the current governance domain and subject class.');
            }

            $subjectTypeKeys = array_map(
                static fn (string $subjectTypeKey): string => trim($subjectTypeKey),
                self::uniqueSorted($subjectTypeKeys),
            );

            foreach ($subjectTypeKeys as $subjectTypeKey) {
                $subjectType = $registry->find($domainKey, $subjectTypeKey);

                if ($subjectType === null) {
                    throw new InvalidArgumentException('Unknown subject type ['.$subjectTypeKey.'] for domain ['.$domainKey.'].');
                }

                if ($subjectType->subjectClass->value !== $subjectClass) {
                    throw new InvalidArgumentException('Subject type ['.$subjectTypeKey.'] does not belong to subject class ['.$subjectClass.'].');
                }

                if (! $subjectType->active) {
                    throw new InvalidArgumentException('Inactive subject type ['.$subjectTypeKey.'] cannot be selected.');
                }
            }

            $filtersHash = self::filtersHash($filters);

            foreach ($subjectTypeKeys as $subjectTypeKey) {
                $subjectKey = implode('|', [$domainKey, $subjectClass, $subjectTypeKey]);
                $existingFiltersHash = $subjectFilters[$subjectKey] ?? null;

                if ($existingFiltersHash !== null && $existingFiltersHash !== $filtersHash) {
                    throw new InvalidArgumentException('Ambiguous baseline scope filters were provided for ['.$subjectTypeKey.'].');
                }

                $subjectFilters[$subjectKey] = $filtersHash;
            }

            $entryKey = implode('|', [$domainKey, $subjectClass, $filtersHash]);

            if (! array_key_exists($entryKey, $normalizedEntries)) {
                $normalizedEntries[$entryKey] = [
                    'domain_key' => $domainKey,
                    'subject_class' => $subjectClass,
                    'subject_type_keys' => [],
                    'filters' => $filters,
                ];
            }

            $normalizedEntries[$entryKey]['subject_type_keys'] = self::uniqueSorted(array_merge(
                $normalizedEntries[$entryKey]['subject_type_keys'],
                $subjectTypeKeys,
            ));
        }

        $normalizedEntries = array_values($normalizedEntries);

        usort($normalizedEntries, static function (array $left, array $right): int {
            $leftKey = implode('|', [
                $left['domain_key'],
                $left['subject_class'],
                self::filtersHash($left['filters']),
                implode(',', $left['subject_type_keys']),
            ]);
            $rightKey = implode('|', [
                $right['domain_key'],
                $right['subject_class'],
                self::filtersHash($right['filters']),
                implode(',', $right['subject_type_keys']),
            ]);

            return $leftKey <=> $rightKey;
        });

        return $normalizedEntries;
    }

    /**
     * @param  list<array{domain_key: string, subject_class: string, subject_type_keys: list<string>, filters: array<string, mixed>}>  $entries
     * @return array{0: list<string>, 1: list<string>}
     */
    private static function legacyProjectionFromEntries(array $entries): array
    {
        $policyTypes = [];
        $foundationTypes = [];

        foreach ($entries as $entry) {
            if ($entry['domain_key'] === GovernanceDomainKey::Intune->value
                && $entry['subject_class'] === GovernanceSubjectClass::Policy->value) {
                $policyTypes = array_merge($policyTypes, $entry['subject_type_keys']);
            }

            if ($entry['domain_key'] === GovernanceDomainKey::PlatformFoundation->value
                && $entry['subject_class'] === GovernanceSubjectClass::ConfigurationResource->value) {
                $foundationTypes = array_merge($foundationTypes, $entry['subject_type_keys']);
            }
        }

        return [
            self::uniqueSorted($policyTypes),
            self::uniqueSorted($foundationTypes),
        ];
    }

    /**
     * @param  mixed  $values
     * @return list<string>
     */
    private static function stringList(mixed $values): array
    {
        if (! is_array($values)) {
            return [];
        }

        return array_values(array_filter($values, 'is_string'));
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return array<string, mixed>
     */
    private static function normalizeFilters(array $filters): array
    {
        ksort($filters);

        foreach ($filters as $key => $value) {
            if (is_array($value)) {
                $filters[$key] = self::normalizeFilterArray($value);
            }
        }

        return $filters;
    }

    /**
     * @param  array<int|string, mixed>  $values
     * @return array<int|string, mixed>
     */
    private static function normalizeFilterArray(array $values): array
    {
        foreach ($values as $key => $value) {
            if (is_array($value)) {
                $values[$key] = self::normalizeFilterArray($value);
            }
        }

        if (array_is_list($values)) {
            sort($values);

            return array_values($values);
        }

        ksort($values);

        return $values;
    }

    /**
     * @param  array<string, mixed>  $filters
     */
    private static function filtersHash(array $filters): string
    {
        return json_encode($filters, JSON_THROW_ON_ERROR);
    }
}
