<?php

namespace App\Services\Drift;

use App\Models\Finding;
use App\Models\InventorySyncRun;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use App\Services\Drift\Normalizers\ScopeTagsNormalizer;
use App\Services\Drift\Normalizers\SettingsNormalizer;
use Illuminate\Support\Arr;
use RuntimeException;

class DriftFindingGenerator
{
    public function __construct(
        private readonly DriftHasher $hasher,
        private readonly DriftEvidence $evidence,
        private readonly SettingsNormalizer $settingsNormalizer,
        private readonly ScopeTagsNormalizer $scopeTagsNormalizer,
    ) {}

    public function generate(Tenant $tenant, InventorySyncRun $baseline, InventorySyncRun $current, string $scopeKey): int
    {
        if (! $baseline->finished_at || ! $current->finished_at) {
            throw new RuntimeException('Baseline/current run must be finished.');
        }

        /** @var array<string, mixed> $selection */
        $selection = is_array($current->selection_payload) ? $current->selection_payload : [];

        $policyTypes = Arr::get($selection, 'policy_types');
        if (! is_array($policyTypes)) {
            $policyTypes = [];
        }

        $policyTypes = array_values(array_filter(array_map('strval', $policyTypes)));

        $created = 0;

        Policy::query()
            ->where('tenant_id', $tenant->getKey())
            ->whereIn('policy_type', $policyTypes)
            ->orderBy('id')
            ->chunk(200, function ($policies) use ($tenant, $baseline, $current, $scopeKey, &$created): void {
                foreach ($policies as $policy) {
                    if (! $policy instanceof Policy) {
                        continue;
                    }

                    $baselineVersion = $this->versionForRun($policy, $baseline);
                    $currentVersion = $this->versionForRun($policy, $current);

                    if ($baselineVersion instanceof PolicyVersion || $currentVersion instanceof PolicyVersion) {
                        $policyType = (string) ($policy->policy_type ?? '');
                        $platform = is_string($policy->platform ?? null) ? $policy->platform : null;

                        $baselineSnapshot = $baselineVersion instanceof PolicyVersion && is_array($baselineVersion->snapshot)
                            ? $baselineVersion->snapshot
                            : [];
                        $currentSnapshot = $currentVersion instanceof PolicyVersion && is_array($currentVersion->snapshot)
                            ? $currentVersion->snapshot
                            : [];

                        $baselineNormalized = $this->settingsNormalizer->normalizeForDiff($baselineSnapshot, $policyType, $platform);
                        $currentNormalized = $this->settingsNormalizer->normalizeForDiff($currentSnapshot, $policyType, $platform);

                        $baselineSnapshotHash = $this->hasher->hashNormalized($baselineNormalized);
                        $currentSnapshotHash = $this->hasher->hashNormalized($currentNormalized);

                        if ($baselineSnapshotHash !== $currentSnapshotHash) {
                            $changeType = match (true) {
                                $baselineVersion instanceof PolicyVersion && ! $currentVersion instanceof PolicyVersion => 'removed',
                                ! $baselineVersion instanceof PolicyVersion && $currentVersion instanceof PolicyVersion => 'added',
                                default => 'modified',
                            };

                            $fingerprint = $this->hasher->fingerprint(
                                tenantId: (int) $tenant->getKey(),
                                scopeKey: $scopeKey,
                                subjectType: 'policy',
                                subjectExternalId: (string) $policy->external_id,
                                changeType: $changeType,
                                baselineHash: $baselineSnapshotHash,
                                currentHash: $currentSnapshotHash,
                            );

                            $rawEvidence = [
                                'change_type' => $changeType,
                                'summary' => [
                                    'kind' => 'policy_snapshot',
                                    'changed_fields' => ['snapshot_hash'],
                                ],
                                'baseline' => [
                                    'policy_id' => $policy->external_id,
                                    'policy_version_id' => $baselineVersion?->getKey(),
                                    'snapshot_hash' => $baselineSnapshotHash,
                                ],
                                'current' => [
                                    'policy_id' => $policy->external_id,
                                    'policy_version_id' => $currentVersion?->getKey(),
                                    'snapshot_hash' => $currentSnapshotHash,
                                ],
                            ];

                            $finding = Finding::query()->firstOrNew([
                                'tenant_id' => $tenant->getKey(),
                                'fingerprint' => $fingerprint,
                            ]);

                            $wasNew = ! $finding->exists;

                            $finding->forceFill([
                                'finding_type' => Finding::FINDING_TYPE_DRIFT,
                                'scope_key' => $scopeKey,
                                'baseline_run_id' => $baseline->getKey(),
                                'current_run_id' => $current->getKey(),
                                'subject_type' => 'policy',
                                'subject_external_id' => (string) $policy->external_id,
                                'severity' => Finding::SEVERITY_MEDIUM,
                                'evidence_jsonb' => $this->evidence->sanitize($rawEvidence),
                            ]);

                            if ($wasNew) {
                                $finding->forceFill([
                                    'status' => Finding::STATUS_NEW,
                                    'acknowledged_at' => null,
                                    'acknowledged_by_user_id' => null,
                                ]);
                            }

                            $finding->save();

                            if ($wasNew) {
                                $created++;
                            }
                        }
                    }

                    if (! $baselineVersion instanceof PolicyVersion || ! $currentVersion instanceof PolicyVersion) {
                        continue;
                    }

                    $baselineAssignments = is_array($baselineVersion->assignments) ? $baselineVersion->assignments : [];
                    $currentAssignments = is_array($currentVersion->assignments) ? $currentVersion->assignments : [];

                    $baselineAssignmentsHash = $this->hasher->hashNormalized($baselineAssignments);
                    $currentAssignmentsHash = $this->hasher->hashNormalized($currentAssignments);

                    if ($baselineAssignmentsHash !== $currentAssignmentsHash) {
                        $fingerprint = $this->hasher->fingerprint(
                            tenantId: (int) $tenant->getKey(),
                            scopeKey: $scopeKey,
                            subjectType: 'assignment',
                            subjectExternalId: (string) $policy->external_id,
                            changeType: 'modified',
                            baselineHash: (string) ($baselineAssignmentsHash ?? ''),
                            currentHash: (string) ($currentAssignmentsHash ?? ''),
                        );

                        $rawEvidence = [
                            'change_type' => 'modified',
                            'summary' => [
                                'kind' => 'policy_assignments',
                                'changed_fields' => ['assignments_hash'],
                            ],
                            'baseline' => [
                                'policy_id' => $policy->external_id,
                                'policy_version_id' => $baselineVersion->getKey(),
                                'assignments_hash' => $baselineAssignmentsHash,
                            ],
                            'current' => [
                                'policy_id' => $policy->external_id,
                                'policy_version_id' => $currentVersion->getKey(),
                                'assignments_hash' => $currentAssignmentsHash,
                            ],
                        ];

                        $finding = Finding::query()->firstOrNew([
                            'tenant_id' => $tenant->getKey(),
                            'fingerprint' => $fingerprint,
                        ]);

                        $wasNew = ! $finding->exists;

                        $finding->forceFill([
                            'finding_type' => Finding::FINDING_TYPE_DRIFT,
                            'scope_key' => $scopeKey,
                            'baseline_run_id' => $baseline->getKey(),
                            'current_run_id' => $current->getKey(),
                            'subject_type' => 'assignment',
                            'subject_external_id' => (string) $policy->external_id,
                            'severity' => Finding::SEVERITY_MEDIUM,
                            'evidence_jsonb' => $this->evidence->sanitize($rawEvidence),
                        ]);

                        if ($wasNew) {
                            $finding->forceFill([
                                'status' => Finding::STATUS_NEW,
                                'acknowledged_at' => null,
                                'acknowledged_by_user_id' => null,
                            ]);
                        }

                        $finding->save();

                        if ($wasNew) {
                            $created++;
                        }
                    }

                    $baselineScopeTagIds = $this->scopeTagsNormalizer->normalizeIdsForHash($baselineVersion->scope_tags);
                    $currentScopeTagIds = $this->scopeTagsNormalizer->normalizeIdsForHash($currentVersion->scope_tags);

                    if ($baselineScopeTagIds === null || $currentScopeTagIds === null) {
                        continue;
                    }

                    $baselineScopeTagsHash = $this->hasher->hashNormalized($baselineScopeTagIds);
                    $currentScopeTagsHash = $this->hasher->hashNormalized($currentScopeTagIds);

                    if ($baselineScopeTagsHash === $currentScopeTagsHash) {
                        continue;
                    }

                    $fingerprint = $this->hasher->fingerprint(
                        tenantId: (int) $tenant->getKey(),
                        scopeKey: $scopeKey,
                        subjectType: 'scope_tag',
                        subjectExternalId: (string) $policy->external_id,
                        changeType: 'modified',
                        baselineHash: $baselineScopeTagsHash,
                        currentHash: $currentScopeTagsHash,
                    );

                    $rawEvidence = [
                        'change_type' => 'modified',
                        'summary' => [
                            'kind' => 'policy_scope_tags',
                            'changed_fields' => ['scope_tags_hash'],
                        ],
                        'baseline' => [
                            'policy_id' => $policy->external_id,
                            'policy_version_id' => $baselineVersion->getKey(),
                            'scope_tags_hash' => $baselineScopeTagsHash,
                        ],
                        'current' => [
                            'policy_id' => $policy->external_id,
                            'policy_version_id' => $currentVersion->getKey(),
                            'scope_tags_hash' => $currentScopeTagsHash,
                        ],
                    ];

                    $finding = Finding::query()->firstOrNew([
                        'tenant_id' => $tenant->getKey(),
                        'fingerprint' => $fingerprint,
                    ]);

                    $wasNew = ! $finding->exists;

                    $finding->forceFill([
                        'finding_type' => Finding::FINDING_TYPE_DRIFT,
                        'scope_key' => $scopeKey,
                        'baseline_run_id' => $baseline->getKey(),
                        'current_run_id' => $current->getKey(),
                        'subject_type' => 'scope_tag',
                        'subject_external_id' => (string) $policy->external_id,
                        'severity' => Finding::SEVERITY_MEDIUM,
                        'evidence_jsonb' => $this->evidence->sanitize($rawEvidence),
                    ]);

                    if ($wasNew) {
                        $finding->forceFill([
                            'status' => Finding::STATUS_NEW,
                            'acknowledged_at' => null,
                            'acknowledged_by_user_id' => null,
                        ]);
                    }

                    $finding->save();

                    if ($wasNew) {
                        $created++;
                    }
                }
            });

        return $created;
    }

    private function versionForRun(Policy $policy, InventorySyncRun $run): ?PolicyVersion
    {
        if (! $run->finished_at) {
            return null;
        }

        return PolicyVersion::query()
            ->where('tenant_id', $policy->tenant_id)
            ->where('policy_id', $policy->getKey())
            ->where('captured_at', '<=', $run->finished_at)
            ->latest('captured_at')
            ->first();
    }
}
