<?php

declare(strict_types=1);

namespace App\Services;

use App\Exceptions\Entitlements\WorkspaceEntitlementBlockedException;
use App\Exceptions\ReviewPackEvidenceResolutionException;
use App\Jobs\GenerateReviewPackJob;
use App\Models\EvidenceSnapshot;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\ManagedEnvironment;
use App\Models\EnvironmentReview;
use App\Models\User;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
use App\Services\Evidence\EvidenceResolutionRequest;
use App\Services\Evidence\EvidenceSnapshotResolver;
use App\Support\Audit\AuditActionId;
use App\Support\OperationRunType;
use App\Support\ProductTelemetry\ProductTelemetryRecorder;
use App\Support\ProductTelemetry\ProductUsageEventCatalog;
use App\Support\ReviewPackStatus;
use Illuminate\Support\Facades\URL;

class ReviewPackService
{
    public const string REVIEW_DERIVED_DELIVERY_CONTRACT = 'auditor_ready_executive_export.v1';

    public const string EXECUTIVE_ENTRYPOINT_FILENAME = 'executive-summary.md';

    public function __construct(
        private OperationRunService $operationRunService,
        private EvidenceSnapshotResolver $snapshotResolver,
        private WorkspaceAuditLogger $auditLogger,
        private WorkspaceCommercialLifecycleResolver $workspaceCommercialLifecycleResolver,
        private ProductTelemetryRecorder $productTelemetryRecorder,
    ) {}

    /**
     * @var list<string>
     */
    private const REQUIRED_EVIDENCE_DIMENSIONS = [
        'findings_summary',
        'permission_posture',
        'entra_admin_roles',
        'baseline_drift_posture',
        'operations_summary',
    ];

    /**
     * Create an OperationRun + ReviewPack and dispatch the generation job.
     *
     * @param  array<string, mixed>  $options
     */
    public function generate(ManagedEnvironment $tenant, User $user, array $options = []): ReviewPack
    {
        $this->assertReviewPackGenerationAllowed($tenant);

        $options = $this->normalizeOptions($options);
        $snapshot = $this->resolveSnapshot($tenant);
        $fingerprint = $this->computeFingerprintForSnapshot($snapshot, $options);

        $existing = $this->findExistingPack($tenant, $fingerprint);

        if ($existing instanceof ReviewPack) {
            $this->recordReviewPackRequestTelemetry($existing, $user, 'tenant');

            return $existing;
        }

        $operationRun = $this->operationRunService->ensureRun(
            tenant: $tenant,
            type: OperationRunType::ReviewPackGenerate->value,
            inputs: [
                'include_pii' => $options['include_pii'],
                'include_operations' => $options['include_operations'],
                'evidence_snapshot_id' => (int) $snapshot->getKey(),
            ],
            initiator: $user,
        );

        if (! $operationRun->wasRecentlyCreated) {
            $queuedPack = $this->findPackForRun($tenant, $operationRun);

            if ($queuedPack instanceof ReviewPack) {
                $this->recordReviewPackRequestTelemetry($queuedPack, $user, 'tenant');

                return $queuedPack;
            }
        }

        $reviewPack = ReviewPack::create([
            'managed_environment_id' => (int) $tenant->getKey(),
            'workspace_id' => (int) $tenant->workspace_id,
            'operation_run_id' => (int) $operationRun->getKey(),
            'evidence_snapshot_id' => (int) $snapshot->getKey(),
            'initiated_by_user_id' => (int) $user->getKey(),
            'status' => ReviewPackStatus::Queued->value,
            'options' => $options,
            'summary' => [
                'finding_outcomes' => is_array($snapshot->summary['finding_outcomes'] ?? null)
                    ? $snapshot->summary['finding_outcomes']
                    : [],
                'finding_report_buckets' => is_array($snapshot->summary['finding_report_buckets'] ?? null)
                    ? $snapshot->summary['finding_report_buckets']
                    : [],
                'risk_acceptance' => is_array($snapshot->summary['risk_acceptance'] ?? null)
                    ? $snapshot->summary['risk_acceptance']
                    : [],
                'evidence_resolution' => [
                    'outcome' => 'resolved',
                    'snapshot_id' => (int) $snapshot->getKey(),
                    'snapshot_fingerprint' => (string) $snapshot->fingerprint,
                    'completeness_state' => (string) $snapshot->completeness_state,
                    'required_dimensions' => self::REQUIRED_EVIDENCE_DIMENSIONS,
                ],
            ],
        ]);

        $this->operationRunService->dispatchOrFail($operationRun, function () use ($reviewPack, $operationRun): void {
            GenerateReviewPackJob::dispatch(
                reviewPackId: (int) $reviewPack->getKey(),
                operationRunId: (int) $operationRun->getKey(),
            );
        });

        $this->recordReviewPackRequestTelemetry($reviewPack, $user, 'tenant');

        return $reviewPack;
    }

