<?php

declare(strict_types=1);

namespace App\Services\PermissionPosture;

use App\Models\AlertRule;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\StoredReport;
use App\Models\ManagedEnvironment;
use App\Services\Findings\FindingSlaPolicy;
use App\Services\Findings\FindingWorkflowService;
use App\Support\ProductTelemetry\ProductTelemetryRecorder;
use App\Support\ProductTelemetry\ProductUsageEventCatalog;
use Carbon\CarbonImmutable;

/**
 * Generates, auto-resolves, and re-opens permission posture findings
 * based on the output of ManagedEnvironmentPermissionService::compare().
 */
final class PermissionPostureFindingGenerator implements FindingGeneratorContract
{
    public function __construct(
        private readonly PostureScoreCalculator $scoreCalculator,
        private readonly FindingSlaPolicy $slaPolicy,
        private readonly ProductTelemetryRecorder $productTelemetryRecorder,
        private readonly ?FindingWorkflowService $findingWorkflowService = null,
    ) {}

    /**
     * @param  array{overall_status: string, permissions: array<int, array{key: string, type: string, features: array<int, string>, status: string, ...}>, ...}  $permissionComparison
     */
    public function generate(ManagedEnvironment $tenant, array $permissionComparison, ?OperationRun $operationRun = null): PostureResult
    {
        $permissions = $permissionComparison['permissions'] ?? [];
        $permissions = is_array($permissions) ? $permissions : [];

        $observedAt = $this->resolveObservedAt($permissionComparison, $operationRun);

        $created = 0;
        $resolved = 0;
        $reopened = 0;
        $unchanged = 0;
        $errors = 0;
        $alertEvents = [];
        $processedPermissionKeys = [];

        foreach ($permissions as $permission) {
            if (! is_array($permission)) {
                continue;
            }

            $key = $permission['key'] ?? '';
            $type = $permission['type'] ?? 'application';
            $status = $permission['status'] ?? 'granted';
            $features = is_array($permission['features'] ?? null) ? $permission['features'] : [];

            if ($key === '') {
                continue;
            }

            $processedPermissionKeys[] = $key;

            if ($status === 'error') {
                $this->handleErrorPermission($tenant, $key, $type, $features, $observedAt, $operationRun);
                $errors++;

                continue;
            }

            if ($status === 'missing') {
                $result = $this->handleMissingPermission($tenant, $key, $type, $features, $observedAt, $operationRun);

                if ($result === 'created') {
                    $created++;
                    $alertEvents[] = $this->buildAlertEvent($tenant, $key, $type, $features);
                } elseif ($result === 'reopened') {
                    $reopened++;
                    $alertEvents[] = $this->buildAlertEvent($tenant, $key, $type, $features);
                } else {
                    $unchanged++;
                }

                continue;
            }

            // status === 'granted'
            if ($this->resolveExistingFinding($tenant, $key, 'permission_granted', $observedAt)) {
                $resolved++;
            }
        }

        // Step 9: Resolve stale findings for permissions removed from registry
        $resolved += $this->resolveStaleFindings($tenant, $processedPermissionKeys, $observedAt);

        $postureScore = $this->scoreCalculator->calculate($permissionComparison);

        $report = $this->createStoredReport($tenant, $permissionComparison, $permissions, $postureScore);
        $this->recordStoredReportTelemetry($report, $operationRun);

        return new PostureResult(
            findingsCreated: $created,
            findingsResolved: $resolved,
            findingsReopened: $reopened,
            findingsUnchanged: $unchanged,
            errorsRecorded: $errors,
            postureScore: $postureScore,
            storedReportId: (int) $report->getKey(),
        );
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function getAlertEvents(): array
    {
        return $this->alertEvents;
    }

    /** @var array<int, array<string, mixed>> */
    private array $alertEvents = [];

    private function handleMissingPermission(
        ManagedEnvironment $tenant,
        string $key,
        string $type,
        array $features,
        CarbonImmutable $observedAt,
        ?OperationRun $operationRun,
    ): string {
        $fingerprint = $this->fingerprint($tenant, $key);
        $evidence = $this->buildEvidence($key, $type, 'missing', $features, $observedAt);
        $severity = $this->deriveSeverity(count($features));

        $finding = Finding::query()
            ->where('managed_environment_id', $tenant->getKey())
            ->where('finding_type', Finding::FINDING_TYPE_PERMISSION_POSTURE)
            ->where('fingerprint', $fingerprint)
            ->first();

        if ($finding instanceof Finding) {
            $this->observeFinding($finding, $tenant, $observedAt, $severity);

            $finding->forceFill([
                'severity' => $severity,
                'evidence_jsonb' => $evidence,
                'current_operation_run_id' => $operationRun?->getKey(),
            ]);

            if ($finding->status === Finding::STATUS_RESOLVED) {
                $resolvedAt = $finding->resolved_at;

                if ($resolvedAt === null || $observedAt->greaterThan(CarbonImmutable::instance($resolvedAt))) {
                    $finding->save();

                    $this->findingWorkflowService()->reopenBySystem(
                        finding: $finding,
                        tenant: $tenant,
                        reopenedAt: $observedAt,
                        operationRunId: $operationRun?->getKey(),
                    );

                    return 'reopened';
                }
            }

            // Already open (new or acknowledged) — unchanged
            $finding->save();

            return 'unchanged';
        }

        $slaDays = $this->slaPolicy->daysForSeverity($severity, $tenant);

        Finding::create([
            'managed_environment_id' => (int) $tenant->getKey(),
            'finding_type' => Finding::FINDING_TYPE_PERMISSION_POSTURE,
            'source' => 'permission_check',
            'scope_key' => hash('sha256', 'permission_posture:'.$tenant->getKey()),
            'fingerprint' => $fingerprint,
            'subject_type' => 'permission',
            'subject_external_id' => $key,
            'severity' => $severity,
            'status' => Finding::STATUS_NEW,
            'evidence_jsonb' => $evidence,
            'current_operation_run_id' => $operationRun?->getKey(),
            'first_seen_at' => $observedAt,
            'last_seen_at' => $observedAt,
            'times_seen' => 1,
            'sla_days' => $slaDays,
            'due_at' => $this->slaPolicy->dueAtForSeverity($severity, $tenant, $observedAt),
        ]);

        return 'created';
    }

    private function handleErrorPermission(
        ManagedEnvironment $tenant,
        string $key,
        string $type,
        array $features,
        CarbonImmutable $observedAt,
        ?OperationRun $operationRun,
    ): void {
        $fingerprint = $this->errorFingerprint($tenant, $key);

        $evidence = $this->buildEvidence($key, $type, 'error', $features, $observedAt);
        $evidence['check_error'] = true;
        $severity = Finding::SEVERITY_LOW;

        $existing = Finding::query()
            ->where('managed_environment_id', $tenant->getKey())
            ->where('finding_type', Finding::FINDING_TYPE_PERMISSION_POSTURE)
            ->where('fingerprint', $fingerprint)
            ->first();

        if ($existing instanceof Finding) {
            $this->observeFinding($existing, $tenant, $observedAt, $severity);

            $existing->forceFill([
                'severity' => $severity,
                'evidence_jsonb' => $evidence,
                'current_operation_run_id' => $operationRun?->getKey(),
            ]);

            if ($existing->status === Finding::STATUS_RESOLVED) {
                $resolvedAt = $existing->resolved_at;

                if ($resolvedAt === null || $observedAt->greaterThan(CarbonImmutable::instance($resolvedAt))) {
                    $existing->save();

                    $this->findingWorkflowService()->reopenBySystem(
                        finding: $existing,
                        tenant: $tenant,
                        reopenedAt: $observedAt,
                        operationRunId: $operationRun?->getKey(),
                    );

                    return;
                }
            }

            $existing->save();

            return;
        }

        $slaDays = $this->slaPolicy->daysForSeverity($severity, $tenant);

        Finding::create([
            'managed_environment_id' => (int) $tenant->getKey(),
            'finding_type' => Finding::FINDING_TYPE_PERMISSION_POSTURE,
            'source' => 'permission_check',
            'scope_key' => hash('sha256', 'permission_posture_error:'.$tenant->getKey()),
            'fingerprint' => $fingerprint,
            'subject_type' => 'permission',
            'subject_external_id' => $key,
            'severity' => $severity,
            'status' => Finding::STATUS_NEW,
            'evidence_jsonb' => $evidence,
            'current_operation_run_id' => $operationRun?->getKey(),
            'first_seen_at' => $observedAt,
            'last_seen_at' => $observedAt,
            'times_seen' => 1,
            'sla_days' => $slaDays,
            'due_at' => $this->slaPolicy->dueAtForSeverity($severity, $tenant, $observedAt),
        ]);
    }

    private function resolveExistingFinding(ManagedEnvironment $tenant, string $key, string $reason, CarbonImmutable $observedAt): bool
    {
        $fingerprint = $this->fingerprint($tenant, $key);

        $finding = Finding::query()
            ->where('managed_environment_id', $tenant->getKey())
            ->where('finding_type', Finding::FINDING_TYPE_PERMISSION_POSTURE)
            ->where('fingerprint', $fingerprint)
            ->whereIn('status', Finding::openStatusesForQuery())
            ->first();

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

        $this->findingWorkflowService()->resolveBySystem(
            finding: $finding,
            tenant: $tenant,
            reason: $reason,
            resolvedAt: $observedAt,
        );

        return true;
    }

    /**
     * Resolve any open permission_posture findings whose permission_key is not
     * in the current comparison (handles registry removals).
     */
    private function resolveStaleFindings(ManagedEnvironment $tenant, array $processedPermissionKeys, CarbonImmutable $observedAt): int
    {
        $staleFindings = Finding::query()
            ->where('managed_environment_id', $tenant->getKey())
            ->where('finding_type', Finding::FINDING_TYPE_PERMISSION_POSTURE)
            ->whereIn('status', Finding::openStatusesForQuery())
            ->get();

        $resolved = 0;

        foreach ($staleFindings as $finding) {
            $evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
            $permissionKey = $evidence['permission_key'] ?? null;

            // Skip error findings (they have check_error=true in evidence)
            if (($evidence['check_error'] ?? false) === true) {
                continue;
            }

            if ($permissionKey !== null && ! in_array($permissionKey, $processedPermissionKeys, true)) {
                $this->findingWorkflowService()->resolveBySystem(
                    finding: $finding,
                    tenant: $tenant,
                    reason: 'permission_removed_from_registry',
                    resolvedAt: $observedAt,
                );
                $resolved++;
            }
        }

        return $resolved;
    }

    private function resolveObservedAt(array $comparison, ?OperationRun $operationRun): CarbonImmutable
    {
        if ($operationRun?->completed_at !== null) {
            return CarbonImmutable::instance($operationRun->completed_at);
        }

        $refreshedAt = $comparison['last_refreshed_at'] ?? null;

        if (is_string($refreshedAt) && trim($refreshedAt) !== '') {
            try {
                return CarbonImmutable::parse($refreshedAt);
            } catch (\Throwable) {
                // Fall through.
            }
        }

        return CarbonImmutable::now();
    }

    private function observeFinding(Finding $finding, ManagedEnvironment $tenant, CarbonImmutable $observedAt, string $severity): void
    {
        if ($finding->first_seen_at === null) {
            $finding->first_seen_at = $observedAt;
        }

        $firstSeenAt = CarbonImmutable::instance($finding->first_seen_at);

        $lastSeenAt = $finding->last_seen_at;
        $timesSeen = is_numeric($finding->times_seen) ? (int) $finding->times_seen : 0;

        if ($lastSeenAt === null || $observedAt->greaterThan(CarbonImmutable::instance($lastSeenAt))) {
            $finding->last_seen_at = $observedAt;
            $finding->times_seen = max(0, $timesSeen) + 1;
        } elseif ($timesSeen < 1) {
            $finding->times_seen = 1;
        }

        if ($finding->sla_days === null) {
            $finding->sla_days = $this->slaPolicy->daysForSeverity($severity, $tenant);
        }

        if ($finding->due_at === null) {
            $finding->due_at = $this->slaPolicy->dueAtForSeverity($severity, $tenant, $firstSeenAt);
        }
    }

    /**
     * @param  array<int, array<string, mixed>>  $permissions
     */
    private function createStoredReport(
        ManagedEnvironment $tenant,
        array $permissionComparison,
        array $permissions,
        int $postureScore,
    ): StoredReport {
        $grantedCount = 0;

        foreach ($permissions as $permission) {
            if (is_array($permission) && ($permission['status'] ?? null) === 'granted') {
                $grantedCount++;
            }
        }

        return StoredReport::create([
            'managed_environment_id' => (int) $tenant->getKey(),
            'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
            'payload' => [
                'posture_score' => $postureScore,
                'required_count' => count($permissions),
                'granted_count' => $grantedCount,
                'checked_at' => now()->toIso8601String(),
                'permissions' => array_map(
                    static fn (array $p): array => [
                        'key' => $p['key'] ?? '',
                        'type' => $p['type'] ?? 'application',
                        'status' => $p['status'] ?? 'unknown',
                        'features' => is_array($p['features'] ?? null) ? $p['features'] : [],
                    ],
                    array_filter($permissions, static fn (mixed $p): bool => is_array($p)),
                ),
            ],
        ]);
    }

    private function recordStoredReportTelemetry(StoredReport $report, ?OperationRun $operationRun): void
    {
        if (! $operationRun instanceof OperationRun || ! is_numeric($operationRun->user_id)) {
            return;
        }

        $this->productTelemetryRecorder->record(
            eventName: ProductUsageEventCatalog::STORED_REPORT_CREATED,
            workspaceId: (int) $report->workspace_id,
            tenantId: (int) $report->managed_environment_id,
            userId: (int) $operationRun->user_id,
            subjectType: 'stored_report',
            subjectId: (int) $report->getKey(),
            metadata: [
                'report_type' => $report->report_type,
            ],
            occurredAt: $report->created_at ?? now(),
        );
    }

    /**
     * @return array<string, mixed>
     */
    private function buildAlertEvent(ManagedEnvironment $tenant, string $key, string $type, array $features): array
    {
        $event = [
            'event_type' => AlertRule::EVENT_PERMISSION_MISSING,
            'managed_environment_id' => (int) $tenant->getKey(),
            'severity' => $this->deriveSeverity(count($features)),
            'fingerprint_key' => 'permission_missing:'.$tenant->getKey().':'.$key,
            'title' => 'Missing permission: '.$key,
            'body' => sprintf(
                'ManagedEnvironment %s is missing %s. Blocked features: %s.',
                $tenant->name ?? (string) $tenant->getKey(),
                $key,
                implode(', ', $features) ?: 'none',
            ),
            'metadata' => [
                'permission_key' => $key,
                'permission_type' => $type,
                'blocked_features' => $features,
            ],
        ];

        $this->alertEvents[] = $event;

        return $event;
    }

    /**
     * @return array<string, mixed>
     */
    private function buildEvidence(string $key, string $type, string $actualStatus, array $features, CarbonImmutable $observedAt): array
    {
        return [
            'permission_key' => $key,
            'permission_type' => $type,
            'expected_status' => 'granted',
            'actual_status' => $actualStatus,
            'blocked_features' => $features,
            'checked_at' => $observedAt->toIso8601String(),
        ];
    }

    private function deriveSeverity(int $featureCount): string
    {
        return match (true) {
            $featureCount >= 3 => Finding::SEVERITY_CRITICAL,
            $featureCount === 2 => Finding::SEVERITY_HIGH,
            $featureCount === 1 => Finding::SEVERITY_MEDIUM,
            default => Finding::SEVERITY_LOW,
        };
    }

    private function fingerprint(ManagedEnvironment $tenant, string $permissionKey): string
    {
        return substr(hash('sha256', 'permission_posture:'.$tenant->getKey().':'.$permissionKey), 0, 64);
    }

    private function errorFingerprint(ManagedEnvironment $tenant, string $permissionKey): string
    {
        return substr(hash('sha256', 'permission_posture:'.$tenant->getKey().':'.$permissionKey.':error'), 0, 64);
    }

    private function findingWorkflowService(): FindingWorkflowService
    {
        return $this->findingWorkflowService ?? app(FindingWorkflowService::class);
    }
}
