<?php

declare(strict_types=1);

namespace App\Support\Artifacts;

use App\Models\EvidenceSnapshotItem;
use App\Models\Finding;
use App\Models\InventoryItem;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\StoredReport;
use App\Support\Governance\Controls\CanonicalControlResolutionRequest;
use App\Support\Governance\Controls\CanonicalControlResolver;
use App\Support\Inventory\InventoryPolicyTypeMeta;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;

final readonly class ArtifactSourceResolver
{
    public function __construct(
        private CanonicalControlResolver $canonicalControlResolver,
    ) {}

    public function forFinding(Finding $finding): ArtifactSourceDescriptor
    {
        $controlResolution = $this->canonicalControlResolutionForFinding($finding);
        $evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
        $targetIdentifier = $this->firstString([
            $finding->subject_external_id,
            Arr::get($evidence, 'policy_id'),
            Arr::get($evidence, 'policy_external_id'),
            Arr::get($evidence, 'external_id'),
            Arr::get($evidence, 'id'),
        ]);

        return new ArtifactSourceDescriptor(
            workspaceId: (int) $finding->workspace_id,
            tenantId: (int) $finding->managed_environment_id,
            managedEnvironmentId: (int) $finding->managed_environment_id,
            sourceFamily: ArtifactSourceTaxonomy::SOURCE_FAMILY_FINDING,
            sourceKind: ArtifactSourceTaxonomy::SOURCE_KIND_MODEL_SUMMARY,
            providerKey: $this->providerKey($evidence),
            providerConnectionId: $this->providerConnectionId($finding->tenant, $evidence),
            sourceTargetKind: $targetIdentifier !== null
                ? ArtifactSourceTaxonomy::SOURCE_TARGET_GOVERNED_SUBJECT
                : ArtifactSourceTaxonomy::SOURCE_TARGET_MANAGED_ENVIRONMENT,
            sourceTargetIdentifier: $targetIdentifier ?? (string) $finding->managed_environment_id,
            detectorKey: $this->findingDetectorKey($finding),
            controlKey: $this->controlKeyFromResolution($controlResolution),
        );
    }

    public function providerDetailForFinding(Finding $finding): ArtifactProviderDetail
    {
        $evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
        $policyType = $this->firstString([
            Arr::get($evidence, 'policy_type'),
            Arr::get($evidence, 'provider_object_type'),
        ]);
        $typeDescriptor = InventoryPolicyTypeMeta::typeDescriptorFor($policyType, $evidence);

        return new ArtifactProviderDetail(
            legacyFindingType: $this->nullableString($finding->finding_type),
            legacyPolicyType: $policyType,
            providerObjectType: $typeDescriptor['provider_object_type'] ?? $policyType ?? $this->nullableString($finding->finding_type),
            providerDisplayType: $typeDescriptor['provider_display_type'] ?? Str::headline((string) $finding->finding_type),
            detectorDetail: $this->findingDetectorKey($finding),
        );
    }

    public function forStoredReport(StoredReport $report): ArtifactSourceDescriptor
    {
        $payload = is_array($report->payload) ? $report->payload : [];
        $detectorKey = $this->storedReportDetectorKey((string) $report->report_type);

        return new ArtifactSourceDescriptor(
            workspaceId: (int) $report->workspace_id,
            tenantId: (int) $report->managed_environment_id,
            managedEnvironmentId: (int) $report->managed_environment_id,
            sourceFamily: ArtifactSourceTaxonomy::SOURCE_FAMILY_STORED_REPORT,
            sourceKind: ArtifactSourceTaxonomy::SOURCE_KIND_STORED_REPORT,
            providerKey: $this->providerKey($payload),
            providerConnectionId: $this->providerConnectionId($report->tenant, $payload),
            sourceTargetKind: ArtifactSourceTaxonomy::SOURCE_TARGET_MANAGED_ENVIRONMENT,
            sourceTargetIdentifier: (string) $report->managed_environment_id,
            detectorKey: $detectorKey,
            controlKey: $this->controlKeyForReport((string) $report->report_type),
        );
    }

    public function providerDetailForStoredReport(StoredReport $report): ArtifactProviderDetail
    {
        return new ArtifactProviderDetail(
            legacyReportType: $this->nullableString($report->report_type),
            providerObjectType: $this->nullableString($report->report_type),
            providerDisplayType: Str::headline(str_replace('.', '_', (string) $report->report_type)),
            detectorDetail: $this->storedReportDetectorKey((string) $report->report_type),
        );
    }

    public function forInventoryItem(InventoryItem $item): ArtifactSourceDescriptor
    {
        $metadata = is_array($item->meta_jsonb) ? $item->meta_jsonb : [];
        $typeDescriptor = InventoryPolicyTypeMeta::typeDescriptorFor($this->nullableString($item->policy_type), $metadata);
        $detectorKey = 'inventory.'.($typeDescriptor['provider_object_type'] ?? $item->policy_type ?? 'unknown');

        return new ArtifactSourceDescriptor(
            workspaceId: (int) $item->workspace_id,
            tenantId: (int) $item->managed_environment_id,
            managedEnvironmentId: (int) $item->managed_environment_id,
            sourceFamily: ArtifactSourceTaxonomy::SOURCE_FAMILY_INVENTORY,
            sourceKind: ArtifactSourceTaxonomy::SOURCE_KIND_INVENTORY_PROJECTION,
            providerKey: $this->providerKey($metadata),
            providerConnectionId: $this->providerConnectionId($item->tenant, $metadata),
            sourceTargetKind: ArtifactSourceTaxonomy::SOURCE_TARGET_GOVERNED_SUBJECT,
            sourceTargetIdentifier: $this->firstString([$item->external_id, $item->getKey() !== null ? (string) $item->getKey() : null]),
            detectorKey: $detectorKey,
            controlKey: $this->controlKeyForInventoryPolicyType($this->nullableString($item->policy_type)),
        );
    }

    public function providerDetailForInventoryItem(InventoryItem $item): ArtifactProviderDetail
    {
        $metadata = is_array($item->meta_jsonb) ? $item->meta_jsonb : [];
        $typeDescriptor = InventoryPolicyTypeMeta::typeDescriptorFor($this->nullableString($item->policy_type), $metadata);

        return new ArtifactProviderDetail(
            legacyPolicyType: $typeDescriptor['legacy_policy_type'] ?? null,
            providerObjectType: $typeDescriptor['provider_object_type'] ?? null,
            providerDisplayType: $typeDescriptor['provider_display_type'] ?? null,
            detectorDetail: 'inventory.'.($typeDescriptor['provider_object_type'] ?? 'unknown'),
        );
    }

    public function forEvidenceSnapshotItem(EvidenceSnapshotItem $item): ArtifactSourceDescriptor
    {
        $payload = is_array($item->summary_payload) ? $item->summary_payload : [];
        $descriptor = $payload['source_descriptor'] ?? null;

        if (is_array($descriptor)) {
            return ArtifactSourceDescriptor::fromArray($descriptor);
        }

        $tenant = $item->tenant;

        return $this->forEvidenceProviderPayload(
            $tenant instanceof ManagedEnvironment ? $tenant : null,
            [
                'dimension_key' => $item->dimension_key,
                'source_kind' => $item->source_kind,
                'source_record_type' => $item->source_record_type,
                'source_record_id' => $item->source_record_id,
                'summary_payload' => $payload,
            ],
            workspaceId: (int) $item->workspace_id,
            managedEnvironmentId: (int) $item->managed_environment_id,
        );
    }

    /**
     * @param  array<string, mixed>  $item
     */
    public function forEvidenceProviderPayload(?ManagedEnvironment $tenant, array $item, ?int $workspaceId = null, ?int $managedEnvironmentId = null): ArtifactSourceDescriptor
    {
        $sourceKind = $this->sourceKind($item['source_kind'] ?? null);
        $dimensionKey = $this->nullableString($item['dimension_key'] ?? null);
        $sourceRecordType = $this->nullableString($item['source_record_type'] ?? null);
        $sourceRecordId = $this->nullableString($item['source_record_id'] ?? null);
        $summaryPayload = is_array($item['summary_payload'] ?? null) ? $item['summary_payload'] : [];
        $sourceTargetKind = $this->sourceTargetKindForEvidenceItem($sourceKind, $sourceRecordType);

        $workspaceId ??= (int) $tenant?->workspace_id;
        $managedEnvironmentId ??= (int) $tenant?->getKey();

        return new ArtifactSourceDescriptor(
            workspaceId: $workspaceId,
            tenantId: $managedEnvironmentId,
            managedEnvironmentId: $managedEnvironmentId,
            sourceFamily: $this->sourceFamilyForEvidenceItem($sourceKind, $sourceRecordType, $dimensionKey),
            sourceKind: $sourceKind,
            providerKey: $this->providerKey($summaryPayload),
            providerConnectionId: $this->providerConnectionId($tenant, $summaryPayload),
            sourceTargetKind: $sourceTargetKind,
            sourceTargetIdentifier: $this->sourceTargetIdentifier($sourceTargetKind, $sourceRecordId, $managedEnvironmentId),
            detectorKey: $dimensionKey,
            controlKey: $this->controlKeyForEvidenceDimension($dimensionKey, $summaryPayload),
        );
    }

    /**
     * @param  array<string, mixed>  $item
     */
    public function providerDetailForEvidenceProviderPayload(array $item): ArtifactProviderDetail
    {
        $sourceKind = $this->sourceKind($item['source_kind'] ?? null);
        $dimensionKey = $this->nullableString($item['dimension_key'] ?? null);
        $sourceRecordType = $this->nullableString($item['source_record_type'] ?? null);
        $sourceRecordId = $this->nullableString($item['source_record_id'] ?? null);

        if (($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_STORED_REPORT || $sourceRecordType === StoredReport::class) && is_numeric($sourceRecordId)) {
            $report = StoredReport::query()->find((int) $sourceRecordId);

            if ($report instanceof StoredReport) {
                return $this->providerDetailForStoredReport($report);
            }
        }

        return match ($this->sourceFamilyForEvidenceItem($sourceKind, $sourceRecordType, $dimensionKey)) {
            ArtifactSourceTaxonomy::SOURCE_FAMILY_FINDING => new ArtifactProviderDetail(
                legacyFindingType: $dimensionKey,
                providerObjectType: $sourceRecordType,
                providerDisplayType: 'Findings summary',
                detectorDetail: $dimensionKey,
            ),
            ArtifactSourceTaxonomy::SOURCE_FAMILY_OPERATION_RUN => new ArtifactProviderDetail(
                providerObjectType: OperationRun::class,
                providerDisplayType: 'Operation run',
                detectorDetail: $dimensionKey,
            ),
            ArtifactSourceTaxonomy::SOURCE_FAMILY_INVENTORY => new ArtifactProviderDetail(
                providerObjectType: InventoryItem::class,
                providerDisplayType: 'Inventory projection',
                detectorDetail: $dimensionKey,
            ),
            default => new ArtifactProviderDetail(
                providerObjectType: $sourceRecordType,
                providerDisplayType: $dimensionKey !== null ? Str::headline($dimensionKey) : null,
                detectorDetail: $dimensionKey,
            ),
        };
    }

    public function canonicalControlResolutionForFinding(Finding $finding): array
    {
        return $this->canonicalControlResolver
            ->resolve($this->resolutionRequestForFinding($finding))
            ->toArray();
    }

    public function resolutionRequestForFinding(Finding $finding): CanonicalControlResolutionRequest
    {
        $evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
        $findingType = (string) $finding->finding_type;

        if ($findingType === Finding::FINDING_TYPE_PERMISSION_POSTURE) {
            return new CanonicalControlResolutionRequest(
                provider: $this->providerKey($evidence),
                consumerContext: 'evidence',
                subjectFamilyKey: 'permission_posture',
                workload: 'entra',
                signalKey: 'permission_posture.required_graph_permission',
            );
        }

        if ($findingType === Finding::FINDING_TYPE_ENTRA_ADMIN_ROLES) {
            $roleTemplateId = (string) ($evidence['role_template_id'] ?? '');

            return new CanonicalControlResolutionRequest(
                provider: $this->providerKey($evidence),
                consumerContext: 'evidence',
                subjectFamilyKey: 'entra_admin_roles',
                workload: 'entra',
                signalKey: $roleTemplateId === '62e90394-69f5-4237-9190-012177145e10'
                    ? 'entra_admin_roles.global_admin_assignment'
                    : 'entra_admin_roles.privileged_role_assignment',
            );
        }

        if ($findingType === Finding::FINDING_TYPE_DRIFT) {
            $policyType = $this->firstString([
                $evidence['policy_type'] ?? null,
                'drift',
            ]) ?? 'drift';

            return new CanonicalControlResolutionRequest(
                provider: $this->providerKey($evidence),
                consumerContext: 'evidence',
                subjectFamilyKey: $policyType,
                workload: 'intune',
                signalKey: match ($policyType) {
                    'deviceCompliancePolicy' => 'intune.device_compliance_policy',
                    'drift' => 'finding.drift',
                    default => 'intune.device_configuration_drift',
                },
            );
        }

        return new CanonicalControlResolutionRequest(
            provider: $this->providerKey($evidence),
            consumerContext: 'evidence',
            subjectFamilyKey: $findingType,
        );
    }

    /**
     * @param  array<string, mixed>  $resolution
     */
    private function controlKeyFromResolution(array $resolution): ?string
    {
        return $this->nullableString(Arr::get($resolution, 'control.control_key'));
    }

    private function findingDetectorKey(Finding $finding): ?string
    {
        $evidence = is_array($finding->evidence_jsonb) ? $finding->evidence_jsonb : [];
        $findingType = (string) $finding->finding_type;

        if ($findingType === Finding::FINDING_TYPE_PERMISSION_POSTURE) {
            return 'permission_posture.required_graph_permission';
        }

        if ($findingType === Finding::FINDING_TYPE_ENTRA_ADMIN_ROLES) {
            $roleTemplateId = (string) ($evidence['role_template_id'] ?? '');

            return $roleTemplateId === '62e90394-69f5-4237-9190-012177145e10'
                ? 'entra_admin_roles.global_admin_assignment'
                : 'entra_admin_roles.privileged_role_assignment';
        }

        if ($findingType === Finding::FINDING_TYPE_DRIFT) {
            $policyType = $this->nullableString($evidence['policy_type'] ?? null) ?? 'drift';

            return match ($policyType) {
                'deviceCompliancePolicy' => 'intune.device_compliance_policy',
                'drift' => 'finding.drift',
                default => 'intune.device_configuration_drift',
            };
        }

        return $findingType !== '' ? $findingType : null;
    }

    private function storedReportDetectorKey(string $reportType): ?string
    {
        return match ($reportType) {
            StoredReport::REPORT_TYPE_PERMISSION_POSTURE => 'permission_posture.required_graph_permission',
            StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES => 'entra_admin_roles.privileged_role_assignment',
            default => $this->nullableString($reportType),
        };
    }

    private function controlKeyForReport(string $reportType): ?string
    {
        $request = match ($reportType) {
            StoredReport::REPORT_TYPE_PERMISSION_POSTURE => new CanonicalControlResolutionRequest(
                provider: 'microsoft',
                consumerContext: 'report',
                subjectFamilyKey: 'permission_posture',
                workload: 'entra',
                signalKey: 'permission_posture.required_graph_permission',
            ),
            StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES => new CanonicalControlResolutionRequest(
                provider: 'microsoft',
                consumerContext: 'report',
                subjectFamilyKey: 'entra_admin_roles',
                workload: 'entra',
                signalKey: 'entra_admin_roles.privileged_role_assignment',
            ),
            default => new CanonicalControlResolutionRequest(
                provider: 'microsoft',
                consumerContext: 'report',
                subjectFamilyKey: $reportType,
            ),
        };

        return $this->controlKeyFromResolution($this->canonicalControlResolver->resolve($request)->toArray());
    }

    private function controlKeyForInventoryPolicyType(?string $policyType): ?string
    {
        if ($policyType === null) {
            return null;
        }

        $request = new CanonicalControlResolutionRequest(
            provider: 'microsoft',
            consumerContext: 'evidence',
            subjectFamilyKey: $policyType,
            workload: str_contains($policyType, 'conditionalAccess') ? 'entra' : 'intune',
            signalKey: match ($policyType) {
                'deviceCompliancePolicy' => 'intune.device_compliance_policy',
                'conditionalAccessPolicy' => 'conditional_access.policy_state',
                default => 'intune.device_configuration_drift',
            },
        );

        return $this->controlKeyFromResolution($this->canonicalControlResolver->resolve($request)->toArray());
    }

    /**
     * @param  array<string, mixed>  $summaryPayload
     */
    private function controlKeyForEvidenceDimension(?string $dimensionKey, array $summaryPayload): ?string
    {
        $direct = $this->nullableString(Arr::get($summaryPayload, 'source_descriptor.control_key'))
            ?? $this->nullableString(Arr::get($summaryPayload, 'control_key'));

        if ($direct !== null) {
            return $direct;
        }

        $control = collect(Arr::wrap($summaryPayload['canonical_controls'] ?? []))
            ->first(static fn (mixed $item): bool => is_array($item) && filled($item['control_key'] ?? null));

        if (is_array($control)) {
            return $this->nullableString($control['control_key'] ?? null);
        }

        return match ($dimensionKey) {
            'permission_posture' => $this->controlKeyForReport(StoredReport::REPORT_TYPE_PERMISSION_POSTURE),
            'entra_admin_roles' => $this->controlKeyForReport(StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES),
            default => null,
        };
    }

    private function sourceKind(mixed $value): string
    {
        if (is_string($value) && ArtifactSourceTaxonomy::isSourceKind($value)) {
            return $value;
        }

        return ArtifactSourceTaxonomy::SOURCE_KIND_MODEL_SUMMARY;
    }

    private function sourceFamilyForEvidenceItem(string $sourceKind, ?string $sourceRecordType, ?string $dimensionKey): string
    {
        if ($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_STORED_REPORT || $sourceRecordType === StoredReport::class || $sourceRecordType === 'stored_report') {
            return ArtifactSourceTaxonomy::SOURCE_FAMILY_STORED_REPORT;
        }

        if ($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_OPERATION_ROLLUP || $sourceRecordType === OperationRun::class || $sourceRecordType === 'operation_run') {
            return ArtifactSourceTaxonomy::SOURCE_FAMILY_OPERATION_RUN;
        }

        if ($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_INVENTORY_PROJECTION || $sourceRecordType === InventoryItem::class || $sourceRecordType === 'inventory_item') {
            return ArtifactSourceTaxonomy::SOURCE_FAMILY_INVENTORY;
        }

        if ($sourceRecordType === Finding::class || $sourceRecordType === 'finding' || in_array($dimensionKey, ['findings_summary', 'baseline_drift_posture'], true)) {
            return ArtifactSourceTaxonomy::SOURCE_FAMILY_FINDING;
        }

        return ArtifactSourceTaxonomy::SOURCE_FAMILY_EVIDENCE_SNAPSHOT;
    }

    private function sourceTargetKindForEvidenceItem(string $sourceKind, ?string $sourceRecordType): string
    {
        if ($sourceKind === ArtifactSourceTaxonomy::SOURCE_KIND_OPERATION_ROLLUP || $sourceRecordType === OperationRun::class || $sourceRecordType === 'operation_run') {
            return ArtifactSourceTaxonomy::SOURCE_TARGET_OPERATION_RUN;
        }

        if ($sourceRecordType === ProviderConnection::class || $sourceRecordType === 'provider_connection') {
            return ArtifactSourceTaxonomy::SOURCE_TARGET_PROVIDER_CONNECTION;
        }

        return ArtifactSourceTaxonomy::SOURCE_TARGET_MANAGED_ENVIRONMENT;
    }

    private function sourceTargetIdentifier(string $sourceTargetKind, ?string $sourceRecordId, int $managedEnvironmentId): ?string
    {
        return match ($sourceTargetKind) {
            ArtifactSourceTaxonomy::SOURCE_TARGET_OPERATION_RUN,
            ArtifactSourceTaxonomy::SOURCE_TARGET_PROVIDER_CONNECTION,
            ArtifactSourceTaxonomy::SOURCE_TARGET_GOVERNED_SUBJECT => $sourceRecordId,
            default => (string) $managedEnvironmentId,
        };
    }

    /**
     * @param  array<string, mixed>  $payload
     */
    private function providerKey(array $payload): string
    {
        return $this->firstString([
            Arr::get($payload, 'source_descriptor.provider_key'),
            Arr::get($payload, 'provider_key'),
            Arr::get($payload, 'provider'),
        ]) ?? 'microsoft';
    }

    /**
     * @param  array<string, mixed>  $payload
     */
    private function providerConnectionId(?ManagedEnvironment $tenant, array $payload): ?int
    {
        $value = Arr::get($payload, 'source_descriptor.provider_connection_id')
            ?? Arr::get($payload, 'provider_connection_id')
            ?? Arr::get($payload, 'providerConnectionId')
            ?? Arr::get($payload, 'provider_connection.id');

        if (is_numeric($value)) {
            return (int) $value;
        }

        if (! $tenant instanceof ManagedEnvironment || ! $tenant->exists) {
            return null;
        }

        $connectionId = $tenant->providerConnections()
            ->where('provider', $this->providerKey($payload))
            ->where('is_default', true)
            ->value('id');

        return is_numeric($connectionId) ? (int) $connectionId : null;
    }

    /**
     * @param  list<mixed>  $values
     */
    private function firstString(array $values): ?string
    {
        foreach ($values as $value) {
            $string = $this->nullableString($value);

            if ($string !== null) {
                return $string;
            }
        }

        return null;
    }

    private function nullableString(mixed $value): ?string
    {
        if ($value instanceof Model && $value->getKey() !== null) {
            return (string) $value->getKey();
        }

        if (! is_scalar($value)) {
            return null;
        }

        $value = trim((string) $value);

        return $value === '' ? null : $value;
    }
}