    /**
     * Create a review-derived executive pack.
     *
     * @param  array<string, mixed>  $options
     */
    public function generateFromReview(EnvironmentReview $review, User $user, array $options = []): ReviewPack
    {
        $review->loadMissing(['tenant', 'evidenceSnapshot', 'sections']);

        $tenant = $review->tenant;
        $snapshot = $review->evidenceSnapshot;

        if (! $tenant instanceof ManagedEnvironment || ! $snapshot instanceof EvidenceSnapshot) {
            throw new \InvalidArgumentException('Review exports require an anchored evidence snapshot.');
        }

        $this->assertReviewPackGenerationAllowed($tenant);

        $options = $this->normalizeOptions($options);
        $fingerprint = $this->computeFingerprintForReview($review, $options);
        $existing = $this->findExistingPackForReview($review, $fingerprint);

        if ($existing instanceof ReviewPack) {
            $this->logReviewExport($review, $user, $existing, 'reused');
            $this->recordReviewPackRequestTelemetry($existing, $user, 'environment_review');

            return $existing;
        }

        $operationRun = $this->operationRunService->ensureRun(
            tenant: $tenant,
            type: OperationRunType::ReviewPackGenerate->value,
            inputs: [
                'environment_review_id' => (int) $review->getKey(),
                'include_pii' => $options['include_pii'],
                'include_operations' => $options['include_operations'],
                'evidence_snapshot_id' => (int) $snapshot->getKey(),
            ],
            initiator: $user,
        );

        if (! $operationRun->wasRecentlyCreated) {
            $queuedPack = $this->findPackForRun($tenant, $operationRun);

            if ($queuedPack instanceof ReviewPack) {
                $this->logReviewExport($review, $user, $queuedPack, 'reused_active_run');
                $this->recordReviewPackRequestTelemetry($queuedPack, $user, 'environment_review');

                return $queuedPack;
            }
        }

        $reviewPack = ReviewPack::create([
            'managed_environment_id' => (int) $tenant->getKey(),
            'workspace_id' => (int) $tenant->workspace_id,
            'environment_review_id' => (int) $review->getKey(),
            'operation_run_id' => (int) $operationRun->getKey(),
            'evidence_snapshot_id' => (int) $snapshot->getKey(),
            'initiated_by_user_id' => (int) $user->getKey(),
            'status' => ReviewPackStatus::Queued->value,
            'options' => $options,
            'summary' => [
                'environment_review_id' => (int) $review->getKey(),
                'review_status' => (string) $review->status,
                'review_completeness_state' => (string) $review->completeness_state,
                'section_count' => $review->sections->count(),
                'delivery_bundle' => [
                    'contract' => self::REVIEW_DERIVED_DELIVERY_CONTRACT,
                    'executive_entrypoint_file' => self::EXECUTIVE_ENTRYPOINT_FILENAME,
                    'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'],
                ],
                'finding_outcomes' => is_array($review->summary['finding_outcomes'] ?? null)
                    ? $review->summary['finding_outcomes']
                    : [],
                'finding_report_buckets' => is_array($review->summary['finding_report_buckets'] ?? null)
                    ? $review->summary['finding_report_buckets']
                    : [],
                'evidence_resolution' => [
                    'outcome' => 'resolved',
                    'snapshot_id' => (int) $snapshot->getKey(),
                    'snapshot_fingerprint' => (string) $snapshot->fingerprint,
                    'completeness_state' => (string) $snapshot->completeness_state,
                    'required_dimensions' => self::REQUIRED_EVIDENCE_DIMENSIONS,
                ],
            ],
        ]);

