<?php

declare(strict_types=1);

namespace App\Services\Evidence;

use App\Jobs\GenerateEvidenceSnapshotJob;
use App\Models\EvidenceSnapshot;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Services\Evidence\Contracts\EvidenceSourceProvider;
use App\Services\Evidence\Sources\BaselineDriftPostureSource;
use App\Services\Evidence\Sources\EntraAdminRolesSource;
use App\Services\Evidence\Sources\FindingsSummarySource;
use App\Services\Evidence\Sources\OperationsSummarySource;
use App\Services\Evidence\Sources\PermissionPostureSource;
use App\Services\OperationRunService;
use App\Support\Audit\AuditActionId;
use App\Support\Artifacts\ArtifactSourceResolver;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\OperationRunType;
use InvalidArgumentException;

final class EvidenceSnapshotService
{
    public function __construct(
        private readonly OperationRunService $operationRuns,
        private readonly WorkspaceAuditLogger $auditLogger,
        private readonly EvidenceCompletenessEvaluator $completenessEvaluator,
        private readonly ArtifactSourceResolver $artifactSourceResolver,
    ) {}

    public function generate(ManagedEnvironment $tenant, User $user, bool $allowStale = false): EvidenceSnapshot
    {
        $fingerprint = $this->computeFingerprint($tenant);
        $existing = $this->findExistingSnapshot($tenant, $fingerprint);

        if ($existing instanceof EvidenceSnapshot) {
            return $existing;
        }

        $operationRun = $this->operationRuns->ensureRunWithIdentity(
            tenant: $tenant,
            type: OperationRunType::EvidenceSnapshotGenerate->value,
            identityInputs: [
                'managed_environment_id' => (int) $tenant->getKey(),
                'fingerprint' => $fingerprint,
            ],
            context: [
                'managed_environment_id' => (int) $tenant->getKey(),
                'workspace_id' => (int) $tenant->workspace_id,
                'allow_stale' => $allowStale,
                'fingerprint' => $fingerprint,
            ],
            initiator: $user,
        );

        $snapshot = EvidenceSnapshot::query()->create([
            'managed_environment_id' => (int) $tenant->getKey(),
            'workspace_id' => (int) $tenant->workspace_id,
            'operation_run_id' => (int) $operationRun->getKey(),
            'initiated_by_user_id' => (int) $user->getKey(),
            'fingerprint' => $fingerprint,
            'status' => EvidenceSnapshotStatus::Queued->value,
            'completeness_state' => EvidenceCompletenessState::Missing->value,
            'summary' => [
                'allow_stale' => $allowStale,
                'requested_at' => now()->toIso8601String(),
            ],
        ]);

        $this->operationRuns->dispatchOrFail($operationRun, function () use ($snapshot, $operationRun): void {
            GenerateEvidenceSnapshotJob::dispatch(
                snapshotId: (int) $snapshot->getKey(),
                operationRunId: (int) $operationRun->getKey(),
            );
        });

        $this->auditLogger->log(
            workspace: $tenant->workspace,
            action: AuditActionId::EvidenceSnapshotCreated,
            context: [
                'metadata' => [
                    'status' => EvidenceSnapshotStatus::Queued->value,
                ],
            ],
            actor: $user,
            resourceType: 'evidence_snapshot',
            resourceId: (string) $snapshot->getKey(),
            targetLabel: sprintf('Evidence snapshot #%d', (int) $snapshot->getKey()),
            operationRunId: (int) $operationRun->getKey(),
            tenant: $tenant,
        );

        return $snapshot;
    }

