<?php

declare(strict_types=1);

namespace App\Support\Baselines;

use App\Models\BaselineProfile;
use App\Models\BaselineTenantAssignment;
use App\Models\Finding;
use App\Models\InventoryItem;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use Illuminate\Support\Facades\Cache;

final class BaselineCompareStats
{
    /**
     * @param  array<string, int>  $severityCounts
     * @param  list<string>  $uncoveredTypes
     * @param  array<string, int>  $evidenceGapsTopReasons
     */
    private function __construct(
        public readonly string $state,
        public readonly ?string $message,
        public readonly ?string $profileName,
        public readonly ?int $profileId,
        public readonly ?int $snapshotId,
        public readonly ?int $duplicateNamePoliciesCount,
        public readonly ?int $operationRunId,
        public readonly ?int $findingsCount,
        public readonly array $severityCounts,
        public readonly ?string $lastComparedHuman,
        public readonly ?string $lastComparedIso,
        public readonly ?string $failureReason,
        public readonly ?string $reasonCode = null,
        public readonly ?string $reasonMessage = null,
        public readonly ?string $coverageStatus = null,
        public readonly ?int $uncoveredTypesCount = null,
        public readonly array $uncoveredTypes = [],
        public readonly ?string $fidelity = null,
        public readonly ?int $evidenceGapsCount = null,
        public readonly array $evidenceGapsTopReasons = [],
        public readonly ?array $rbacRoleDefinitionSummary = null,
    ) {}

