<?php

namespace App\Services\Inventory;

use App\Models\InventoryItem;
use App\Models\InventoryLink;
use App\Support\Enums\RelationshipType;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;

class DependencyExtractionService
{
    /**
     * Extracts dependencies for a given inventory item using the raw policy payload if available.
     * Idempotent via unique key on inventory_links. Enforces a max of 50 outbound edges per item.
     *
     * @param  array<string, mixed>  $policyData
     */
    public function extractForPolicyData(InventoryItem $item, array $policyData): array
    {
        $warnings = [];
        $edges = collect();

        $edges = $edges->merge($this->extractAssignments($item, $policyData, $warnings));
        $edges = $edges->merge($this->extractScopedBy($item, $policyData));

        // Prevent PostgreSQL cardinality violation on upsert by deduplicating payload rows.
        $edges = $edges
            ->unique(fn (array $e) => implode('|', [
                (string) ($e['managed_environment_id'] ?? ''),
                (string) ($e['source_type'] ?? ''),
                (string) ($e['source_id'] ?? ''),
                (string) ($e['target_type'] ?? ''),
                (string) ($e['target_id'] ?? ''),
                (string) ($e['relationship_type'] ?? ''),
            ]))
            ->values();

        // Enforce max 50 outbound edges by priority: assigned_to > scoped_by > others
        $priorities = [
            RelationshipType::AssignedToInclude->value => 1,
            RelationshipType::AssignedToExclude->value => 2,
            RelationshipType::AssignedTo->value => 3, // legacy
            RelationshipType::UsesAssignmentFilter->value => 4,
            RelationshipType::ScopedBy->value => 5,
            RelationshipType::Targets->value => 6,
            RelationshipType::DependsOn->value => 7,
        ];

        /** @var Collection<int, array{managed_environment_id:int,source_type:string,source_id:string,target_type:string,target_id:?string,relationship_type:string,metadata:array}> $sorted */
        $sorted = $edges->sortBy(fn ($e) => $priorities[$e['relationship_type']] ?? 99)->values();

        $limited = $sorted->take(50);

        $now = now();
        $payload = $limited->map(function (array $e) use ($item, $now) {
            $metadata = $e['metadata'] ?? null;
            if (is_array($metadata)) {
                // Ensure portability across SQLite/Postgres when using upsert via query builder
                $e['metadata'] = json_encode($metadata);
            }

            return array_merge($e, [
                'workspace_id' => $item->workspace_id,
                'created_at' => $now,
                'updated_at' => $now,
            ]);
        })->all();

        if (! empty($payload)) {
            InventoryLink::query()->upsert(
                $payload,
                ['managed_environment_id', 'source_type', 'source_id', 'target_type', 'target_id', 'relationship_type'],
                ['metadata', 'updated_at']
            );
        }

        return $warnings;
    }

    /**
     * @param  array<string, mixed>  $policyData
     * @return Collection<int, array<string, mixed>>
     */
    private function extractAssignments(InventoryItem $item, array $policyData, array &$warnings): Collection
    {
        $assignments = Arr::get($policyData, 'assignments');
        if (! is_array($assignments)) {
            return collect();
        }

        $edges = [];

        foreach ($assignments as $assignment) {
            if (! is_array($assignment)) {
                continue;
            }

            $policyId = (string) ($policyData['id'] ?? $item->external_id);

            $target = Arr::get($assignment, 'target');
            $odataType = is_array($target) ? (Arr::get($target, '@odata.type') ?? Arr::get($target, '@OData.Type')) : null;
            $odataType = is_string($odataType) ? strtolower($odataType) : null;

            $groupId = Arr::get($assignment, 'target.groupId') ?? Arr::get($assignment, 'groupId');
            $groupId = is_string($groupId) ? trim($groupId) : null;

            $filterId = Arr::get($assignment, 'deviceAndAppManagementAssignmentFilterId')
                ?? Arr::get($assignment, 'assignmentFilterId')
                ?? Arr::get($assignment, 'target.deviceAndAppManagementAssignmentFilterId');
            $filterId = is_string($filterId) ? trim($filterId) : null;

            $filterType = Arr::get($assignment, 'deviceAndAppManagementAssignmentFilterType')
                ?? Arr::get($assignment, 'assignmentFilterType')
                ?? Arr::get($assignment, 'target.deviceAndAppManagementAssignmentFilterType');
            $filterType = is_string($filterType) ? strtolower(trim($filterType)) : null;
            $filterMode = in_array($filterType, ['include', 'exclude'], true) ? $filterType : null;

            if (is_string($filterId) && $filterId !== '') {
                $edges[] = [
                    'managed_environment_id' => (int) $item->managed_environment_id,
                    'source_type' => 'inventory_item',
                    'source_id' => (string) $item->external_id,
                    'target_type' => 'foundation_object',
                    'target_id' => $filterId,
                    'relationship_type' => RelationshipType::UsesAssignmentFilter->value,
                    'metadata' => array_filter([
                        'last_known_name' => null,
                        'foundation_type' => 'assignment_filter',
                        'filter_mode' => $filterMode,
                    ], fn ($v) => $v !== null),
                ];
            }

            if (is_string($groupId) && $groupId !== '') {
                $relationshipType = RelationshipType::AssignedToInclude->value;
                if (is_string($odataType) && str_contains($odataType, 'exclusion')) {
                    $relationshipType = RelationshipType::AssignedToExclude->value;
                }
                $edges[] = [
                    'managed_environment_id' => (int) $item->managed_environment_id,
                    'source_type' => 'inventory_item',
                    'source_id' => (string) $item->external_id,
                    'target_type' => 'foundation_object',
                    'target_id' => $groupId,
                    'relationship_type' => $relationshipType,
                    'metadata' => [
                        'last_known_name' => null,
                        'foundation_type' => 'aad_group',
                    ],
                ];

                continue;
            }

            // Known non-group targets (e.g. allDevices/allLicensedUsers) are out-of-scope for edges.
            if (is_string($odataType) && (str_contains($odataType, 'alldevices') || str_contains($odataType, 'alllicensedusers') || str_contains($odataType, 'allusers'))) {
                continue;
            }

            $warning = [
                'type' => 'unsupported_reference',
                'policy_id' => $policyId,
                'raw_ref' => $assignment,
                'reason' => 'unsupported_assignment_target_shape',
            ];

            $warnings[] = $warning;
            Log::info('Unsupported reference shape encountered', $warning);
        }

        return collect($edges);
    }

    /**
     * @param  array<string, mixed>  $policyData
     * @return Collection<int, array<string, mixed>>
     */
    private function extractScopedBy(InventoryItem $item, array $policyData): Collection
    {
        $scopeTags = Arr::get($policyData, 'roleScopeTagIds');
        if (! is_array($scopeTags)) {
            return collect();
        }

        $edges = [];

        foreach ($scopeTags as $tagId) {
            if (is_string($tagId) && $tagId !== '') {
                $edges[] = [
                    'managed_environment_id' => (int) $item->managed_environment_id,
                    'source_type' => 'inventory_item',
                    'source_id' => (string) $item->external_id,
                    'target_type' => 'foundation_object',
                    'target_id' => $tagId,
                    'relationship_type' => RelationshipType::ScopedBy->value,
                    'metadata' => [
                        'last_known_name' => null,
                        'foundation_type' => 'scope_tag',
                    ],
                ];
            }
        }

        return collect($edges);
    }
}