    public function refresh(EvidenceSnapshot $snapshot, User $user): EvidenceSnapshot
    {
        $tenant = $snapshot->tenant;

        if (! $tenant instanceof ManagedEnvironment) {
            throw new InvalidArgumentException('Snapshot tenant could not be resolved.');
        }

        $refreshed = $this->generate($tenant, $user);

        $this->auditLogger->log(
            workspace: $tenant->workspace,
            action: AuditActionId::EvidenceSnapshotRefreshed,
            context: [
                'metadata' => [
                    'previous_snapshot_id' => (int) $snapshot->getKey(),
                    'new_snapshot_id' => (int) $refreshed->getKey(),
                ],
            ],
            actor: $user,
            resourceType: 'evidence_snapshot',
            resourceId: (string) $refreshed->getKey(),
            targetLabel: sprintf('Evidence snapshot #%d', (int) $refreshed->getKey()),
            operationRunId: $refreshed->operation_run_id,
            tenant: $tenant,
        );

        return $refreshed;
    }

    public function expire(EvidenceSnapshot $snapshot, User $user, string $reason): EvidenceSnapshot
    {
        $reason = $this->validatedReason($reason, 'expiration_reason');

        $snapshot->forceFill([
            'status' => EvidenceSnapshotStatus::Expired->value,
            'expires_at' => now(),
        ])->save();

        $tenant = $snapshot->tenant;

        if ($tenant instanceof ManagedEnvironment) {
            $this->auditLogger->log(
                workspace: $tenant->workspace,
                action: AuditActionId::EvidenceSnapshotExpired,
                context: [
                    'metadata' => [
                        'before_status' => EvidenceSnapshotStatus::Active->value,
                        'after_status' => EvidenceSnapshotStatus::Expired->value,
                        'reason' => $reason,
                    ],
                ],
                actor: $user,
                resourceType: 'evidence_snapshot',
                resourceId: (string) $snapshot->getKey(),
                targetLabel: sprintf('Evidence snapshot #%d', (int) $snapshot->getKey()),
                tenant: $tenant,
            );
        }

        return $snapshot;
    }

    /**
     * @return list<EvidenceSourceProvider>
     */
    public function providers(): array
    {
        return [
            app(FindingsSummarySource::class),
            app(PermissionPostureSource::class),
            app(EntraAdminRolesSource::class),
            app(BaselineDriftPostureSource::class),
            app(OperationsSummarySource::class),
        ];
    }