    public static function forTenant(?Tenant $tenant): self
    {
        if (! $tenant instanceof Tenant) {
            return self::empty('no_tenant', 'No tenant selected.');
        }

        $assignment = BaselineTenantAssignment::query()
            ->where('tenant_id', $tenant->getKey())
            ->first();

        if (! $assignment instanceof BaselineTenantAssignment) {
            return self::empty(
                'no_assignment',
                'This tenant has no baseline assignment. A workspace manager can assign a baseline profile to this tenant.',
            );
        }

        $profile = $assignment->baselineProfile;

        if (! $profile instanceof BaselineProfile) {
            return self::empty(
                'no_assignment',
                'The assigned baseline profile no longer exists.',
            );
        }

        $profileName = (string) $profile->name;
        $profileId = (int) $profile->getKey();
        $snapshotId = $profile->active_snapshot_id !== null ? (int) $profile->active_snapshot_id : null;

        $profileScope = BaselineScope::fromJsonb(
            is_array($profile->scope_jsonb) ? $profile->scope_jsonb : null,
        );
        $overrideScope = $assignment->override_scope_jsonb !== null
            ? BaselineScope::fromJsonb(is_array($assignment->override_scope_jsonb) ? $assignment->override_scope_jsonb : null)
            : null;
        $effectiveScope = BaselineScope::effective($profileScope, $overrideScope);

        $duplicateNamePoliciesCount = self::duplicateNamePoliciesCount($tenant, $effectiveScope);

        if ($snapshotId === null) {
            return self::empty(
                'no_snapshot',
                'The baseline profile has no active snapshot yet. A workspace manager needs to capture a snapshot first.',
                profileName: $profileName,
                profileId: $profileId,
                duplicateNamePoliciesCount: $duplicateNamePoliciesCount,
            );
        }

        $latestRun = OperationRun::query()
            ->where('tenant_id', $tenant->getKey())
            ->where('type', 'baseline_compare')
            ->latest('id')
            ->first();

        [$coverageStatus, $uncoveredTypes, $fidelity] = self::coverageInfoForRun($latestRun);
        [$evidenceGapsCount, $evidenceGapsTopReasons] = self::evidenceGapSummaryForRun($latestRun);
        [$reasonCode, $reasonMessage] = self::reasonInfoForRun($latestRun);
        $rbacRoleDefinitionSummary = self::rbacRoleDefinitionSummaryForRun($latestRun);

        // Active run (queued/running)
        if ($latestRun instanceof OperationRun && in_array($latestRun->status, ['queued', 'running'], true)) {
            return new self(
                state: 'comparing',
                message: 'A baseline comparison is currently in progress.',
                profileName: $profileName,
                profileId: $profileId,
                snapshotId: $snapshotId,
                duplicateNamePoliciesCount: $duplicateNamePoliciesCount,
                operationRunId: (int) $latestRun->getKey(),
                findingsCount: null,
                severityCounts: [],
                lastComparedHuman: null,
                lastComparedIso: null,
                failureReason: null,
                reasonCode: $reasonCode,
                reasonMessage: $reasonMessage,
                coverageStatus: $coverageStatus,
                uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
                uncoveredTypes: $uncoveredTypes,
                fidelity: $fidelity,
                evidenceGapsCount: $evidenceGapsCount,
                evidenceGapsTopReasons: $evidenceGapsTopReasons,
                rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary,
            );
        }

        // Failed run — explicit error state
        if ($latestRun instanceof OperationRun && $latestRun->outcome === 'failed') {
            $failureSummary = is_array($latestRun->failure_summary) ? $latestRun->failure_summary : [];
            $failureReason = $failureSummary['message']
                ?? $failureSummary['reason']
                ?? 'The comparison job failed. Check the run details for more information.';

            return new self(
                state: 'failed',
                message: (string) $failureReason,
                profileName: $profileName,
                profileId: $profileId,
                snapshotId: $snapshotId,
                duplicateNamePoliciesCount: $duplicateNamePoliciesCount,
                operationRunId: (int) $latestRun->getKey(),
                findingsCount: null,
                severityCounts: [],
                lastComparedHuman: $latestRun->finished_at?->diffForHumans(),
                lastComparedIso: $latestRun->finished_at?->toIso8601String(),
                failureReason: (string) $failureReason,
                reasonCode: $reasonCode,
                reasonMessage: $reasonMessage,
                coverageStatus: $coverageStatus,
                uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
                uncoveredTypes: $uncoveredTypes,
                fidelity: $fidelity,
                evidenceGapsCount: $evidenceGapsCount,
                evidenceGapsTopReasons: $evidenceGapsTopReasons,
                rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary,
            );
        }

        $lastComparedHuman = null;
        $lastComparedIso = null;

        if ($latestRun instanceof OperationRun && $latestRun->finished_at !== null) {
            $lastComparedHuman = $latestRun->finished_at->diffForHumans();
            $lastComparedIso = $latestRun->finished_at->toIso8601String();
        }

        $scopeKey = 'baseline_profile:'.$profile->getKey();

        // Single grouped query instead of 4 separate COUNT queries
        $severityRows = Finding::query()
            ->where('tenant_id', $tenant->getKey())
            ->where('finding_type', Finding::FINDING_TYPE_DRIFT)
            ->where('source', 'baseline.compare')
            ->where('scope_key', $scopeKey)
            ->whereIn('status', Finding::openStatusesForQuery())
            ->selectRaw('severity, count(*) as cnt')
            ->groupBy('severity')
            ->pluck('cnt', 'severity');

        $totalFindings = (int) $severityRows->sum();
        $severityCounts = [
            'high' => (int) ($severityRows[Finding::SEVERITY_HIGH] ?? 0),
            'medium' => (int) ($severityRows[Finding::SEVERITY_MEDIUM] ?? 0),
            'low' => (int) ($severityRows[Finding::SEVERITY_LOW] ?? 0),
        ];

        if ($totalFindings > 0) {
            return new self(
                state: 'ready',
                message: null,
                profileName: $profileName,
                profileId: $profileId,
                snapshotId: $snapshotId,
                duplicateNamePoliciesCount: $duplicateNamePoliciesCount,
                operationRunId: $latestRun instanceof OperationRun ? (int) $latestRun->getKey() : null,
                findingsCount: $totalFindings,
                severityCounts: $severityCounts,
                lastComparedHuman: $lastComparedHuman,
                lastComparedIso: $lastComparedIso,
                failureReason: null,
                reasonCode: $reasonCode,
                reasonMessage: $reasonMessage,
                coverageStatus: $coverageStatus,
                uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
                uncoveredTypes: $uncoveredTypes,
                fidelity: $fidelity,
                evidenceGapsCount: $evidenceGapsCount,
                evidenceGapsTopReasons: $evidenceGapsTopReasons,
                rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary,
            );
        }

        if ($latestRun instanceof OperationRun && $latestRun->status === 'completed' && in_array($latestRun->outcome, ['succeeded', 'partially_succeeded'], true)) {
            return new self(
                state: 'ready',
                message: $latestRun->outcome === 'succeeded'
                    ? 'No open drift findings for this baseline comparison. The tenant matches the baseline.'
                    : 'Comparison completed with warnings. Findings may be incomplete due to missing coverage.',
                profileName: $profileName,
                profileId: $profileId,
                snapshotId: $snapshotId,
                duplicateNamePoliciesCount: $duplicateNamePoliciesCount,
                operationRunId: (int) $latestRun->getKey(),
                findingsCount: 0,
                severityCounts: $severityCounts,
                lastComparedHuman: $lastComparedHuman,
                lastComparedIso: $lastComparedIso,
                failureReason: null,
                reasonCode: $reasonCode,
                reasonMessage: $reasonMessage,
                coverageStatus: $coverageStatus,
                uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
                uncoveredTypes: $uncoveredTypes,
                fidelity: $fidelity,
                evidenceGapsCount: $evidenceGapsCount,
                evidenceGapsTopReasons: $evidenceGapsTopReasons,
                rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary,
            );
        }

        return new self(
            state: 'idle',
            message: 'Baseline profile is assigned and has a snapshot. Run "Compare Now" to check for drift.',
            profileName: $profileName,
            profileId: $profileId,
            snapshotId: $snapshotId,
            duplicateNamePoliciesCount: $duplicateNamePoliciesCount,
            operationRunId: null,
            findingsCount: null,
            severityCounts: $severityCounts,
            lastComparedHuman: $lastComparedHuman,
            lastComparedIso: $lastComparedIso,
            failureReason: null,
            reasonCode: $reasonCode,
            reasonMessage: $reasonMessage,
            coverageStatus: $coverageStatus,
            uncoveredTypesCount: $uncoveredTypes !== [] ? count($uncoveredTypes) : 0,
            uncoveredTypes: $uncoveredTypes,
            fidelity: $fidelity,
            evidenceGapsCount: $evidenceGapsCount,
            evidenceGapsTopReasons: $evidenceGapsTopReasons,
            rbacRoleDefinitionSummary: $rbacRoleDefinitionSummary,
        );
    }

