<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\ManagedEnvironment;
use App\Models\EnvironmentReview;
use App\Services\Intune\SecretClassificationService;
use App\Services\OperationRunService;
use App\Services\ReviewPackService;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\RedactionIntegrity;
use App\Support\ReviewPackStatus;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Throwable;
use ZipArchive;

class GenerateReviewPackJob implements ShouldQueue
{
    use Queueable;

    public int $timeout = 240;

    public bool $failOnTimeout = true;

    public function __construct(
        public int $reviewPackId,
        public int $operationRunId,
    ) {}

    public function handle(OperationRunService $operationRunService): void
    {
        $reviewPack = ReviewPack::query()->with(['tenant', 'evidenceSnapshot.items', 'environmentReview.sections'])->find($this->reviewPackId);
        $operationRun = OperationRun::query()->find($this->operationRunId);

        if (! $reviewPack instanceof ReviewPack || ! $operationRun instanceof OperationRun) {
            Log::warning('GenerateReviewPackJob: missing records', [
                'review_pack_id' => $this->reviewPackId,
                'operation_run_id' => $this->operationRunId,
            ]);

            return;
        }

        $tenant = $reviewPack->tenant;

        if (! $tenant instanceof ManagedEnvironment) {
            $this->markFailed($reviewPack, $operationRun, $operationRunService, 'tenant_not_found', 'ManagedEnvironment not found');

            return;
        }

        $snapshot = $reviewPack->evidenceSnapshot;

        if (! $snapshot instanceof EvidenceSnapshot) {
            $this->markFailed($reviewPack, $operationRun, $operationRunService, 'missing_snapshot', 'Evidence snapshot not found');

            return;
        }

        // Mark running via OperationRunService (auto-sets started_at)
        $operationRunService->updateRun($operationRun, OperationRunStatus::Running->value);
        $reviewPack->update(['status' => ReviewPackStatus::Generating->value]);

        try {
            $this->executeGeneration($reviewPack, $operationRun, $tenant, $snapshot, $operationRunService);
        } catch (Throwable $e) {
            $this->markFailed($reviewPack, $operationRun, $operationRunService, 'generation_error', $e->getMessage());

            throw $e;
        }
    }