        $this->operationRunService->dispatchOrFail($operationRun, function () use ($reviewPack, $operationRun): void {
            GenerateReviewPackJob::dispatch(
                reviewPackId: (int) $reviewPack->getKey(),
                operationRunId: (int) $operationRun->getKey(),
            );
        });

        $this->logReviewExport($review, $user, $reviewPack, 'queued');
        $this->recordReviewPackRequestTelemetry($reviewPack, $user, 'environment_review');

        return $reviewPack;
    }

    /**
     * Compute a deterministic fingerprint for deduplication.
     *
     * @param  array<string, mixed>  $options
     */
    public function computeFingerprint(ManagedEnvironment $tenant, array $options): string
    {
        return $this->computeFingerprintForSnapshot($this->resolveSnapshot($tenant), $this->normalizeOptions($options));
    }

    /**
     * Generate a signed download URL for a review pack.
     *
     * @param  array<string, scalar|null>  $parameters
     */
    public function generateDownloadUrl(ReviewPack $pack, array $parameters = []): string
    {
        $ttlMinutes = (int) config('tenantpilot.review_pack.download_url_ttl_minutes', 60);

        return URL::signedRoute(
            'admin.review-packs.download',
            array_merge(['reviewPack' => $pack->getKey()], $parameters),
            now()->addMinutes($ttlMinutes),
        );
    }

    /**
     * @return array<string, mixed>
     */
    public function reviewPackGenerationDecisionForTenant(ManagedEnvironment $tenant): array
    {
        $tenant->loadMissing('workspace');
        $decision = $this->workspaceCommercialLifecycleResolver->reviewPackStartDecisionForTenant($tenant);

        $entitlementDecision = is_array($decision['entitlement_decision'] ?? null)
            ? $decision['entitlement_decision']
            : [];

        return $decision + [
            'effective_value' => $entitlementDecision['effective_value'] ?? null,
            'source' => $decision['source'] ?? null,
            'current_usage' => $entitlementDecision['current_usage'] ?? null,
            'remaining_capacity' => $entitlementDecision['remaining_capacity'] ?? null,
        ];
    }

    private function recordReviewPackRequestTelemetry(ReviewPack $reviewPack, User $user, string $sourceSurface): void
    {
        $this->productTelemetryRecorder->record(
            eventName: ProductUsageEventCatalog::REVIEW_PACK_REQUESTED,
            workspaceId: (int) $reviewPack->workspace_id,
            tenantId: (int) $reviewPack->managed_environment_id,
            userId: (int) $user->getKey(),
            subjectType: 'review_pack',
            subjectId: (int) $reviewPack->getKey(),
            metadata: [
                'source_surface' => $sourceSurface,
                'include_operations' => (bool) ($reviewPack->options['include_operations'] ?? false),
                'include_pii' => (bool) ($reviewPack->options['include_pii'] ?? false),
            ],
            occurredAt: $reviewPack->created_at ?? now(),
        );
    }

    /**
     * Find an existing ready, non-expired pack with the same fingerprint.
     */
    public function findExistingPack(ManagedEnvironment $tenant, string $fingerprint): ?ReviewPack
    {
        return ReviewPack::query()
            ->forTenant((int) $tenant->getKey())
            ->ready()
            ->where('fingerprint', $fingerprint)
            ->where('expires_at', '>', now())
            ->first();
    }

    public function findExistingPackForReview(EnvironmentReview $review, string $fingerprint): ?ReviewPack
    {
        return ReviewPack::query()
            ->where('environment_review_id', (int) $review->getKey())
            ->ready()
            ->where('fingerprint', $fingerprint)
            ->where('expires_at', '>', now())
            ->first();
    }

    /**
     * Check if a generation run is currently active for this tenant.
     */
    public function checkActiveRun(ManagedEnvironment $tenant): bool
    {
        return OperationRun::query()
            ->where('managed_environment_id', (int) $tenant->getKey())
            ->where('type', OperationRunType::ReviewPackGenerate->value)
            ->active()
            ->exists();
    }

    public function checkActiveRunForReview(EnvironmentReview $review): bool
    {
        return OperationRun::query()
            ->where('managed_environment_id', (int) $review->managed_environment_id)
            ->where('type', OperationRunType::ReviewPackGenerate->value)
            ->whereJsonContains('context->environment_review_id', (int) $review->getKey())
            ->active()
            ->exists();
    }

    /**
     * @param  array<string, mixed>  $options
     * @return array{include_pii: bool, include_operations: bool}
     */
    private function normalizeOptions(array $options): array
    {
        return [
            'include_pii' => (bool) ($options['include_pii'] ?? config('tenantpilot.review_pack.include_pii_default', true)),
            'include_operations' => (bool) ($options['include_operations'] ?? config('tenantpilot.review_pack.include_operations_default', true)),
        ];
    }

    private function assertReviewPackGenerationAllowed(ManagedEnvironment $tenant): void
    {
        $decision = $this->reviewPackGenerationDecisionForTenant($tenant);

        if (! (bool) ($decision['is_blocked'] ?? false)) {
            return;
        }

        throw new WorkspaceEntitlementBlockedException($decision);
    }

    private function computeFingerprintForSnapshot(EvidenceSnapshot $snapshot, array $options): string
    {
        $data = [
            'managed_environment_id' => (int) $snapshot->managed_environment_id,
            'evidence_snapshot_id' => (int) $snapshot->getKey(),
            'evidence_fingerprint' => (string) $snapshot->fingerprint,
            'include_pii' => (bool) ($options['include_pii'] ?? true),
            'include_operations' => (bool) ($options['include_operations'] ?? true),
        ];

        return hash('sha256', json_encode($data, JSON_THROW_ON_ERROR));
    }

    public function computeFingerprintForReview(EnvironmentReview $review, array $options): string
    {
        $data = [
            'environment_review_id' => (int) $review->getKey(),
            'review_fingerprint' => (string) $review->fingerprint,
            'review_status' => (string) $review->status,
            'delivery_contract' => self::REVIEW_DERIVED_DELIVERY_CONTRACT,
            'include_pii' => (bool) ($options['include_pii'] ?? true),
            'include_operations' => (bool) ($options['include_operations'] ?? true),
        ];

        return hash('sha256', json_encode($data, JSON_THROW_ON_ERROR));
    }

    private function resolveSnapshot(ManagedEnvironment $tenant): EvidenceSnapshot
    {
        $result = $this->snapshotResolver->resolve(new EvidenceResolutionRequest(
            workspaceId: (int) $tenant->workspace_id,
            tenantId: (int) $tenant->getKey(),
            requiredDimensions: self::REQUIRED_EVIDENCE_DIMENSIONS,
        ));

        if (! $result->isResolved()) {
            throw new ReviewPackEvidenceResolutionException($result);
        }

        return $result->snapshot;
    }

    private function findPackForRun(ManagedEnvironment $tenant, OperationRun $operationRun): ?ReviewPack
    {
        return ReviewPack::query()
            ->where('managed_environment_id', (int) $tenant->getKey())
            ->where('operation_run_id', (int) $operationRun->getKey())
            ->latest('id')
            ->first();
    }

    private function logReviewExport(EnvironmentReview $review, User $user, ReviewPack $reviewPack, string $mode): void
    {
        $tenant = $review->tenant;

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

        $this->auditLogger->log(
            workspace: $tenant->workspace,
            action: AuditActionId::EnvironmentReviewExported,
            context: [
                'metadata' => [
                    'review_id' => (int) $review->getKey(),
                    'review_pack_id' => (int) $reviewPack->getKey(),
                    'mode' => $mode,
                    'status' => (string) $reviewPack->status,
                ],
            ],
            actor: $user,
            resourceType: 'environment_review',
            resourceId: (string) $review->getKey(),
            targetLabel: sprintf('ManagedEnvironment review #%d', (int) $review->getKey()),
            operationRunId: $reviewPack->operation_run_id,
            tenant: $tenant,
        );
    }
}