    /**
     * Create a DTO for widget consumption (only open/new findings).
     */
    public static function forWidget(?Tenant $tenant): self
    {
        if (! $tenant instanceof Tenant) {
            return self::empty('no_tenant', null);
        }

        $assignment = BaselineTenantAssignment::query()
            ->where('tenant_id', $tenant->getKey())
            ->with('baselineProfile')
            ->first();

        if (! $assignment instanceof BaselineTenantAssignment || $assignment->baselineProfile === null) {
            return self::empty('no_assignment', null);
        }

        $profile = $assignment->baselineProfile;
        $scopeKey = 'baseline_profile:'.$profile->getKey();

        $severityRows = Finding::query()
            ->where('tenant_id', $tenant->getKey())
            ->where('finding_type', Finding::FINDING_TYPE_DRIFT)
            ->where('source', 'baseline.compare')
            ->where('scope_key', $scopeKey)
            ->where('status', Finding::STATUS_NEW)
            ->selectRaw('severity, count(*) as cnt')
            ->groupBy('severity')
            ->pluck('cnt', 'severity');

        $totalFindings = (int) $severityRows->sum();

        $latestRun = OperationRun::query()
            ->where('tenant_id', $tenant->getKey())
            ->where('type', 'baseline_compare')
            ->where('context->baseline_profile_id', (string) $profile->getKey())
            ->whereNotNull('completed_at')
            ->latest('completed_at')
            ->first();

        return new self(
            state: $totalFindings > 0 ? 'ready' : 'idle',
            message: null,
            profileName: (string) $profile->name,
            profileId: (int) $profile->getKey(),
            snapshotId: $profile->active_snapshot_id !== null ? (int) $profile->active_snapshot_id : null,
            duplicateNamePoliciesCount: null,
            operationRunId: $latestRun instanceof OperationRun ? (int) $latestRun->getKey() : null,
            findingsCount: $totalFindings,
            severityCounts: [
                'high' => (int) ($severityRows[Finding::SEVERITY_HIGH] ?? 0),
                'medium' => (int) ($severityRows[Finding::SEVERITY_MEDIUM] ?? 0),
                'low' => (int) ($severityRows[Finding::SEVERITY_LOW] ?? 0),
            ],
            lastComparedHuman: $latestRun?->finished_at?->diffForHumans(),
            lastComparedIso: $latestRun?->finished_at?->toIso8601String(),
            failureReason: null,
        );
    }

