<?php

declare(strict_types=1);

namespace App\Support\GovernanceDecisions;

use App\Filament\Resources\EvidenceSnapshotResource;
use App\Filament\Resources\FindingExceptionResource;
use App\Filament\Resources\StoredReportResource;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Models\FindingExceptionEvidenceReference;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\StoredReport;
use App\Models\User;
use App\Models\Workspace;
use App\Support\Auth\Capabilities;
use App\Support\OperationRunLinks;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;

final readonly class GovernanceDecisionRegisterBuilder
{
    private const int RECENTLY_CLOSED_DAYS = 30;

    /**
     * @var list<string>
     */
    private const array TERMINAL_STATUSES = [
        FindingException::STATUS_REJECTED,
        FindingException::STATUS_REVOKED,
        FindingException::STATUS_SUPERSEDED,
    ];

    /**
     * @param  array<int, ManagedEnvironment>  $visibleTenants
     * @return array{
     *     rows: list<array<string, mixed>>,
     *     counts: array{open: int, recently_closed: int},
     * }
     */
    public function build(Workspace $workspace, array $visibleTenants, string $registerState = 'open'): array
    {
        $visibleTenantIds = array_values(array_map(
            static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey(),
            $visibleTenants,
        ));

        if ($visibleTenantIds === []) {
            return [
                'rows' => [],
                'counts' => [
                    'open' => 0,
                    'recently_closed' => 0,
                ],
            ];
        }

        $rows = FindingException::query()
            ->where('workspace_id', (int) $workspace->getKey())
            ->whereIn('managed_environment_id', $visibleTenantIds)
            ->with(['tenant', 'owner:id,name', 'currentDecision', 'evidenceReferences', 'finding.currentRun', 'finding.baselineRun'])
            ->get()
            ->map(fn (FindingException $exception): ?array => $this->buildRow($exception))
            ->filter()
            ->values();

        /** @var Collection<int, array<string, mixed>> $openRows */
        $openRows = $rows
            ->where('register_state', 'open')
            ->sortBy([
                ['due_at', 'asc'],
                ['exception_id', 'asc'],
            ])
            ->values();

        /** @var Collection<int, array<string, mixed>> $recentlyClosedRows */
        $recentlyClosedRows = $rows
            ->where('register_state', 'recently_closed')
            ->sortByDesc('decision_at')
            ->values();

        return [
            'rows' => match ($registerState) {
                'recently_closed' => $recentlyClosedRows->all(),
                default => $openRows->all(),
            },
            'counts' => [
                'open' => $openRows->count(),
                'recently_closed' => $recentlyClosedRows->count(),
            ],
        ];
    }

    /**
     * @return array<string, mixed>|null
     */
    private function buildRow(FindingException $exception): ?array
    {
        $currentDecision = $exception->currentDecision;

        if (! $currentDecision instanceof FindingExceptionDecision) {
            return null;
        }

        $registerState = $this->resolveRegisterState($exception, $currentDecision);

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

        $proofMetadata = $this->buildProofMetadata($exception);
        $operationRunMetadata = $this->buildOperationRunMetadata($exception);

        return [
            'exception_id' => (int) $exception->getKey(),
            'register_state' => $registerState,
            'tenant_name' => $exception->tenant?->name,
            'owner_name' => $exception->owner?->name,
            'status' => (string) $exception->status,
            'current_validity_state' => (string) $exception->current_validity_state,
            'next_action_label' => $registerState === 'open'
                ? $this->resolveNextActionLabel($exception, $currentDecision)
                : 'Decision closed',
            'closure_reason' => $registerState === 'recently_closed'
                ? (string) $currentDecision->reason
                : null,
            'due_at' => $exception->review_due_at ?? $exception->expires_at,
            'decision_at' => $currentDecision->decided_at,
            ...$proofMetadata,
            ...$operationRunMetadata,
        ];
    }

    /**
     * @return array{
     *     proof_count: int,
     *     proof_state: string,
     *     proof_label: string,
     *     proof_url: string|null,
     *     proof_url_label: string|null,
     *     proof_unavailable_reason: string|null,
     * }
     */
    private function buildProofMetadata(FindingException $exception): array
    {
        $references = $this->evidenceReferences($exception);
        $proofCount = $references->count();

        if ($proofCount === 0) {
            return [
                'proof_count' => 0,
                'proof_state' => 'not_linked',
                'proof_label' => 'No linked proof',
                'proof_url' => null,
                'proof_url_label' => null,
                'proof_unavailable_reason' => null,
            ];
        }

        if ($proofCount === 1) {
            $directLink = $this->resolveDirectProofLink($exception, $references->first());

            if (is_array($directLink)) {
                return [
                    'proof_count' => $proofCount,
                    'proof_state' => $directLink['state'],
                    'proof_label' => $this->proofLabel($proofCount),
                    'proof_url' => $directLink['url'],
                    'proof_url_label' => $directLink['label'],
                    'proof_unavailable_reason' => null,
                ];
            }
        }

        return [
            'proof_count' => $proofCount,
            'proof_state' => 'linked_detail_section',
            'proof_label' => $this->proofLabel($proofCount),
            'proof_url' => $this->findingExceptionDetailUrl($exception),
            'proof_url_label' => 'View proof',
            'proof_unavailable_reason' => null,
        ];
    }

    /**
     * @return array{
     *     operation_run_state: string,
     *     operation_run_url: string|null,
     *     operation_run_label: string,
     * }
     */
    private function buildOperationRunMetadata(FindingException $exception): array
    {
        $run = $this->sourceFindingOperationRun($exception);

        if (! $run instanceof OperationRun) {
            $runs = $this->evidenceOperationRunsFor($exception)
                ->unique(fn (OperationRun $run): int => (int) $run->getKey())
                ->values();

            if ($runs->count() !== 1) {
                return [
                    'operation_run_state' => $runs->isEmpty() ? 'not_linked' : 'run_not_available',
                    'operation_run_url' => null,
                    'operation_run_label' => 'No operation linked',
                ];
            }

            /** @var OperationRun $run */
            $run = $runs->first();
        }

        if (! $this->canViewOperationRun($run, $exception)) {
            return [
                'operation_run_state' => 'run_not_available',
                'operation_run_url' => null,
                'operation_run_label' => 'No operation linked',
            ];
        }

        return [
            'operation_run_state' => 'linked_run',
            'operation_run_url' => OperationRunLinks::tenantlessView($run),
            'operation_run_label' => 'View operation',
        ];
    }

    private function sourceFindingOperationRun(FindingException $exception): ?OperationRun
    {
        $finding = $exception->relationLoaded('finding')
            ? $exception->finding
            : $exception->finding()->with(['currentRun', 'baselineRun'])->first();

        if (! $finding instanceof Finding) {
            return null;
        }

        if ($finding->currentRun instanceof OperationRun) {
            return $finding->currentRun;
        }

        return $finding->baselineRun instanceof OperationRun
            ? $finding->baselineRun
            : null;
    }

    /**
     * @return Collection<int, FindingExceptionEvidenceReference>
     */
    private function evidenceReferences(FindingException $exception): Collection
    {
        if ($exception->relationLoaded('evidenceReferences')) {
            return $exception->evidenceReferences
                ->filter(fn (mixed $reference): bool => $reference instanceof FindingExceptionEvidenceReference)
                ->values();
        }

        return $exception->evidenceReferences()->get();
    }

    /**
     * @return array{state: string, url: string, label: string}|null
     */
    private function resolveDirectProofLink(FindingException $exception, ?FindingExceptionEvidenceReference $reference): ?array
    {
        if (! $reference instanceof FindingExceptionEvidenceReference) {
            return null;
        }

        if ($this->isEvidenceSnapshotReference($reference)) {
            $snapshot = $this->resolveEvidenceSnapshot($exception, $reference);

            if ($snapshot instanceof EvidenceSnapshot && $this->canViewEvidenceSnapshot($snapshot)) {
                return [
                    'state' => 'linked_evidence',
                    'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin', tenant: $snapshot->tenant),
                    'label' => 'View evidence',
                ];
            }
        }

        if ($this->isStoredReportReference($reference)) {
            $report = $this->resolveStoredReport($exception, $reference);

            if ($report instanceof StoredReport && $this->canViewStoredReport($report)) {
                return [
                    'state' => 'linked_report',
                    'url' => StoredReportResource::getUrl('view', ['record' => $report], panel: 'admin', tenant: $report->tenant),
                    'label' => 'View report',
                ];
            }
        }

        return null;
    }

    /**
     * @return Collection<int, OperationRun>
     */
    private function evidenceOperationRunsFor(FindingException $exception): Collection
    {
        return $this->evidenceReferences($exception)
            ->filter(fn (FindingExceptionEvidenceReference $reference): bool => $this->isEvidenceSnapshotReference($reference))
            ->map(fn (FindingExceptionEvidenceReference $reference): ?EvidenceSnapshot => $this->resolveEvidenceSnapshot($exception, $reference))
            ->filter(fn (?EvidenceSnapshot $snapshot): bool => $snapshot instanceof EvidenceSnapshot)
            ->map(fn (EvidenceSnapshot $snapshot): ?OperationRun => $snapshot->operationRun)
            ->filter(fn (?OperationRun $run): bool => $run instanceof OperationRun)
            ->values();
    }

    private function resolveEvidenceSnapshot(FindingException $exception, FindingExceptionEvidenceReference $reference): ?EvidenceSnapshot
    {
        $sourceId = $this->numericSourceId($reference);

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

        return EvidenceSnapshot::query()
            ->with(['tenant', 'operationRun'])
            ->whereKey($sourceId)
            ->where('workspace_id', (int) $exception->workspace_id)
            ->where('managed_environment_id', (int) $exception->managed_environment_id)
            ->first();
    }

    private function resolveStoredReport(FindingException $exception, FindingExceptionEvidenceReference $reference): ?StoredReport
    {
        $sourceId = $this->numericSourceId($reference);

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

        return StoredReport::query()
            ->with('tenant')
            ->whereKey($sourceId)
            ->where('workspace_id', (int) $exception->workspace_id)
            ->where('managed_environment_id', (int) $exception->managed_environment_id)
            ->whereIn('report_type', StoredReportResource::supportedReportTypes())
            ->first();
    }

    private function canViewEvidenceSnapshot(EvidenceSnapshot $snapshot): bool
    {
        $user = auth()->user();
        $tenant = $snapshot->tenant;

        return $user instanceof User
            && $tenant instanceof ManagedEnvironment
            && $user->canAccessTenant($tenant)
            && $user->can(Capabilities::EVIDENCE_VIEW, $tenant);
    }

    private function canViewStoredReport(StoredReport $report): bool
    {
        $user = auth()->user();
        $tenant = $report->tenant;
        $capability = StoredReportResource::capabilityForReportType((string) $report->report_type);

        return $user instanceof User
            && $tenant instanceof ManagedEnvironment
            && is_string($capability)
            && $user->canAccessTenant($tenant)
            && $user->can($capability, $tenant);
    }

    private function canViewOperationRun(OperationRun $run, FindingException $exception): bool
    {
        $user = auth()->user();

        return $user instanceof User
            && $this->runMatchesExceptionScope($run, $exception)
            && Gate::forUser($user)->allows('view', $run);
    }

    private function runMatchesExceptionScope(OperationRun $run, FindingException $exception): bool
    {
        return (int) $run->workspace_id === (int) $exception->workspace_id
            && (int) $run->managed_environment_id === (int) $exception->managed_environment_id;
    }

    private function findingExceptionDetailUrl(FindingException $exception): ?string
    {
        $tenant = $exception->tenant;

        if (! $tenant instanceof ManagedEnvironment) {
            return null;
        }

        return FindingExceptionResource::getUrl('view', ['record' => $exception], panel: 'admin', tenant: $tenant);
    }

    private function proofLabel(int $proofCount): string
    {
        return $proofCount === 1
            ? '1 proof item'
            : $proofCount.' proof items';
    }

    private function numericSourceId(FindingExceptionEvidenceReference $reference): ?int
    {
        if (! is_string($reference->source_id) && ! is_numeric($reference->source_id)) {
            return null;
        }

        $sourceId = trim((string) $reference->source_id);

        if ($sourceId === '' || ! ctype_digit($sourceId)) {
            return null;
        }

        $sourceId = (int) $sourceId;

        return $sourceId > 0 ? $sourceId : null;
    }

    private function isEvidenceSnapshotReference(FindingExceptionEvidenceReference $reference): bool
    {
        return in_array((string) $reference->source_type, ['evidence_snapshot', EvidenceSnapshot::class], true);
    }

    private function isStoredReportReference(FindingExceptionEvidenceReference $reference): bool
    {
        return in_array((string) $reference->source_type, ['stored_report', StoredReport::class], true);
    }

    private function resolveRegisterState(FindingException $exception, FindingExceptionDecision $currentDecision): ?string
    {
        $status = (string) $exception->status;

        if (in_array($status, self::TERMINAL_STATUSES, true)) {
            return $this->isRecentlyClosed($currentDecision->decided_at)
                ? 'recently_closed'
                : null;
        }

        return 'open';
    }

    private function resolveNextActionLabel(FindingException $exception, FindingExceptionDecision $currentDecision): string
    {
        if ($exception->isPendingRenewal() || $currentDecision->decision_type === FindingExceptionDecision::TYPE_RENEWAL_REQUESTED) {
            return 'Review renewal';
        }

        if ($exception->isPending()) {
            return 'Review approval';
        }

        return 'Review follow-up';
    }

    private function isRecentlyClosed(?CarbonInterface $decidedAt): bool
    {
        if (! $decidedAt instanceof CarbonInterface) {
            return false;
        }

        return $decidedAt->greaterThanOrEqualTo(now()->startOfDay()->subDays(self::RECENTLY_CLOSED_DAYS));
    }
}
