<?php

declare(strict_types=1);

namespace App\Support\Governance\Controls;

use App\Models\EvidenceSnapshot;
use App\Models\EvidenceSnapshotItem;
use App\Models\Finding;
use App\Support\EnvironmentReviewCompletenessState;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

final readonly class ComplianceEvidenceMappingV1
{
    public const string VERSION_KEY = 'compliance_evidence_mapping.v1';

    public const string SECTION_KEY = 'control_interpretation';

    public function __construct(
        private CanonicalControlCatalog $catalog,
    ) {}

    /**
     * @return array{
     *     summary: array<string, mixed>,
     *     section: array<string, mixed>
     * }
     */
    public function interpret(EvidenceSnapshot $snapshot, ?EvidenceSnapshotItem $findingsItem): array
    {
        $findingsSummary = $this->findingsSummary($findingsItem);
        $entries = $this->findingEntries($findingsSummary);
        $unresolvedEntryCount = $entries
            ->filter(static fn (array $entry): bool => (string) data_get($entry, 'canonical_control_resolution.status') !== 'resolved')
            ->count();
        $controls = $this->controlDefinitions($findingsSummary, $entries);
        $snapshotLimitations = $this->snapshotLimitations($snapshot, $findingsItem, $unresolvedEntryCount);

        $controlSummaries = $controls
            ->map(fn (CanonicalControlDefinition $definition): array => $this->controlSummary(
                definition: $definition,
                entries: $this->entriesForControl($entries, $definition->controlKey),
                snapshotLimitations: $snapshotLimitations,
            ))
            ->values()
            ->all();

        $globalLimitations = $this->globalLimitations($controlSummaries, $snapshotLimitations, $controls->isEmpty(), $unresolvedEntryCount);
        $limitationCounts = $this->limitationCounts($controlSummaries, $globalLimitations);

        $summary = [
            'version_key' => self::VERSION_KEY,
            'display_label' => 'Compliance evidence mapping v1',
            'non_certification_disclosure' => 'TenantPilot interprets available evidence for review readiness. This is not a certification, legal attestation, or compliance guarantee.',
            'mapped_control_count' => count($controlSummaries),
            'follow_up_required_count' => collect($controlSummaries)
                ->where('readiness_bucket', 'follow_up_required')
                ->count(),
            'limitation_counts' => $limitationCounts,
            'limitations' => $globalLimitations,
            'controls' => $controlSummaries,
        ];

        return [
            'summary' => $summary,
            'section' => [
                'section_key' => self::SECTION_KEY,
                'title' => 'Control readiness interpretation',
                'sort_order' => 15,
                'required' => true,
                'completeness_state' => $this->sectionCompleteness($findingsItem, $controls->isEmpty(), $snapshotLimitations),
                'source_snapshot_fingerprint' => $this->sourceFingerprint($findingsItem) ?? (string) $snapshot->fingerprint,
                'summary_payload' => Arr::except($summary, ['controls']),
                'render_payload' => [
                    'entries' => array_map(
                        fn (array $control): array => $this->controlExplanation($control, $snapshot),
                        $controlSummaries,
                    ),
                    'disclosure' => $summary['non_certification_disclosure'],
                    'next_actions' => $this->sectionNextActions($controlSummaries, $globalLimitations),
                    'empty_state' => $controlSummaries === []
                        ? 'No canonical controls are mapped in this released review. Treat the control view as partial until evidence references can be mapped.'
                        : null,
                ],
                'measured_at' => $findingsItem?->measured_at ?? $snapshot->generated_at,
            ],
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function findingsSummary(?EvidenceSnapshotItem $findingsItem): array
    {
        return is_array($findingsItem?->summary_payload) ? $findingsItem->summary_payload : [];
    }

    /**
     * @param  array<string, mixed>  $findingsSummary
     * @return Collection<int, array<string, mixed>>
     */
    private function findingEntries(array $findingsSummary): Collection
    {
        return collect(Arr::wrap($findingsSummary['entries'] ?? []))
            ->filter(static fn (mixed $entry): bool => is_array($entry))
            ->values();
    }

    /**
     * @param  array<string, mixed>  $findingsSummary
     * @param  Collection<int, array<string, mixed>>  $entries
     * @return Collection<int, CanonicalControlDefinition>
     */
    private function controlDefinitions(array $findingsSummary, Collection $entries): Collection
    {
        $summaryControls = collect(Arr::wrap($findingsSummary['canonical_controls'] ?? []))
            ->filter(static fn (mixed $control): bool => is_array($control));

        $entryControls = $entries
            ->map(static fn (array $entry): mixed => data_get($entry, 'canonical_control_resolution.control'))
            ->filter(static fn (mixed $control): bool => is_array($control));

        return $summaryControls
            ->merge($entryControls)
            ->map(fn (array $control): ?CanonicalControlDefinition => $this->definitionFor($control))
            ->filter()
            ->unique(static fn (CanonicalControlDefinition $definition): string => $definition->controlKey)
            ->sortBy(static fn (CanonicalControlDefinition $definition): string => $definition->name)
            ->values();
    }

    /**
     * @param  array<string, mixed>  $control
     */
    private function definitionFor(array $control): ?CanonicalControlDefinition
    {
        $controlKey = $control['control_key'] ?? null;

        if (! is_string($controlKey) || trim($controlKey) === '') {
            return null;
        }

        return $this->catalog->find($controlKey);
    }

    /**
     * @param  Collection<int, array<string, mixed>>  $entries
     * @return Collection<int, array<string, mixed>>
     */
    private function entriesForControl(Collection $entries, string $controlKey): Collection
    {
        return $entries
            ->filter(static fn (array $entry): bool => (string) data_get($entry, 'canonical_control_resolution.control.control_key') === $controlKey)
            ->values();
    }

    /**
     * @param  Collection<int, array<string, mixed>>  $entries
     * @param  list<string>  $snapshotLimitations
     * @return array<string, mixed>
     */
    private function controlSummary(CanonicalControlDefinition $definition, Collection $entries, array $snapshotLimitations): array
    {
        $openEntries = $entries->filter(static fn (array $entry): bool => in_array((string) ($entry['status'] ?? ''), Finding::openStatuses(), true));
        $acceptedRiskEntries = $entries->filter(static fn (array $entry): bool => (string) ($entry['status'] ?? '') === Finding::STATUS_RISK_ACCEPTED);
        $governanceWarnings = $entries->filter(static fn (array $entry): bool => self::hasGovernanceWarning($entry));
        $limitationFlags = $this->controlLimitations($acceptedRiskEntries->count(), $snapshotLimitations);
        $readinessBucket = $this->readinessBucket(
            openCount: $openEntries->count(),
            acceptedRiskCount: $acceptedRiskEntries->count(),
            governanceWarningCount: $governanceWarnings->count(),
            limitationFlags: $limitationFlags,
        );

        return [
            'control_key' => $definition->controlKey,
            'control_name' => $definition->name,
            'domain_key' => $definition->domainKey,
            'readiness_bucket' => $readinessBucket,
            'readiness_label' => self::readinessLabel($readinessBucket),
            'limitation_flags' => $limitationFlags,
            'limitation_labels' => array_map(self::limitationLabel(...), $limitationFlags),
            'customer_summary' => $this->customerSummary($definition, $readinessBucket, $openEntries->count(), $acceptedRiskEntries->count()),
            'evidence_basis_summary' => $this->evidenceBasisSummary($entries->count(), $openEntries->count(), $acceptedRiskEntries->count()),
            'accepted_risk_summary' => $acceptedRiskEntries->isEmpty()
                ? null
                : $this->acceptedRiskSummary($acceptedRiskEntries, $governanceWarnings->count()),
            'recommended_next_action' => $this->recommendedNextAction($readinessBucket, $acceptedRiskEntries->count(), $limitationFlags),
            'detail_anchor' => 'control-'.$definition->controlKey,
            'supporting_finding_ids' => $entries
                ->pluck('id')
                ->filter(static fn (mixed $id): bool => is_numeric($id))
                ->map(static fn (mixed $id): int => (int) $id)
                ->values()
                ->all(),
            'finding_count' => $entries->count(),
            'open_finding_count' => $openEntries->count(),
            'accepted_risk_count' => $acceptedRiskEntries->count(),
        ];
    }

    /**
     * @param  Collection<int, array<string, mixed>>  $acceptedRiskEntries
     */
    private function acceptedRiskSummary(Collection $acceptedRiskEntries, int $governanceWarningCount): string
    {
        if ($governanceWarningCount > 0) {
            return sprintf(
                '%d accepted-risk finding(s) need governance follow-up before relying on this interpretation.',
                $acceptedRiskEntries->count(),
            );
        }

        return sprintf(
            '%d accepted-risk finding(s) are part of the evidence basis and qualify the readiness view.',
            $acceptedRiskEntries->count(),
        );
    }

    /**
     * @param  list<string>  $snapshotLimitations
     * @return list<string>
     */
    private function controlLimitations(int $acceptedRiskCount, array $snapshotLimitations): array
    {
        $limitations = $snapshotLimitations;

        if ($acceptedRiskCount > 0) {
            $limitations[] = 'accepted_risk_influenced';
        }

        return array_values(array_unique($limitations));
    }

    /**
     * @param  list<string>  $limitationFlags
     */
    private function readinessBucket(int $openCount, int $acceptedRiskCount, int $governanceWarningCount, array $limitationFlags): string
    {
        if ($openCount > 0 || $governanceWarningCount > 0) {
            return 'follow_up_required';
        }

        if ($acceptedRiskCount > 0 || $limitationFlags !== []) {
            return 'review_recommended';
        }

        return 'evidence_on_record';
    }

    private function customerSummary(CanonicalControlDefinition $definition, string $readinessBucket, int $openCount, int $acceptedRiskCount): string
    {
        return match ($readinessBucket) {
            'follow_up_required' => sprintf(
                '%s needs follow-up because %d open finding(s) remain in the released evidence basis.',
                $definition->name,
                $openCount,
            ),
            'review_recommended' => $acceptedRiskCount > 0
                ? sprintf('%s has evidence on record with accepted-risk context that should be reviewed before relying on the interpretation.', $definition->name)
                : sprintf('%s has evidence on record, with limitations that should be reviewed before relying on the interpretation.', $definition->name),
            default => sprintf('%s has evidence on record in this released review.', $definition->name),
        };
    }

    private function evidenceBasisSummary(int $signalCount, int $openCount, int $acceptedRiskCount): string
    {
        $parts = [
            sprintf('%d evidence signal(s) reference this control.', $signalCount),
        ];

        if ($openCount > 0) {
            $parts[] = sprintf('%d open finding(s) still need follow-up.', $openCount);
        }

        if ($acceptedRiskCount > 0) {
            $parts[] = sprintf('%d accepted-risk finding(s) qualify this view.', $acceptedRiskCount);
        }

        return implode(' ', $parts);
    }

    /**
     * @param  list<string>  $limitationFlags
     */
    private function recommendedNextAction(string $readinessBucket, int $acceptedRiskCount, array $limitationFlags): string
    {
        if ($readinessBucket === 'follow_up_required') {
            return 'Review the surfaced findings with the tenant and agree ownership plus follow-up timing.';
        }

        if ($acceptedRiskCount > 0) {
            return 'Review the accepted-risk owner and next review date before customer delivery.';
        }

        if ($limitationFlags !== []) {
            return 'Confirm the evidence basis and limitations before using this control as customer-facing readiness support.';
        }

        return 'Keep this evidence on record and revisit it during the normal review cadence.';
    }

    /**
     * @param  array<string, mixed>  $control
     * @return array<string, mixed>
     */
    private function controlExplanation(array $control, EvidenceSnapshot $snapshot): array
    {
        return [
            'title' => $control['control_name'],
            'control_key' => $control['control_key'],
            'control_name' => $control['control_name'],
            'readiness_bucket' => $control['readiness_bucket'],
            'readiness_label' => $control['readiness_label'],
            'limitation_flags' => $control['limitation_flags'],
            'limitation_labels' => $control['limitation_labels'],
            'customer_summary' => $control['customer_summary'],
            'evidence_basis_summary' => $control['evidence_basis_summary'],
            'accepted_risk_summary' => $control['accepted_risk_summary'],
            'explanation_text' => $control['customer_summary'],
            'evidence_basis_items' => array_values(array_filter([
                $control['evidence_basis_summary'],
                $control['accepted_risk_summary'],
            ])),
            'accepted_risk_context' => $control['accepted_risk_summary'],
            'recommended_next_action' => $control['recommended_next_action'],
            'proof_access_state' => $this->proofAccessState($snapshot),
            'supporting_finding_ids' => $control['supporting_finding_ids'],
        ];
    }

    /**
     * @param  list<array<string, mixed>>  $controlSummaries
     * @param  list<string>  $globalLimitations
     * @return list<string>
     */
    private function sectionNextActions(array $controlSummaries, array $globalLimitations): array
    {
        if ($controlSummaries === []) {
            return ['Review unmapped evidence before using this review for customer-facing readiness discussions.'];
        }

        $actions = collect($controlSummaries)
            ->pluck('recommended_next_action')
            ->filter(static fn (mixed $action): bool => is_string($action) && trim($action) !== '')
            ->unique()
            ->values()
            ->all();

        if (in_array('unmapped', $globalLimitations, true)) {
            $actions[] = 'Treat this review as partial until unmapped evidence can be interpreted.';
        }

        return array_values(array_unique($actions));
    }

    /**
     * @param  list<array<string, mixed>>  $controlSummaries
     * @param  list<string>  $snapshotLimitations
     * @return list<string>
     */
    private function globalLimitations(array $controlSummaries, array $snapshotLimitations, bool $noMappedControls, int $unresolvedEntryCount): array
    {
        $limitations = $snapshotLimitations;

        if ($noMappedControls) {
            $limitations[] = 'unmapped';
        }

        if ($unresolvedEntryCount > 0) {
            $limitations[] = 'partial_mapping';
        }

        foreach ($controlSummaries as $control) {
            foreach (Arr::wrap($control['limitation_flags'] ?? []) as $limitation) {
                if (is_string($limitation) && trim($limitation) !== '') {
                    $limitations[] = $limitation;
                }
            }
        }

        return array_values(array_unique($limitations));
    }

    /**
     * @param  list<array<string, mixed>>  $controlSummaries
     * @param  list<string>  $globalLimitations
     * @return array<string, int>
     */
    private function limitationCounts(array $controlSummaries, array $globalLimitations): array
    {
        $counts = collect($controlSummaries)
            ->flatMap(static fn (array $control): array => Arr::wrap($control['limitation_flags'] ?? []))
            ->filter(static fn (mixed $limitation): bool => is_string($limitation) && trim($limitation) !== '')
            ->countBy()
            ->all();

        foreach ($globalLimitations as $limitation) {
            $counts[$limitation] = max((int) ($counts[$limitation] ?? 0), 1);
        }

        ksort($counts);

        return array_map('intval', $counts);
    }

    /**
     * @return list<string>
     */
    private function snapshotLimitations(EvidenceSnapshot $snapshot, ?EvidenceSnapshotItem $findingsItem, int $unresolvedEntryCount): array
    {
        $limitations = [];
        $state = (string) ($findingsItem?->state ?? $snapshot->completeness_state);

        if ($state === EnvironmentReviewCompletenessState::Stale->value || (string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) {
            $limitations[] = 'stale_evidence';
        }

        if (in_array($state, [EnvironmentReviewCompletenessState::Partial->value, EnvironmentReviewCompletenessState::Missing->value], true)) {
            $limitations[] = 'partial_mapping';
        }

        if ($unresolvedEntryCount > 0) {
            $limitations[] = 'partial_mapping';
        }

        if (! $snapshot->exists || $snapshot->generated_at === null) {
            $limitations[] = 'supporting_evidence_unavailable';
        }

        return array_values(array_unique($limitations));
    }

    /**
     * @param  list<string>  $snapshotLimitations
     */
    private function sectionCompleteness(?EvidenceSnapshotItem $findingsItem, bool $noMappedControls, array $snapshotLimitations): string
    {
        if (! $findingsItem instanceof EvidenceSnapshotItem) {
            return EnvironmentReviewCompletenessState::Missing->value;
        }

        if (in_array('stale_evidence', $snapshotLimitations, true)) {
            return EnvironmentReviewCompletenessState::Stale->value;
        }

        if ($noMappedControls || in_array('partial_mapping', $snapshotLimitations, true)) {
            return EnvironmentReviewCompletenessState::Partial->value;
        }

        return EnvironmentReviewCompletenessState::tryFrom((string) $findingsItem->state)?->value
            ?? EnvironmentReviewCompletenessState::Missing->value;
    }

    private function proofAccessState(EvidenceSnapshot $snapshot): string
    {
        if ((string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) {
            return 'expired';
        }

        if (! $snapshot->exists || $snapshot->generated_at === null) {
            return 'unavailable';
        }

        return 'available';
    }

    private function sourceFingerprint(?EvidenceSnapshotItem $item): ?string
    {
        $fingerprint = $item?->source_fingerprint;

        return is_string($fingerprint) && $fingerprint !== '' ? $fingerprint : null;
    }

    /**
     * @param  array<string, mixed>  $entry
     */
    private static function hasGovernanceWarning(array $entry): bool
    {
        if (is_string($entry['governance_warning'] ?? null) && trim((string) $entry['governance_warning']) !== '') {
            return true;
        }

        return in_array((string) ($entry['governance_state'] ?? ''), [
            'expired_exception',
            'revoked_exception',
            'rejected_exception',
            'risk_accepted_without_valid_exception',
        ], true);
    }

    public static function readinessLabel(string $bucket): string
    {
        return match ($bucket) {
            'follow_up_required' => 'Follow-up required',
            'review_recommended' => 'Review recommended',
            'evidence_on_record' => 'Evidence on record',
            default => Str::headline($bucket),
        };
    }

    public static function limitationLabel(string $flag): string
    {
        return match ($flag) {
            'accepted_risk_influenced' => 'Accepted risk influences this view',
            'partial_mapping' => 'Partial evidence mapping',
            'stale_evidence' => 'Evidence freshness needs review',
            'supporting_evidence_unavailable' => 'Supporting evidence unavailable',
            'unmapped' => 'No mapped control coverage',
            default => Str::headline($flag),
        };
    }
}