    private static function duplicateNamePoliciesCount(Tenant $tenant, BaselineScope $effectiveScope): int
    {
        $policyTypes = $effectiveScope->allTypes();

        if ($policyTypes === []) {
            return 0;
        }

        $latestInventorySyncRunId = self::latestInventorySyncRunId($tenant);

        $compute = static function () use ($tenant, $policyTypes, $latestInventorySyncRunId): int {
            /**
             * @var array<string, int> $countsByKey
             */
            $countsByKey = [];

            $query = InventoryItem::query()
                ->where('tenant_id', (int) $tenant->getKey())
                ->whereIn('policy_type', $policyTypes)
                ->whereNotNull('display_name')
                ->select(['id', 'policy_type', 'display_name']);

            if (is_int($latestInventorySyncRunId) && $latestInventorySyncRunId > 0) {
                $query->where('last_seen_operation_run_id', $latestInventorySyncRunId);
            }

            $query
                ->orderBy('id')
                ->chunkById(1_000, function ($inventoryItems) use (&$countsByKey): void {
                    foreach ($inventoryItems as $inventoryItem) {
                        $displayName = is_string($inventoryItem->display_name) ? $inventoryItem->display_name : null;
                        $subjectKey = BaselineSubjectKey::fromDisplayName($displayName);

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

                        $logicalKey = (string) $inventoryItem->policy_type.'|'.$subjectKey;
                        $countsByKey[$logicalKey] = ($countsByKey[$logicalKey] ?? 0) + 1;
                    }
                });

            $duplicatePolicies = 0;

            foreach ($countsByKey as $count) {
                if ($count > 1) {
                    $duplicatePolicies += $count;
                }
            }

            return $duplicatePolicies;
        };

        if (app()->environment('testing')) {
            return $compute();
        }

        $cacheKey = sprintf(
            'baseline_compare:tenant:%d:duplicate_names:%s:%s',
            (int) $tenant->getKey(),
            hash('sha256', implode('|', $policyTypes)),
            $latestInventorySyncRunId ?? 'all',
        );

        return (int) Cache::remember($cacheKey, now()->addSeconds(60), $compute);
    }

    private static function latestInventorySyncRunId(Tenant $tenant): ?int
    {
        $run = OperationRun::query()
            ->where('tenant_id', (int) $tenant->getKey())
            ->where('type', OperationRunType::InventorySync->value)
            ->where('status', OperationRunStatus::Completed->value)
            ->orderByDesc('completed_at')
            ->orderByDesc('id')
            ->first(['id']);

        return $run instanceof OperationRun ? (int) $run->getKey() : null;
    }

    /**
     * @return array{0: ?string, 1: list<string>, 2: ?string}
     */
    private static function coverageInfoForRun(?OperationRun $run): array
    {
        if (! $run instanceof OperationRun) {
            return [null, [], null];
        }

        $context = is_array($run->context) ? $run->context : [];
        $baselineCompare = $context['baseline_compare'] ?? null;

        if (! is_array($baselineCompare)) {
            return [null, [], null];
        }

        $coverage = $baselineCompare['coverage'] ?? null;
        $coverage = is_array($coverage) ? $coverage : [];

        $proof = $coverage['proof'] ?? null;
        $proof = is_bool($proof) ? $proof : null;

        $uncoveredTypes = $coverage['uncovered_types'] ?? null;
        $uncoveredTypes = is_array($uncoveredTypes) ? array_values(array_filter($uncoveredTypes, 'is_string')) : [];
        $uncoveredTypes = array_values(array_unique(array_filter(array_map('trim', $uncoveredTypes), fn (string $type): bool => $type !== '')));
        sort($uncoveredTypes, SORT_STRING);

        $coverageStatus = null;

        if ($proof === false) {
            $coverageStatus = 'unproven';
        } elseif ($uncoveredTypes !== []) {
            $coverageStatus = 'warning';
        } elseif ($proof === true) {
            $coverageStatus = 'ok';
        }

        $fidelity = $baselineCompare['fidelity'] ?? null;
        $fidelity = is_string($fidelity) ? trim($fidelity) : null;
        $fidelity = $fidelity !== '' ? $fidelity : null;

        return [$coverageStatus, $uncoveredTypes, $fidelity];
    }

