<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Services\Findings\FindingSlaPolicy;
use App\Services\OperationRunService;
use App\Services\Runbooks\FindingsLifecycleBackfillRunbookService;
use App\Support\OpsUx\RunFailureSanitizer;
use Carbon\CarbonImmutable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Throwable;

class BackfillFindingLifecycleTenantIntoWorkspaceRunJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public readonly int $operationRunId,
        public readonly int $workspaceId,
        public readonly int $tenantId,
    ) {}

    public function handle(
        OperationRunService $operationRunService,
        FindingSlaPolicy $slaPolicy,
        FindingsLifecycleBackfillRunbookService $runbookService,
    ): void {
        $tenant = Tenant::query()->find($this->tenantId);

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

        if ((int) $tenant->workspace_id !== $this->workspaceId) {
            return;
        }

        $run = OperationRun::query()->find($this->operationRunId);

        if (! $run instanceof OperationRun) {
            return;
        }

        if ((int) $run->workspace_id !== $this->workspaceId) {
            return;
        }

        if ($run->tenant_id !== null) {
            return;
        }

        if ($run->status === 'queued') {
            $operationRunService->updateRun($run, status: 'running');
        }

        $lock = Cache::lock(sprintf('tenantpilot:findings:lifecycle_backfill:tenant:%d', $this->tenantId), 900);

        if (! $lock->get()) {
            $operationRunService->appendFailures($run, [
                [
                    'code' => 'findings.lifecycle.backfill.lock_busy',
                    'message' => sprintf('Tenant %d is already running a findings lifecycle backfill.', $this->tenantId),
                ],
            ]);

            $operationRunService->incrementSummaryCounts($run, [
                'failed' => 1,
                'processed' => 1,
            ]);

            $operationRunService->maybeCompleteBulkRun($run);
            $runbookService->maybeFinalize($run);

            return;
        }

        try {
            $backfillStartedAt = $run->started_at !== null
                ? CarbonImmutable::instance($run->started_at)
                : CarbonImmutable::now('UTC');

            Finding::query()
                ->where('tenant_id', $tenant->getKey())
                ->orderBy('id')
                ->chunkById(200, function (Collection $findings) use ($tenant, $slaPolicy, $operationRunService, $run, $backfillStartedAt): void {
                    $updated = 0;
                    $skipped = 0;

                    foreach ($findings as $finding) {
                        if (! $finding instanceof Finding) {
                            continue;
                        }

                        $originalAttributes = $finding->getAttributes();

                        $this->backfillLifecycleFields($finding, $backfillStartedAt);
                        $this->backfillLegacyAcknowledgedStatus($finding);
                        $this->backfillSlaFields($finding, $tenant, $slaPolicy, $backfillStartedAt);
                        $this->backfillDriftRecurrenceKey($finding);

                        if ($finding->isDirty()) {
                            $finding->save();
                            $updated++;
                        } else {
                            $finding->setRawAttributes($originalAttributes, sync: true);
                            $skipped++;
                        }
                    }

                    if ($updated > 0 || $skipped > 0) {
                        $operationRunService->incrementSummaryCounts($run, [
                            'updated' => $updated,
                            'skipped' => $skipped,
                        ]);
                    }
                });

            $consolidatedDuplicates = $this->consolidateDriftDuplicates($tenant, $backfillStartedAt);

            if ($consolidatedDuplicates > 0) {
                $operationRunService->incrementSummaryCounts($run, [
                    'updated' => $consolidatedDuplicates,
                ]);
            }

            $operationRunService->incrementSummaryCounts($run, [
                'processed' => 1,
            ]);

            $operationRunService->maybeCompleteBulkRun($run);
            $runbookService->maybeFinalize($run);
        } catch (Throwable $e) {
            $message = RunFailureSanitizer::sanitizeMessage($e->getMessage());
            $reasonCode = RunFailureSanitizer::normalizeReasonCode($e->getMessage());

            $operationRunService->appendFailures($run, [[
                'code' => 'findings.lifecycle.backfill.failed',
                'reason_code' => $reasonCode,
                'message' => $message !== '' ? $message : sprintf('Tenant %d findings lifecycle backfill failed.', $this->tenantId),
            ]]);

            $operationRunService->incrementSummaryCounts($run, [
                'failed' => 1,
                'processed' => 1,
            ]);

            $operationRunService->maybeCompleteBulkRun($run);
            $runbookService->maybeFinalize($run);

            throw $e;
        } finally {
            $lock->release();
        }
    }

    private function backfillLifecycleFields(Finding $finding, CarbonImmutable $backfillStartedAt): void
    {
        $createdAt = $finding->created_at !== null ? CarbonImmutable::instance($finding->created_at) : $backfillStartedAt;

        if ($finding->first_seen_at === null) {
            $finding->first_seen_at = $createdAt;
        }

        if ($finding->last_seen_at === null) {
            $finding->last_seen_at = $createdAt;
        }

        if ($finding->last_seen_at !== null && $finding->first_seen_at !== null) {
            $lastSeen = CarbonImmutable::instance($finding->last_seen_at);
            $firstSeen = CarbonImmutable::instance($finding->first_seen_at);

            if ($lastSeen->lessThan($firstSeen)) {
                $finding->last_seen_at = $firstSeen;
            }
        }

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

        if ($timesSeen < 1) {
            $finding->times_seen = 1;
        }
    }

    private function backfillLegacyAcknowledgedStatus(Finding $finding): void
    {
        if ($finding->status !== Finding::STATUS_ACKNOWLEDGED) {
            return;
        }

        $finding->status = Finding::STATUS_TRIAGED;

        if ($finding->triaged_at === null) {
            if ($finding->acknowledged_at !== null) {
                $finding->triaged_at = CarbonImmutable::instance($finding->acknowledged_at);
            } elseif ($finding->created_at !== null) {
                $finding->triaged_at = CarbonImmutable::instance($finding->created_at);
            }
        }
    }

    private function backfillSlaFields(
        Finding $finding,
        Tenant $tenant,
        FindingSlaPolicy $slaPolicy,
        CarbonImmutable $backfillStartedAt,
    ): void {
        if (! Finding::isOpenStatus((string) $finding->status)) {
            return;
        }

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

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

    private function backfillDriftRecurrenceKey(Finding $finding): void
    {
        if ($finding->finding_type !== Finding::FINDING_TYPE_DRIFT) {
            return;
        }

        if ($finding->recurrence_key !== null && trim((string) $finding->recurrence_key) !== '') {
            return;
        }

        $tenantId = (int) ($finding->tenant_id ?? 0);
        $scopeKey = (string) ($finding->scope_key ?? '');
        $subjectType = (string) ($finding->subject_type ?? '');
        $subjectExternalId = (string) ($finding->subject_external_id ?? '');

        if ($tenantId <= 0 || $scopeKey === '' || $subjectType === '' || $subjectExternalId === '') {
            return;
        }

        $evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
        $kind = Arr::get($evidence, 'summary.kind');
        $changeType = Arr::get($evidence, 'change_type');

        $kind = is_string($kind) ? $kind : '';
        $changeType = is_string($changeType) ? $changeType : '';

        if ($kind === '') {
            return;
        }

        $dimension = $this->recurrenceDimension($kind, $changeType);

        $finding->recurrence_key = hash('sha256', sprintf(
            'drift:%d:%s:%s:%s:%s',
            $tenantId,
            $scopeKey,
            $subjectType,
            $subjectExternalId,
            $dimension,
        ));
    }

    private function recurrenceDimension(string $kind, string $changeType): string
    {
        $kind = strtolower(trim($kind));
        $changeType = strtolower(trim($changeType));

        return match ($kind) {
            'policy_snapshot', 'baseline_compare' => sprintf('%s:%s', $kind, $changeType !== '' ? $changeType : 'modified'),
            default => $kind,
        };
    }

    private function consolidateDriftDuplicates(Tenant $tenant, CarbonImmutable $backfillStartedAt): int
    {
        $duplicateKeys = Finding::query()
            ->where('tenant_id', $tenant->getKey())
            ->where('finding_type', Finding::FINDING_TYPE_DRIFT)
            ->whereNotNull('recurrence_key')
            ->select(['recurrence_key'])
            ->groupBy('recurrence_key')
            ->havingRaw('COUNT(*) > 1')
            ->pluck('recurrence_key')
            ->filter(static fn (mixed $value): bool => is_string($value) && trim($value) !== '')
            ->values();

        if ($duplicateKeys->isEmpty()) {
            return 0;
        }

        $consolidated = 0;

        foreach ($duplicateKeys as $recurrenceKey) {
            if (! is_string($recurrenceKey) || $recurrenceKey === '') {
                continue;
            }

            $findings = Finding::query()
                ->where('tenant_id', $tenant->getKey())
                ->where('finding_type', Finding::FINDING_TYPE_DRIFT)
                ->where('recurrence_key', $recurrenceKey)
                ->orderBy('id')
                ->get();

            $canonical = $this->chooseCanonicalDriftFinding($findings, $recurrenceKey);

            foreach ($findings as $finding) {
                if (! $finding instanceof Finding) {
                    continue;
                }

                if ($canonical instanceof Finding && (int) $finding->getKey() === (int) $canonical->getKey()) {
                    continue;
                }

                $finding->forceFill([
                    'status' => Finding::STATUS_RESOLVED,
                    'resolved_at' => $backfillStartedAt,
                    'resolved_reason' => 'consolidated_duplicate',
                    'recurrence_key' => null,
                ])->save();

                $consolidated++;
            }
        }

        return $consolidated;
    }

    /**
     * @param  Collection<int, Finding>  $findings
     */
    private function chooseCanonicalDriftFinding(Collection $findings, string $recurrenceKey): ?Finding
    {
        if ($findings->isEmpty()) {
            return null;
        }

        $openCandidates = $findings->filter(static fn (Finding $finding): bool => Finding::isOpenStatus((string) $finding->status));

        $candidates = $openCandidates->isNotEmpty() ? $openCandidates : $findings;

        $alreadyCanonical = $candidates->first(static fn (Finding $finding): bool => (string) $finding->fingerprint === $recurrenceKey);

        if ($alreadyCanonical instanceof Finding) {
            return $alreadyCanonical;
        }

        /** @var Finding $sorted */
        $sorted = $candidates
            ->sortByDesc(function (Finding $finding): array {
                $lastSeen = $finding->last_seen_at !== null ? CarbonImmutable::instance($finding->last_seen_at)->getTimestamp() : 0;
                $createdAt = $finding->created_at !== null ? CarbonImmutable::instance($finding->created_at)->getTimestamp() : 0;

                return [
                    max($lastSeen, $createdAt),
                    (int) $finding->getKey(),
                ];
            })
            ->first();

        return $sorted;
    }
}