    /**
     * @return array{items: list<array<string, mixed>>, fingerprint: string, completeness: string, summary: array<string, mixed>}
     */
    public function buildSnapshotPayload(ManagedEnvironment $tenant): array
    {
        $items = [];
        $fingerprintPayload = [];

        foreach ($this->providers() as $provider) {
            $item = $provider->collect($tenant);
            $descriptor = $this->artifactSourceResolver->forEvidenceProviderPayload($tenant, $item);
            $providerDetail = $this->artifactSourceResolver->providerDetailForEvidenceProviderPayload($item);
            $item['source_descriptor'] = $descriptor->toArray();
            $item['provider_detail'] = $providerDetail->toArray();
            $item['summary_payload'] = array_replace(is_array($item['summary_payload'] ?? null) ? $item['summary_payload'] : [], [
                'source_descriptor' => $item['source_descriptor'],
                'provider_detail' => $item['provider_detail'],
            ]);
            $items[] = $item;
            $fingerprintPayload[$provider->key()] = $item['fingerprint_payload'];
        }

        $completeness = $this->completenessEvaluator->evaluate(array_map(
            static fn (array $item): array => [
                'state' => (string) $item['state'],
                'required' => (bool) $item['required'],
            ],
            $items,
        ));
        $itemsByKey = collect($items)->keyBy('dimension_key');
        $findingsSummary = is_array($itemsByKey->get('findings_summary')['summary_payload'] ?? null)
            ? $itemsByKey->get('findings_summary')['summary_payload']
            : [];
        $operationsSummary = is_array($itemsByKey->get('operations_summary')['summary_payload'] ?? null)
            ? $itemsByKey->get('operations_summary')['summary_payload']
            : [];

        $summary = [
            'dimension_count' => count($items),
            'finding_count' => (int) ($findingsSummary['count'] ?? 0),
            'report_count' => count(array_filter($items, static fn (array $item): bool => in_array($item['dimension_key'], ['permission_posture', 'entra_admin_roles'], true) && $item['source_record_id'] !== null)),
            'operation_count' => (int) ($operationsSummary['operation_count'] ?? 0),
            'missing_dimensions' => count(array_filter($items, static fn (array $item): bool => $item['state'] === EvidenceCompletenessState::Missing->value)),
            'stale_dimensions' => count(array_filter($items, static fn (array $item): bool => $item['state'] === EvidenceCompletenessState::Stale->value)),
            'dimensions' => array_map(static fn (array $item): array => [
                'key' => $item['dimension_key'],
                'state' => $item['state'],
                'required' => $item['required'],
                'source_descriptor' => $item['source_descriptor'],
            ], $items),
            'finding_outcomes' => is_array($findingsSummary['outcome_counts'] ?? null)
                ? $findingsSummary['outcome_counts']
                : [],
            'finding_report_buckets' => is_array($findingsSummary['report_bucket_counts'] ?? null)
                ? $findingsSummary['report_bucket_counts']
                : [],
            'canonical_controls' => is_array($findingsSummary['canonical_controls'] ?? null)
                ? $findingsSummary['canonical_controls']
                : [],
            'risk_acceptance' => is_array($findingsSummary['risk_acceptance'] ?? null)
                ? $findingsSummary['risk_acceptance']
                : [
                    'status_marked_count' => 0,
                    'valid_governed_count' => 0,
                    'warning_count' => 0,
                    'expired_count' => 0,
                    'revoked_count' => 0,
                    'missing_exception_count' => 0,
                ],
            'hardening' => [
                'rbac_last_checked_at' => $tenant->rbac_last_checked_at?->toIso8601String(),
                'rbac_last_setup_at' => $tenant->rbac_last_setup_at?->toIso8601String(),
                'rbac_canary_results' => $tenant->rbac_canary_results,
                'rbac_last_warnings' => $tenant->rbac_last_warnings,
                'rbac_scope_mode' => $tenant->rbac_scope_mode,
            ],
        ];

        return [
            'items' => $items,
            'fingerprint' => EvidenceSnapshotFingerprint::hash($fingerprintPayload),
            'completeness' => $completeness->value,
            'summary' => $summary,
        ];
    }

    public function computeFingerprint(ManagedEnvironment $tenant): string
    {
        return $this->buildSnapshotPayload($tenant)['fingerprint'];
    }

    private function validatedReason(mixed $reason, string $field): string
    {
        if (! is_string($reason)) {
            throw new InvalidArgumentException(sprintf('%s is required.', $field));
        }

        $resolved = trim($reason);

        if ($resolved === '') {
            throw new InvalidArgumentException(sprintf('%s is required.', $field));
        }

        if (mb_strlen($resolved) > 2000) {
            throw new InvalidArgumentException(sprintf('%s must be at most 2000 characters.', $field));
        }

        return $resolved;
    }

    public function checkActiveRun(ManagedEnvironment $tenant): bool
    {
        return $this->operationRuns->findCanonicalRunWithIdentity(
            tenant: $tenant,
            type: OperationRunType::EvidenceSnapshotGenerate->value,
            identityInputs: [
                'managed_environment_id' => (int) $tenant->getKey(),
                'fingerprint' => $this->computeFingerprint($tenant),
            ],
        ) !== null;
    }

    private function findExistingSnapshot(ManagedEnvironment $tenant, string $fingerprint): ?EvidenceSnapshot
    {
        return EvidenceSnapshot::query()
            ->forTenant((int) $tenant->getKey())
            ->where('workspace_id', (int) $tenant->workspace_id)
            ->where('fingerprint', $fingerprint)
            ->where('status', EvidenceSnapshotStatus::Active->value)
            ->where(function ($query): void {
                $query->whereNull('expires_at')->orWhere('expires_at', '>', now());
            })
            ->first();
    }
}