    private function executeGeneration(ReviewPack $reviewPack, OperationRun $operationRun, ManagedEnvironment $tenant, EvidenceSnapshot $snapshot, OperationRunService $operationRunService): void
    {
        $review = $reviewPack->environmentReview;

        if ($review instanceof EnvironmentReview) {
            $this->executeReviewDerivedGeneration($reviewPack, $review, $operationRun, $tenant, $snapshot, $operationRunService);

            return;
        }

        $options = $reviewPack->options ?? [];
        $includePii = (bool) ($options['include_pii'] ?? true);
        $includeOperations = (bool) ($options['include_operations'] ?? true);
        $items = $snapshot->items->keyBy('dimension_key');
        $findingsPayload = $this->itemSummaryPayload($items->get('findings_summary'));
        $permissionPosturePayload = $this->itemSummaryPayload($items->get('permission_posture'));
        $entraRolesPayload = $this->itemSummaryPayload($items->get('entra_admin_roles'));
        $operationsPayload = $this->itemSummaryPayload($items->get('operations_summary'));
        $riskAcceptance = is_array($snapshot->summary['risk_acceptance'] ?? null)
            ? $snapshot->summary['risk_acceptance']
            : (is_array($findingsPayload['risk_acceptance'] ?? null) ? $findingsPayload['risk_acceptance'] : []);

        $findings = collect(is_array($findingsPayload['entries'] ?? null) ? $findingsPayload['entries'] : []);
        $recentOperations = collect($includeOperations && is_array($operationsPayload['entries'] ?? null) ? $operationsPayload['entries'] : []);
        $hardening = is_array($snapshot->summary['hardening'] ?? null) ? $snapshot->summary['hardening'] : [];
        $dataFreshness = $this->computeDataFreshness($items);

        // 6. Build file map
        $fileMap = $this->buildFileMap(
            findings: $findings,
            hardening: $hardening,
            permissionPosture: is_array($permissionPosturePayload['payload'] ?? null) ? $permissionPosturePayload['payload'] : [],
            entraAdminRoles: ['roles' => is_array($entraRolesPayload['roles'] ?? null) ? $entraRolesPayload['roles'] : []],
            recentOperations: $recentOperations,
            tenant: $tenant,
            snapshot: $snapshot,
            dataFreshness: $dataFreshness,
            riskAcceptance: $riskAcceptance,
            includePii: $includePii,
            includeOperations: $includeOperations,
        );
        $fileCount = count($fileMap);

        $operationRun = $operationRunService->updateRun(
            $operationRun,
            status: OperationRunStatus::Running->value,
            outcome: OperationRunOutcome::Pending->value,
            summaryCounts: [
                'total' => $fileCount,
                'processed' => 0,
                'succeeded' => 0,
                'failed' => 0,
                'skipped' => 0,
            ],
        );

        // 7. Assemble ZIP
        $tempFile = tempnam(sys_get_temp_dir(), 'review-pack-');

        try {
            $this->assembleZip($tempFile, $fileMap, function () use ($operationRunService, &$operationRun): void {
                $operationRun = $operationRunService->incrementSummaryCounts($operationRun, [
                    'processed' => 1,
                    'created' => 1,
                ]);
            });

            // 8. Compute SHA-256
            $sha256 = hash_file('sha256', $tempFile);
            $fileSize = filesize($tempFile);

            // 9. Store on exports disk
            $filePath = sprintf(
                'review-packs/%s/%s.zip',
                $tenant->external_id,
                now()->format('Y-m-d-His'),
            );

            Storage::disk('exports')->put($filePath, file_get_contents($tempFile));
        } finally {
            if (file_exists($tempFile)) {
                unlink($tempFile);
            }
        }

        // 10. Compute fingerprint
        $fingerprint = app(ReviewPackService::class)->computeFingerprint($tenant, $options);

        // 11. Compute summary
        $summary = [
            'finding_count' => (int) ($snapshot->summary['finding_count'] ?? $findings->count()),
            'report_count' => (int) ($snapshot->summary['report_count'] ?? 0),
            'operation_count' => $recentOperations->count(),
            'data_freshness' => $dataFreshness,
            'risk_acceptance' => $riskAcceptance,
            'evidence_resolution' => [
                'outcome' => 'resolved',
                'snapshot_id' => (int) $snapshot->getKey(),
                'snapshot_fingerprint' => (string) $snapshot->fingerprint,
                'completeness_state' => (string) $snapshot->completeness_state,
            ],
        ];

        // 12. Update ReviewPack
        $retentionDays = (int) config('tenantpilot.review_pack.retention_days', 90);
        $reviewPack->update([
            'status' => ReviewPackStatus::Ready->value,
            'evidence_snapshot_id' => (int) $snapshot->getKey(),
            'fingerprint' => $fingerprint,
            'sha256' => $sha256,
            'file_size' => $fileSize,
            'file_path' => $filePath,
            'file_disk' => 'exports',
            'generated_at' => now(),
            'expires_at' => now()->addDays($retentionDays),
            'summary' => $summary,
        ]);

        // 13. Mark OperationRun completed (auto-sends OperationRunCompleted notification)
        $operationRunService->updateRun(
            $operationRun,
            status: OperationRunStatus::Completed->value,
            outcome: OperationRunOutcome::Succeeded->value,
            summaryCounts: [
                'total' => $fileCount,
                'processed' => $fileCount,
                'created' => $fileCount,
                'finding_count' => (int) ($summary['finding_count'] ?? 0),
                'report_count' => (int) ($summary['report_count'] ?? 0),
                'operation_count' => (int) ($summary['operation_count'] ?? 0),
            ],
        );
    }

