<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\ReviewPack;
use App\Models\StoredReport;
use App\Models\Tenant;
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 function __construct(
        public int $reviewPackId,
        public int $operationRunId,
    ) {}

    public function handle(OperationRunService $operationRunService): void
    {
        $reviewPack = ReviewPack::query()->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 Tenant) {
            $this->markFailed($reviewPack, $operationRun, $operationRunService, 'tenant_not_found', 'Tenant 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, $operationRunService);
        } catch (Throwable $e) {
            $this->markFailed($reviewPack, $operationRun, $operationRunService, 'generation_error', $e->getMessage());

            throw $e;
        }
    }

    private function executeGeneration(ReviewPack $reviewPack, OperationRun $operationRun, Tenant $tenant, OperationRunService $operationRunService): void
    {
        $options = $reviewPack->options ?? [];
        $includePii = (bool) ($options['include_pii'] ?? true);
        $includeOperations = (bool) ($options['include_operations'] ?? true);
        $tenantId = (int) $tenant->getKey();

        // 1. Collect StoredReports
        $storedReports = StoredReport::query()
            ->where('tenant_id', $tenantId)
            ->whereIn('report_type', [
                StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
                StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES,
            ])
            ->get()
            ->keyBy('report_type');

        // 2. Collect open findings
        $findings = Finding::query()
            ->where('tenant_id', $tenantId)
            ->whereIn('status', Finding::openStatusesForQuery())
            ->orderBy('severity')
            ->orderBy('created_at')
            ->get();

        // 3. Collect tenant hardening fields
        $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,
        ];

        // 4. Collect recent OperationRuns (30 days)
        $recentOperations = $includeOperations
            ? OperationRun::query()
                ->where('tenant_id', $tenantId)
                ->where('created_at', '>=', now()->subDays(30))
                ->orderByDesc('created_at')
                ->get()
            : collect();

        // 5. Data freshness
        $dataFreshness = $this->computeDataFreshness($storedReports, $findings, $tenant);

        // 6. Build file map
        $fileMap = $this->buildFileMap(
            storedReports: $storedReports,
            findings: $findings,
            hardening: $hardening,
            recentOperations: $recentOperations,
            tenant: $tenant,
            dataFreshness: $dataFreshness,
            includePii: $includePii,
            includeOperations: $includeOperations,
        );

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

        try {
            $this->assembleZip($tempFile, $fileMap);

            // 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' => $findings->count(),
            'report_count' => $storedReports->count(),
            'operation_count' => $recentOperations->count(),
            'data_freshness' => $dataFreshness,
        ];

        // 12. Update ReviewPack
        $retentionDays = (int) config('tenantpilot.review_pack.retention_days', 90);
        $reviewPack->update([
            'status' => ReviewPackStatus::Ready->value,
            '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: $summary,
        );
    }

    /**
     * @param  \Illuminate\Support\Collection<string, StoredReport>  $storedReports
     * @param  \Illuminate\Database\Eloquent\Collection<int, Finding>  $findings
     * @return array<string, ?string>
     */
    private function computeDataFreshness($storedReports, $findings, Tenant $tenant): array
    {
        return [
            'permission_posture' => $storedReports->get(StoredReport::REPORT_TYPE_PERMISSION_POSTURE)?->updated_at?->toIso8601String(),
            'entra_admin_roles' => $storedReports->get(StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES)?->updated_at?->toIso8601String(),
            'findings' => $findings->max('updated_at')?->toIso8601String() ?? $findings->max('created_at')?->toIso8601String(),
            'hardening' => $tenant->rbac_last_checked_at?->toIso8601String(),
        ];
    }

    /**
     * Build the file map for the ZIP contents.
     *
     * @return array<string, string>
     */
    private function buildFileMap(
        $storedReports,
        $findings,
        array $hardening,
        $recentOperations,
        Tenant $tenant,
        array $dataFreshness,
        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',
            'tenant_id' => $tenant->external_id,
            'tenant_name' => $includePii ? $tenant->name : '[REDACTED]',
            'generated_at' => now()->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
        $entraReport = $storedReports->get(StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES);
        $files['reports/entra_admin_roles.json'] = json_encode(
            $entraReport ? $this->redactReportPayload($entraReport->payload ?? [], $includePii) : [],
            JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
        );

        // reports/permission_posture.json
        $postureReport = $storedReports->get(StoredReport::REPORT_TYPE_PERMISSION_POSTURE);
        $files['reports/permission_posture.json'] = json_encode(
            $postureReport ? $this->redactReportPayload($postureReport->payload ?? [], $includePii) : [],
            JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
        );

        // summary.json
        $files['summary.json'] = json_encode([
            'data_freshness' => $dataFreshness,
            'finding_count' => $findings->count(),
            'report_count' => $storedReports->count(),
            'operation_count' => $recentOperations->count(),
        ], 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+');
        fputcsv($handle, ['id', 'finding_type', 'severity', 'status', 'title', 'description', 'created_at', 'updated_at']);

        foreach ($findings as $finding) {
            fputcsv($handle, [
                $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(),
            ]);
        }

        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+');
        fputcsv($handle, ['id', 'type', 'status', 'outcome', 'initiator', 'started_at', 'completed_at']);

        foreach ($operations as $operation) {
            fputcsv($handle, [
                $operation->id,
                $operation->type,
                $operation->status,
                $operation->outcome,
                $includePii ? ($operation->user?->name ?? '') : '[REDACTED]',
                $operation->started_at?->toIso8601String(),
                $operation->completed_at?->toIso8601String(),
            ]);
        }

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

        return $content;
    }

    /**
     * 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'];

        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): 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);
        }

        $zip->close();
    }

    private function markFailed(ReviewPack $reviewPack, OperationRun $operationRun, OperationRunService $operationRunService, string $reasonCode, string $errorMessage): void
    {
        $reviewPack->update(['status' => ReviewPackStatus::Failed->value]);

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