<?php

declare(strict_types=1);

namespace App\Support\PortfolioCompare;

use DomainException;
use InvalidArgumentException;

final class CrossEnvironmentPromotionExecutionPlanner
{
    /**
     * @param  array<string, mixed>  $preview
     * @param  array<string, mixed>  $preflight
     * @return array{
     *     selection: array<string, mixed>,
     *     summary: array{total: int, ready: int, excluded: int, skipped: int, created: int, updated: int},
     *     items: list<array<string, mixed>>,
     *     excluded: list<array<string, mixed>>,
     *     identity: array<string, mixed>
     * }
     */
    public function build(array $preview, array $preflight): array
    {
        $previewSelection = $this->selection($preview);
        $preflightSelection = $this->selection($preflight);

        if ($previewSelection !== $preflightSelection) {
            throw new InvalidArgumentException('Promotion preflight is stale. Regenerate the preflight before execution.');
        }

        $items = [];
        $excluded = $this->excludedSubjects($preflight);

        foreach ($this->readySubjects($preflight) as $subject) {
            $item = $this->executionItem($subject);

            if ($item === null) {
                $excluded[] = $this->excludedSubject($subject, 'source_policy_version_missing');

                continue;
            }

            $items[] = $item;
        }

        $items = $this->sortItems($items);
        $excluded = $this->sortItems($excluded);

        if ($items === []) {
            throw new DomainException('Promotion preflight has no executable ready subjects.');
        }

        $summary = [
            'total' => count($items) + count($excluded),
            'ready' => count($items),
            'excluded' => count($excluded),
            'skipped' => count(array_filter($items, static fn (array $item): bool => ($item['execution_action'] ?? null) === 'skip_aligned')),
            'created' => count(array_filter($items, static fn (array $item): bool => ($item['execution_action'] ?? null) === 'create_missing')),
            'updated' => count(array_filter($items, static fn (array $item): bool => ($item['execution_action'] ?? null) === 'update_existing')),
        ];

        return [
            'selection' => $previewSelection,
            'summary' => $summary,
            'items' => $items,
            'excluded' => $excluded,
            'identity' => $this->identity($previewSelection, $items),
        ];
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array{sourceEnvironmentId: ?int, targetEnvironmentId: ?int, policyTypes: list<string>}
     */
    private function selection(array $payload): array
    {
        $selection = is_array($payload['selection'] ?? null) ? $payload['selection'] : [];
        $policyTypes = is_array($selection['policyTypes'] ?? null) ? $selection['policyTypes'] : [];

        $policyTypes = array_values(array_unique(array_filter(array_map(
            static fn (mixed $value): string => is_string($value) ? trim($value) : '',
            $policyTypes,
        ), static fn (string $value): bool => $value !== '')));

        sort($policyTypes);

        return [
            'sourceEnvironmentId' => is_numeric($selection['sourceEnvironmentId'] ?? null) ? (int) $selection['sourceEnvironmentId'] : null,
            'targetEnvironmentId' => is_numeric($selection['targetEnvironmentId'] ?? null) ? (int) $selection['targetEnvironmentId'] : null,
            'policyTypes' => $policyTypes,
        ];
    }

    /**
     * @param  array<string, mixed>  $preflight
     * @return list<array<string, mixed>>
     */
    private function readySubjects(array $preflight): array
    {
        $subjects = data_get($preflight, 'buckets.ready', []);

        if (! is_array($subjects)) {
            return [];
        }

        return array_values(array_filter($subjects, 'is_array'));
    }

    /**
     * @param  array<string, mixed>  $preflight
     * @return list<array<string, mixed>>
     */
    private function excludedSubjects(array $preflight): array
    {
        $excluded = [];

        foreach (['blocked', 'manual_mapping_required'] as $bucket) {
            $subjects = data_get($preflight, 'buckets.'.$bucket, []);

            if (! is_array($subjects)) {
                continue;
            }

            foreach ($subjects as $subject) {
                if (! is_array($subject)) {
                    continue;
                }

                $excluded[] = $this->excludedSubject($subject, $bucket);
            }
        }

        return $excluded;
    }

    /**
     * @param  array<string, mixed>  $subject
     * @return array<string, mixed>|null
     */
    private function executionItem(array $subject): ?array
    {
        $policyVersionId = data_get($subject, 'source.evidence.policyVersionId');

        if (! is_numeric($policyVersionId)) {
            return null;
        }

        $state = is_string($subject['state'] ?? null) ? (string) $subject['state'] : 'blocked';
        $action = match ($state) {
            'match' => 'skip_aligned',
            'missing' => 'create_missing',
            default => 'update_existing',
        };

        return [
            'policy_type' => $this->stringValue($subject, 'policyType'),
            'display_name' => $this->stringValue($subject, 'displayName'),
            'subject_key' => $this->stringValue($subject, 'subjectKey'),
            'compare_state' => $state,
            'execution_action' => $action,
            'readiness_reason_codes' => $this->stringList(data_get($subject, 'preflight.reasonCodes', [])),
            'source' => [
                'managed_environment_id' => $this->intValue(data_get($subject, 'source.environmentId')),
                'inventory_item_id' => $this->intValue(data_get($subject, 'source.inventoryItemId')),
                'subject_external_id' => $this->nullableString(data_get($subject, 'source.subjectExternalId')),
                'policy_version_id' => (int) $policyVersionId,
                'evidence_hash' => $this->nullableString(data_get($subject, 'source.evidence.hash')),
            ],
            'target' => [
                'managed_environment_id' => $this->intValue(data_get($subject, 'target.environmentId')),
                'inventory_item_id' => $this->intValue(data_get($subject, 'target.inventoryItemId')),
                'subject_external_id' => $this->nullableString(data_get($subject, 'target.subjectExternalId')),
            ],
        ];
    }

    /**
     * @param  array<string, mixed>  $subject
     * @return array<string, mixed>
     */
    private function excludedSubject(array $subject, string $reason): array
    {
        return [
            'policy_type' => $this->stringValue($subject, 'policyType'),
            'display_name' => $this->stringValue($subject, 'displayName'),
            'subject_key' => $this->stringValue($subject, 'subjectKey'),
            'compare_state' => $this->stringValue($subject, 'state'),
            'excluded_reason' => $reason,
            'reason_codes' => $this->stringList(data_get($subject, 'preflight.reasonCodes', [])),
        ];
    }

    /**
     * @param  list<array<string, mixed>>  $items
     * @return list<array<string, mixed>>
     */
    private function sortItems(array $items): array
    {
        usort($items, static function (array $left, array $right): int {
            return [
                (string) ($left['policy_type'] ?? ''),
                (string) ($left['subject_key'] ?? ''),
                (string) ($left['display_name'] ?? ''),
            ] <=> [
                (string) ($right['policy_type'] ?? ''),
                (string) ($right['subject_key'] ?? ''),
                (string) ($right['display_name'] ?? ''),
            ];
        });

        return $items;
    }

    /**
     * @param  array<string, mixed>  $selection
     * @param  list<array<string, mixed>>  $items
     * @return array<string, mixed>
     */
    private function identity(array $selection, array $items): array
    {
        return [
            'source_environment_id' => $selection['sourceEnvironmentId'] ?? null,
            'target_environment_id' => $selection['targetEnvironmentId'] ?? null,
            'policy_types' => $selection['policyTypes'] ?? [],
            'subjects' => array_map(static fn (array $item): array => [
                'policy_type' => $item['policy_type'] ?? '',
                'subject_key' => $item['subject_key'] ?? '',
                'source_policy_version_id' => data_get($item, 'source.policy_version_id'),
                'source_evidence_hash' => data_get($item, 'source.evidence_hash'),
                'target_subject_external_id' => data_get($item, 'target.subject_external_id'),
                'execution_action' => $item['execution_action'] ?? '',
            ], $items),
        ];
    }

    /**
     * @param  array<string, mixed>  $subject
     */
    private function stringValue(array $subject, string $key): string
    {
        $value = $subject[$key] ?? null;

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

    private function nullableString(mixed $value): ?string
    {
        return is_string($value) && trim($value) !== '' ? trim($value) : null;
    }

    private function intValue(mixed $value): ?int
    {
        return is_numeric($value) ? (int) $value : null;
    }

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

        return array_values(array_filter(array_map(
            static fn (mixed $value): string => is_string($value) ? trim($value) : '',
            $values,
        ), static fn (string $value): bool => $value !== ''));
    }
}