    private function executeReviewDerivedGeneration(
        ReviewPack $reviewPack,
        EnvironmentReview $review,
        OperationRun $operationRun,
        ManagedEnvironment $tenant,
        EvidenceSnapshot $snapshot,
        OperationRunService $operationRunService,
    ): void {
        $options = $reviewPack->options ?? [];
        $includePii = (bool) ($options['include_pii'] ?? true);
        $includeOperations = (bool) ($options['include_operations'] ?? true);
        $generatedAt = now();

        $fileMap = $this->buildReviewDerivedFileMap(
            reviewPack: $reviewPack,
            review: $review,
            tenant: $tenant,
            snapshot: $snapshot,
            includePii: $includePii,
            includeOperations: $includeOperations,
            generatedAt: $generatedAt,
        );
        $fileCount = count($fileMap);

        $operationRun = $operationRunService->updateRun(
            $operationRun,
            status: OperationRunStatus::Running->value,
            outcome: OperationRunOutcome::Pending->value,
            summaryCounts: [
                'total' => $fileCount,
                'processed' => 0,
                'succeeded' => 0,
                'failed' => 0,
                'skipped' => 0,
            ],
        );

        $tempFile = tempnam(sys_get_temp_dir(), 'review-pack-');

        try {
            $this->assembleZip($tempFile, $fileMap, function () use ($operationRunService, &$operationRun): void {
                $operationRun = $operationRunService->incrementSummaryCounts($operationRun, [
                    'processed' => 1,
                    'created' => 1,
                ]);
            });

            $sha256 = hash_file('sha256', $tempFile);
            $fileSize = filesize($tempFile);
            $filePath = sprintf(
                'review-packs/%s/review-%d-%s.zip',
                $tenant->external_id,
                (int) $review->getKey(),
                $generatedAt->format('Y-m-d-His'),
            );

            Storage::disk('exports')->put($filePath, file_get_contents($tempFile));
        } finally {
            if (file_exists($tempFile)) {
                unlink($tempFile);
            }
        }

        $fingerprint = app(ReviewPackService::class)->computeFingerprintForReview($review, $options);
        $reviewSummary = is_array($review->summary) ? $review->summary : [];
        $governancePackage = is_array($reviewSummary['governance_package'] ?? null)
            ? $this->redactReportPayload($reviewSummary['governance_package'], $includePii)
            : [];
        $summary = [
            'environment_review_id' => (int) $review->getKey(),
            'review_status' => (string) $review->status,
            'review_completeness_state' => (string) $review->completeness_state,
            'section_count' => $review->sections->count(),
            'finding_count' => (int) ($reviewSummary['finding_count'] ?? 0),
            'report_count' => (int) ($reviewSummary['report_count'] ?? 0),
            'operation_count' => $includeOperations ? (int) ($reviewSummary['operation_count'] ?? 0) : 0,
            'highlights' => is_array($reviewSummary['highlights'] ?? null) ? $reviewSummary['highlights'] : [],
            'publish_blockers' => is_array($reviewSummary['publish_blockers'] ?? null) ? $reviewSummary['publish_blockers'] : [],
            'governance_package' => $governancePackage,
            'delivery_bundle' => $this->deliveryBundleSummary($review),
            'evidence_resolution' => [
                'outcome' => 'resolved',
                'snapshot_id' => (int) $snapshot->getKey(),
                'snapshot_fingerprint' => (string) $snapshot->fingerprint,
                'completeness_state' => (string) $snapshot->completeness_state,
            ],
        ];

        $retentionDays = (int) config('tenantpilot.review_pack.retention_days', 90);
        $reviewPack->update([
            'status' => ReviewPackStatus::Ready->value,
            'evidence_snapshot_id' => (int) $snapshot->getKey(),
            'fingerprint' => $fingerprint,
            'sha256' => $sha256,
            'file_size' => $fileSize,
            'file_path' => $filePath,
            'file_disk' => 'exports',
            'generated_at' => $generatedAt,
            'expires_at' => $generatedAt->copy()->addDays($retentionDays),
            'summary' => $summary,
        ]);

        $review->update([
            'current_export_review_pack_id' => (int) $reviewPack->getKey(),
            'summary' => array_merge($reviewSummary, [
                'has_ready_export' => true,
                'current_export_review_pack_id' => (int) $reviewPack->getKey(),
            ]),
        ]);

        $operationRunService->updateRun(
            $operationRun,
            status: OperationRunStatus::Completed->value,
            outcome: OperationRunOutcome::Succeeded->value,
            summaryCounts: [
                'total' => $fileCount,
                'processed' => $fileCount,
                'created' => 1,
                'finding_count' => (int) ($summary['finding_count'] ?? 0),
                'report_count' => (int) ($summary['report_count'] ?? 0),
                'operation_count' => (int) ($summary['operation_count'] ?? 0),
                'errors_recorded' => 0,
            ],
        );
    }