    /**
     * @return array{0: ?string, 1: ?string}
     */
    private static function reasonInfoForRun(?OperationRun $run): array
    {
        if (! $run instanceof OperationRun) {
            return [null, null];
        }

        $context = is_array($run->context) ? $run->context : [];
        $baselineCompare = $context['baseline_compare'] ?? null;

        if (! is_array($baselineCompare)) {
            return [null, null];
        }

        $reasonCode = $baselineCompare['reason_code'] ?? null;
        $reasonCode = is_string($reasonCode) ? trim($reasonCode) : null;
        $reasonCode = $reasonCode !== '' ? $reasonCode : null;

        $enum = $reasonCode !== null ? BaselineCompareReasonCode::tryFrom($reasonCode) : null;

        return [$reasonCode, $enum?->message()];
    }

    /**
     * @return array{0: ?int, 1: array<string, int>}
     */
    private static function evidenceGapSummaryForRun(?OperationRun $run): array
    {
        if (! $run instanceof OperationRun) {
            return [null, []];
        }

        $context = is_array($run->context) ? $run->context : [];
        $baselineCompare = $context['baseline_compare'] ?? null;

        if (! is_array($baselineCompare)) {
            return [null, []];
        }

        $gaps = $baselineCompare['evidence_gaps'] ?? null;

        if (! is_array($gaps)) {
            return [null, []];
        }

        $count = $gaps['count'] ?? null;
        $count = is_numeric($count) ? (int) $count : null;

        $byReason = $gaps['by_reason'] ?? null;
        $byReason = is_array($byReason) ? $byReason : [];

        $normalized = [];

        foreach ($byReason as $reason => $value) {
            if (! is_string($reason) || trim($reason) === '' || ! is_numeric($value)) {
                continue;
            }

            $intValue = (int) $value;

            if ($intValue <= 0) {
                continue;
            }

            $normalized[trim($reason)] = $intValue;
        }

        if ($count === null) {
            $count = array_sum($normalized);
        }

        arsort($normalized);

        return [$count, array_slice($normalized, 0, 6, true)];
    }

    /**
     * @return array{total_compared: int, unchanged: int, modified: int, missing: int, unexpected: int}|null
     */
    private static function rbacRoleDefinitionSummaryForRun(?OperationRun $run): ?array
    {
        if (! $run instanceof OperationRun) {
            return null;
        }

        $context = is_array($run->context) ? $run->context : [];
        $baselineCompare = $context['baseline_compare'] ?? null;
        $summary = is_array($baselineCompare) ? ($baselineCompare['rbac_role_definitions'] ?? null) : null;

        if (! is_array($summary)) {
            return null;
        }

        return [
            'total_compared' => (int) ($summary['total_compared'] ?? 0),
            'unchanged' => (int) ($summary['unchanged'] ?? 0),
            'modified' => (int) ($summary['modified'] ?? 0),
            'missing' => (int) ($summary['missing'] ?? 0),
            'unexpected' => (int) ($summary['unexpected'] ?? 0),
        ];
    }

    private static function empty(
        string $state,
        ?string $message,
        ?string $profileName = null,
        ?int $profileId = null,
        ?int $duplicateNamePoliciesCount = null,
    ): self {
        return new self(
            state: $state,
            message: $message,
            profileName: $profileName,
            profileId: $profileId,
            snapshotId: null,
            duplicateNamePoliciesCount: $duplicateNamePoliciesCount,
            operationRunId: null,
            findingsCount: null,
            severityCounts: [],
            lastComparedHuman: null,
            lastComparedIso: null,
            failureReason: null,
        );
    }
}