    /**
     * @return array<string, ?string>
     */
    private function computeDataFreshness($items): array
    {
        return [
            'permission_posture' => $items->get('permission_posture')?->freshness_at?->toIso8601String(),
            'entra_admin_roles' => $items->get('entra_admin_roles')?->freshness_at?->toIso8601String(),
            'findings' => $items->get('findings_summary')?->freshness_at?->toIso8601String(),
            'hardening' => $items->get('baseline_drift_posture')?->freshness_at?->toIso8601String(),
        ];
    }

    /**
     * Build the file map for the ZIP contents.
     *
     * @return array<string, string>
     */
    private function buildFileMap(
        $findings,
        array $hardening,
        array $permissionPosture,
        array $entraAdminRoles,
        $recentOperations,
        ManagedEnvironment $tenant,
        EvidenceSnapshot $snapshot,
        array $dataFreshness,
        array $riskAcceptance,
        bool $includePii,
        bool $includeOperations,
    ): array {
        $files = [];

        // findings.csv
        $files['findings.csv'] = $this->buildFindingsCsv($findings, $includePii);

        // hardening.json
        $files['hardening.json'] = json_encode($hardening, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);

        // metadata.json
        $files['metadata.json'] = json_encode([
            'version' => '1.0',
            'managed_environment_id' => $tenant->external_id,
            'tenant_name' => $includePii ? $tenant->name : '[REDACTED]',
            'generated_at' => now()->toIso8601String(),
            'evidence_snapshot' => [
                'id' => (int) $snapshot->getKey(),
                'fingerprint' => (string) $snapshot->fingerprint,
                'completeness_state' => (string) $snapshot->completeness_state,
                'generated_at' => $snapshot->generated_at?->toIso8601String(),
            ],
            'redaction_integrity' => [
                'protected_values_hidden' => true,
                'note' => RedactionIntegrity::protectedValueNote(),
            ],
            'options' => [
                'include_pii' => $includePii,
                'include_operations' => $includeOperations,
            ],
        ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);

        // operations.csv
        $files['operations.csv'] = $this->buildOperationsCsv($recentOperations, $includePii);

        // reports/entra_admin_roles.json
        $files['reports/entra_admin_roles.json'] = json_encode(
            $this->redactReportPayload($entraAdminRoles, $includePii),
            JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
        );

        // reports/permission_posture.json
        $files['reports/permission_posture.json'] = json_encode(
            $this->redactReportPayload($permissionPosture, $includePii),
            JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
        );

        // summary.json
        $files['summary.json'] = json_encode([
            'data_freshness' => $dataFreshness,
            'finding_count' => $findings->count(),
            'report_count' => count(array_filter([$permissionPosture, $entraAdminRoles], static fn (array $payload): bool => $payload !== [])),
            'operation_count' => $recentOperations->count(),
            'risk_acceptance' => $riskAcceptance,
            'snapshot_id' => (int) $snapshot->getKey(),
        ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);

        return $files;
    }

    /**
     * Build findings CSV content.
     *
     * @param  \Illuminate\Database\Eloquent\Collection<int, Finding>  $findings
     */
    private function buildFindingsCsv($findings, bool $includePii): string
    {
        $handle = fopen('php://temp', 'r+');
        $this->writeCsvRow($handle, ['id', 'finding_type', 'severity', 'status', 'title', 'description', 'created_at', 'updated_at']);

        foreach ($findings as $finding) {
            $row = $finding instanceof Finding
                ? [
                    $finding->id,
                    $finding->finding_type,
                    $finding->severity,
                    $finding->status,
                    $includePii ? ($finding->title ?? '') : '[REDACTED]',
                    $includePii ? ($finding->description ?? '') : '[REDACTED]',
                    $finding->created_at?->toIso8601String(),
                    $finding->updated_at?->toIso8601String(),
                ]
                : [
                    $finding['id'] ?? '',
                    $finding['finding_type'] ?? '',
                    $finding['severity'] ?? '',
                    $finding['status'] ?? '',
                    $includePii ? ($finding['title'] ?? '') : '[REDACTED]',
                    $includePii ? ($finding['description'] ?? '') : '[REDACTED]',
                    $finding['created_at'] ?? '',
                    $finding['updated_at'] ?? '',
                ];

            $this->writeCsvRow($handle, [
                ...$row,
            ]);
        }

        rewind($handle);
        $content = stream_get_contents($handle);
        fclose($handle);

        return $content;
    }

    /**
     * Build operations CSV content.
     */
    private function buildOperationsCsv($operations, bool $includePii): string
    {
        $handle = fopen('php://temp', 'r+');
        $this->writeCsvRow($handle, ['id', 'type', 'status', 'outcome', 'initiator', 'started_at', 'completed_at']);

        foreach ($operations as $operation) {
            $row = $operation instanceof OperationRun
                ? [
                    $operation->id,
                    $operation->type,
                    $operation->status,
                    $operation->outcome,
                    $includePii ? ($operation->user?->name ?? '') : '[REDACTED]',
                    $operation->started_at?->toIso8601String(),
                    $operation->completed_at?->toIso8601String(),
                ]
                : [
                    $operation['id'] ?? '',
                    $operation['type'] ?? '',
                    $operation['status'] ?? '',
                    $operation['outcome'] ?? '',
                    $includePii ? ($operation['initiator_name'] ?? '') : '[REDACTED]',
                    $operation['started_at'] ?? '',
                    $operation['completed_at'] ?? '',
                ];

            $this->writeCsvRow($handle, [
                ...$row,
            ]);
        }

        rewind($handle);
        $content = stream_get_contents($handle);
        fclose($handle);

        return $content;
    }

    /**
     * @param  resource  $handle
     * @param  array<int, mixed>  $row
     */
    private function writeCsvRow($handle, array $row): void
    {
        fputcsv($handle, $row, ',', '"', '\\');
    }

    /**
     * Redact PII from a report payload.
     *
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    private function redactReportPayload(array $payload, bool $includePii): array
    {
        $payload = $this->redactProtectedPayload($payload);

        return $includePii ? $payload : $this->redactArrayPii($payload);
    }

    /**
     * Recursively redact PII fields from an array.
     *
     * @param  array<string, mixed>  $data
     * @return array<string, mixed>
     */
    private function redactArrayPii(array $data): array
    {
        $piiKeys = [
            'displayName',
            'display_name',
            'userPrincipalName',
            'user_principal_name',
            'email',
            'mail',
            'tenant_name',
            'tenant_label',
            'customer_name',
            'owner_label',
            'owner_name',
            'actor_label',
            'actor_name',
            'initiator_name',
            'requested_by',
            'approved_by',
        ];

        foreach ($data as $key => $value) {
            if (is_string($key) && in_array($key, $piiKeys, true)) {
                $data[$key] = '[REDACTED]';
            } elseif (is_array($value)) {
                $data[$key] = $this->redactArrayPii($value);
            }
        }

        return $data;
    }

    /**
     * @param  array<int|string, mixed>  $data
     * @param  array<int, string>  $segments
     * @return array<int|string, mixed>
     */
    private function redactProtectedPayload(array $data, array $segments = []): array
    {
        foreach ($data as $key => $value) {
            $nextSegments = [...$segments, (string) $key];
            $jsonPointer = $this->jsonPointer($nextSegments);

            if (is_string($key) && $this->classifier()->protectsField('snapshot', $key, $jsonPointer)) {
                $data[$key] = SecretClassificationService::REDACTED;

                continue;
            }

            if (is_array($value)) {
                $data[$key] = $this->redactProtectedPayload($value, $nextSegments);

                continue;
            }

            if (is_string($value)) {
                $data[$key] = $this->classifier()->sanitizeAuditString($value);
            }
        }

        return $data;
    }

    /**
     * @param  array<int, string>  $segments
     */
    private function jsonPointer(array $segments): string
    {
        if ($segments === []) {
            return '/';
        }

        return '/'.implode('/', array_map(
            static fn (string $segment): string => str_replace(['~', '/'], ['~0', '~1'], $segment),
            $segments,
        ));
    }

    private function classifier(): SecretClassificationService
    {
        return app(SecretClassificationService::class);
    }

    /**
     * Assemble a ZIP file from a file map.
     *
     * @param  array<string, string>  $fileMap
     */
    private function assembleZip(string $tempFile, array $fileMap, ?callable $afterWrite = null): void
    {
        $zip = new ZipArchive;
        $result = $zip->open($tempFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);

        if ($result !== true) {
            throw new \RuntimeException("Failed to create ZIP archive: error code {$result}");
        }

        // Add files in alphabetical order for deterministic output
        ksort($fileMap);

        foreach ($fileMap as $filename => $content) {
            $zip->addFromString($filename, $content);

            $afterWrite && $afterWrite();
        }

        $zip->close();
    }

    /**
     * @return array<string, string>
     */
    private function buildReviewDerivedFileMap(
        ReviewPack $reviewPack,
        EnvironmentReview $review,
        ManagedEnvironment $tenant,
        EvidenceSnapshot $snapshot,
        bool $includePii,
        bool $includeOperations,
        \Carbon\CarbonInterface $generatedAt,
    ): array {
        $reviewSummary = is_array($review->summary) ? $review->summary : [];
        $deliveryMetadata = $this->deliveryBundleMetadata(
            reviewPack: $reviewPack,
            review: $review,
            snapshot: $snapshot,
            generatedAt: $generatedAt,
        );

        $sections = $review->sections
            ->filter(fn (mixed $section): bool => $includeOperations || $section->section_key !== 'operations_health')
            ->values();

        $files = [
            'metadata.json' => json_encode([
                'version' => '1.0',
                'managed_environment_id' => $tenant->external_id,
                'tenant_name' => $includePii ? $tenant->name : '[REDACTED]',
                'generated_at' => $generatedAt->toIso8601String(),
                'delivery_bundle' => $deliveryMetadata,
                'environment_review' => [
                    'id' => (int) $review->getKey(),
                    'status' => (string) $review->status,
                    'completeness_state' => (string) $review->completeness_state,
                    'published_at' => $review->published_at?->toIso8601String(),
                    'fingerprint' => (string) $review->fingerprint,
                ],
                'evidence_snapshot' => [
                    'id' => (int) $snapshot->getKey(),
                    'fingerprint' => (string) $snapshot->fingerprint,
                    'completeness_state' => (string) $snapshot->completeness_state,
                    'generated_at' => $snapshot->generated_at?->toIso8601String(),
                ],
                'options' => [
                    'include_pii' => $includePii,
                    'include_operations' => $includeOperations,
                ],
                'redaction_integrity' => [
                    'protected_values_hidden' => true,
                    'note' => RedactionIntegrity::protectedValueNote(),
                ],
            ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
            'summary.json' => json_encode($this->redactReportPayload(array_merge(
                [
                    'environment_review_id' => (int) $review->getKey(),
                    'review_status' => (string) $review->status,
                    'review_completeness_state' => (string) $review->completeness_state,
                ],
                $reviewSummary,
                [
                    'delivery_bundle' => $this->deliveryBundleSummary($review),
                ],
            ), $includePii), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
            'sections.json' => json_encode($sections->map(function ($section) use ($includePii): array {
                $summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
                $renderPayload = is_array($section->render_payload) ? $section->render_payload : [];

                return [
                    'section_key' => (string) $section->section_key,
                    'title' => (string) $section->title,
                    'sort_order' => (int) $section->sort_order,
                    'required' => (bool) $section->required,
                    'completeness_state' => (string) $section->completeness_state,
                    'summary_payload' => $this->redactReportPayload($summaryPayload, $includePii),
                    'render_payload' => $this->redactReportPayload($renderPayload, $includePii),
                ];
            })->all(), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
            ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME => $this->buildExecutiveEntrypoint(
                review: $review,
                tenant: $tenant,
                snapshot: $snapshot,
                reviewSummary: $reviewSummary,
                includePii: $includePii,
                generatedAt: $generatedAt,
            ),
        ];

        foreach ($sections as $section) {
            $renderPayload = is_array($section->render_payload) ? $section->render_payload : [];
            $summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
            $filename = sprintf('sections/%02d-%s.json', (int) $section->sort_order, (string) $section->section_key);

            $files[$filename] = json_encode([
                'title' => (string) $section->title,
                'completeness_state' => (string) $section->completeness_state,
                'summary_payload' => $this->redactReportPayload($summaryPayload, $includePii),
                'render_payload' => $this->redactReportPayload($renderPayload, $includePii),
            ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
        }

        return $files;
    }

    /**
     * @return array<string, mixed>
     */
    private function deliveryBundleSummary(EnvironmentReview $review): array
    {
        return [
            'contract' => ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT,
            'executive_entrypoint_file' => ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME,
            'appendix_files' => ['metadata.json', 'summary.json', 'sections.json'],
            'interpretation_version' => $review->controlInterpretationVersion(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function deliveryBundleMetadata(
        ReviewPack $reviewPack,
        EnvironmentReview $review,
        EvidenceSnapshot $snapshot,
        \Carbon\CarbonInterface $generatedAt,
    ): array {
        return [
            'contract' => ReviewPackService::REVIEW_DERIVED_DELIVERY_CONTRACT,
            'artifact_family' => 'review_pack',
            'review_pack_id' => (int) $reviewPack->getKey(),
            'generated_at' => $generatedAt->toIso8601String(),
            'released_review' => [
                'id' => (int) $review->getKey(),
                'status' => (string) $review->status,
                'completeness_state' => (string) $review->completeness_state,
                'published_at' => $review->published_at?->toIso8601String(),
            ],
            'interpretation_version' => $review->controlInterpretationVersion(),
            'evidence_basis' => [
                'snapshot_id' => (int) $snapshot->getKey(),
                'snapshot_fingerprint' => (string) $snapshot->fingerprint,
                'completeness_state' => (string) $snapshot->completeness_state,
                'generated_at' => $snapshot->generated_at?->toIso8601String(),
            ],
            'entrypoint' => [
                'file' => ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME,
                'role' => 'executive_entrypoint',
                'audience' => 'executive',
                'format' => 'text/markdown',
            ],
            'appendix' => [
                [
                    'file' => 'metadata.json',
                    'role' => 'bundle_metadata',
                    'description' => 'Structured delivery metadata and artifact role map.',
                ],
                [
                    'file' => 'summary.json',
                    'role' => 'review_summary_appendix',
                    'description' => 'Structured released-review summary truth.',
                ],
                [
                    'file' => 'sections.json',
                    'role' => 'section_detail_appendix',
                    'description' => 'Structured released-review section detail.',
                ],
            ],
        ];
    }

    /**
     * @param  array<string, mixed>  $reviewSummary
     */
    private function buildExecutiveEntrypoint(
        EnvironmentReview $review,
        ManagedEnvironment $tenant,
        EvidenceSnapshot $snapshot,
        array $reviewSummary,
        bool $includePii,
        \Carbon\CarbonInterface $generatedAt,
    ): string {
        $package = is_array($reviewSummary['governance_package'] ?? null)
            ? $this->redactReportPayload($reviewSummary['governance_package'], $includePii)
            : [];
        $controlInterpretation = is_array($reviewSummary['control_interpretation'] ?? null)
            ? $reviewSummary['control_interpretation']
            : [];
        $nonCertificationDisclosure = $this->plainText(
            $controlInterpretation['non_certification_disclosure'] ?? null,
            'TenantPilot interprets available evidence for review readiness. This is not a certification, legal attestation, or compliance guarantee.',
        );
        $tenantName = $includePii ? $tenant->name : '[REDACTED]';
        $topFindings = is_array($package['top_findings'] ?? null) ? $package['top_findings'] : [];
        $acceptedRisks = is_array($package['accepted_risks'] ?? null) ? $package['accepted_risks'] : [];
        $decisionSummary = is_array($package['decision_summary'] ?? null) ? $package['decision_summary'] : [];
        $governanceDecisions = is_array($decisionSummary['entries'] ?? null)
            ? $decisionSummary['entries']
            : (is_array($package['governance_decisions'] ?? null) ? $package['governance_decisions'] : []);
        $nextActions = is_array($reviewSummary['recommended_next_actions'] ?? null) ? $reviewSummary['recommended_next_actions'] : [];

        $lines = [
            '# Executive summary',
            '',
            'ManagedEnvironment: '.$this->plainText($tenantName, '[REDACTED]'),
            'Released review: #'.((int) $review->getKey()),
            'Review status: '.$this->plainText($review->status, 'unknown'),
            'Generated at: '.$generatedAt->toIso8601String(),
            '',
            '## Executive story',
            '',
            $this->plainText($package['executive_summary'] ?? null, 'No executive summary is available for this released review.'),
            '',
            '## Evidence basis',
            '',
            $this->plainText(
                $package['evidence_basis_summary'] ?? null,
                sprintf('Anchored to evidence snapshot #%d with %s completeness.', (int) $snapshot->getKey(), (string) $snapshot->completeness_state),
            ),
            '',
            '## Key findings',
            '',
            ...$this->entryBullets($topFindings, 'No key findings are listed for this released review.'),
            '',
            '## Accepted risks',
            '',
            ...$this->entryBullets($acceptedRisks, 'No accepted risks are listed for this released review.'),
            '',
            '## Governance decisions requiring awareness',
            '',
            ...$this->decisionSummaryLines($decisionSummary, $governanceDecisions),
            '',
            '## Next actions',
            '',
            ...$this->textBullets($nextActions, 'No next action is listed for this released review.'),
            '',
            '## Non-certification disclosure',
            '',
            $nonCertificationDisclosure,
            '',
            '## Structured auditor appendix',
            '',
            'This executive entrypoint is the first file to read. The structured auditor appendix remains available in metadata.json, summary.json, and sections.json.',
            '',
        ];

        return implode("\n", $lines);
    }

    /**
     * @param  array<string, mixed>  $decisionSummary
     * @param  array<int, mixed>  $entries
     * @return list<string>
     */
    private function decisionSummaryLines(array $decisionSummary, array $entries): array
    {
        $lines = [];
        $summary = $this->plainText($decisionSummary['summary'] ?? null, '');
        $nextAction = $this->plainText($decisionSummary['next_action'] ?? null, '');

        if ($summary !== '') {
            $lines[] = $summary;
        }

        if ($nextAction !== '') {
            $lines[] = 'Next action: '.$nextAction;
        }

        if ($lines !== []) {
            $lines[] = '';
        }

        return [
            ...$lines,
            ...$this->entryBullets($entries, 'No governance decisions require awareness in this released review.'),
        ];
    }

    /**
     * @param  array<int, mixed>  $entries
     * @return list<string>
     */
    private function entryBullets(array $entries, string $emptyText): array
    {
        if ($entries === []) {
            return ['- '.$emptyText];
        }

        return collect($entries)
            ->filter(static fn (mixed $entry): bool => is_array($entry))
            ->map(function (array $entry): string {
                $title = $this->plainText($entry['title'] ?? null, 'Entry');
                $summary = $this->plainText($entry['summary'] ?? null, '');

                return $summary === '' ? '- '.$title : '- '.$title.' - '.$summary;
            })
            ->values()
            ->all();
    }

    /**
     * @param  array<int, mixed>  $entries
     * @return list<string>
     */
    private function textBullets(array $entries, string $emptyText): array
    {
        $bullets = collect($entries)
            ->filter(static fn (mixed $entry): bool => is_string($entry) && trim($entry) !== '')
            ->map(fn (string $entry): string => '- '.$this->plainText($entry, ''))
            ->values()
            ->all();

        return $bullets === [] ? ['- '.$emptyText] : $bullets;
    }

    private function plainText(mixed $value, string $fallback): string
    {
        if (! is_scalar($value) && $value !== null) {
            return $fallback;
        }

        $text = preg_replace('/\s+/', ' ', trim((string) $value));

        return is_string($text) && $text !== '' ? $text : $fallback;
    }

    private function markFailed(ReviewPack $reviewPack, OperationRun $operationRun, OperationRunService $operationRunService, string $reasonCode, string $errorMessage): void
    {
        $reviewPack->update([
            'status' => ReviewPackStatus::Failed->value,
            'summary' => array_merge($reviewPack->summary ?? [], [
                'evidence_resolution' => array_merge($reviewPack->summary['evidence_resolution'] ?? [], [
                    'outcome' => $reasonCode,
                    'reasons' => [mb_substr($errorMessage, 0, 500)],
                ]),
            ]),
        ]);

        $operationRunService->updateRun(
            $operationRun,
            status: OperationRunStatus::Completed->value,
            outcome: OperationRunOutcome::Failed->value,
            failures: [
                ['code' => $reasonCode, 'message' => mb_substr($errorMessage, 0, 500)],
            ],
        );
    }

    private function itemSummaryPayload(mixed $item): array
    {
        if (! $item instanceof \App\Models\EvidenceSnapshotItem || ! is_array($item->summary_payload)) {
            return [];
        }

        return $item->summary_payload;
    }
}
