<?php

namespace App\Services\Topology;

use App\Models\Customer;
use App\Models\TopologyConnectionRule;
use App\Models\TopologyCustomerConnection;
use App\Models\TopologyDeviceType;
use App\Models\TopologyLink;
use App\Models\TopologyNode;
use App\Models\TopologyPort;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class TopologyService
{
    private const RELATIONS = [
        'device-type' => [],
        'node' => ['tenant:id,tenant_code,tenant_name', 'deviceType:id,tenant_id,code,name,category,color,icon', 'parent:id,code,name'],
        'link' => ['tenant:id,tenant_code,tenant_name', 'sourceNode.deviceType:id,code,name,color', 'targetNode.deviceType:id,code,name,color', 'sourcePort:id,node_id,name,port_number', 'targetPort:id,node_id,name,port_number'],
    ];

    public function list(string $resource, array $filters, User $actor, ?int $tenantId)
    {
        $query = $this->model($resource)::query()->with(self::RELATIONS[$resource]);
        $this->scopeTenant($query, $resource, $actor, $tenantId);
        $sortable = [
            'device-type' => ['id', 'code', 'name', 'category', 'status', 'created_at'],
            'node' => ['id', 'code', 'name', 'status', 'installed_at', 'created_at'],
            'link' => ['id', 'code', 'name', 'link_type', 'status', 'installed_at', 'created_at'],
        ][$resource];
        $sortBy = in_array($filters['sort_by'] ?? null, $sortable, true) ? $filters['sort_by'] : 'id';

        return $query
            ->when(! empty($filters['status']), fn ($q) => $q->where('status', $filters['status']))
            ->when(! empty($filters['device_type_id']) && $resource === 'node', fn ($q) => $q->where('device_type_id', $filters['device_type_id']))
            ->when(! empty($filters['link_type']) && $resource === 'link', fn ($q) => $q->where('link_type', $filters['link_type']))
            ->when(! empty($filters['search']), fn ($q) => $q->where(function ($search) use ($filters) {
                $term = "%{$filters['search']}%";
                $search->where('code', 'ILIKE', $term)->orWhere('name', 'ILIKE', $term);
            }))
            ->orderBy($sortBy, $filters['sort_direction'] ?? 'desc')
            ->paginate($filters['per_page'] ?? 10);
    }

    public function find(string $resource, int $id): Model
    {
        $relations = self::RELATIONS[$resource];
        if ($resource === 'node') {
            $relations = [...$relations, 'ports'];
        }

        return $this->model($resource)::with($relations)->findOrFail($id);
    }

    public function create(string $resource, array $data, User $actor, ?int $tenantId): Model
    {
        return DB::transaction(function () use ($resource, $data, $actor, $tenantId) {
            $data = $this->prepare($resource, $data, $actor, $tenantId);
            $model = $this->model($resource)::create($data);

            return $this->find($resource, $model->id);
        });
    }

    public function update(string $resource, Model $model, array $data, User $actor): Model
    {
        return DB::transaction(function () use ($resource, $model, $data, $actor) {
            if (! $actor->isMasterAdmin()) {
                unset($data['tenant_id']);
            }
            $data['tenant_id'] ??= $model->tenant_id;
            $data = $this->prepare($resource, $data, $actor, $model->tenant_id, $model);
            $model->update($data);

            return $this->find($resource, $model->id);
        });
    }

    public function delete(string $resource, Model $model): void
    {
        if ($resource === 'device-type' && ($model->is_system || $model->nodes()->exists())) {
            throw ValidationException::withMessages(['device_type' => 'Kategori sistem atau kategori yang digunakan tidak dapat dihapus.']);
        }
        if ($resource === 'node' && ($model->incomingLinks()->exists() || $model->outgoingLinks()->exists())) {
            throw ValidationException::withMessages(['node' => 'Perangkat masih terhubung dengan jalur jaringan.']);
        }
        if ($resource === 'node' && TopologyCustomerConnection::where('odp_node_id', $model->id)->where('status', 'active')->exists()) {
            throw ValidationException::withMessages(['node' => 'Perangkat masih melayani customer aktif.']);
        }
        $model->delete();
    }

    public function options(User $actor, ?int $tenantId): array
    {
        $types = TopologyDeviceType::query();
        $nodes = TopologyNode::query()->with('deviceType:id,code,name,color');
        $this->scopeTenant($types, 'device-type', $actor, $tenantId);
        $this->scopeTenant($nodes, 'node', $actor, $tenantId);

        return [
            'device_types' => $types->where('status', 'active')->orderBy('name')->get(),
            'nodes' => $nodes->where('status', 'active')->orderBy('name')->get(),
        ];
    }

    public function ports(TopologyNode $node): Collection
    {
        return $node->ports()->get();
    }

    public function createPort(TopologyNode $node, array $data): TopologyPort
    {
        if (! $node->deviceType->can_have_ports) {
            throw ValidationException::withMessages(['node_id' => 'Kategori perangkat ini tidak mendukung port.']);
        }

        return $node->ports()->create([...$data, 'tenant_id' => $node->tenant_id]);
    }

    public function updatePort(TopologyPort $port, array $data): TopologyPort
    {
        $port->update($data);

        return $port->fresh();
    }

    public function deletePort(TopologyPort $port): void
    {
        if (TopologyLink::where('source_port_id', $port->id)->orWhere('target_port_id', $port->id)->exists()) {
            throw ValidationException::withMessages(['port' => 'Port masih digunakan oleh jalur jaringan.']);
        }
        $port->delete();
    }

    public function map(array $filters, User $actor, ?int $tenantId): array
    {
        $nodes = TopologyNode::query()->with('deviceType:id,code,name,color,icon');
        $links = TopologyLink::query()->with(['sourceNode:id,latitude,longitude', 'targetNode:id,latitude,longitude']);
        $this->scopeTenant($nodes, 'node', $actor, $tenantId);
        $this->scopeTenant($links, 'link', $actor, $tenantId);

        if (! empty($filters['bbox'])) {
            [$minLng, $minLat, $maxLng, $maxLat] = array_map('floatval', explode(',', $filters['bbox']));
            $nodes->whereBetween('longitude', [$minLng, $maxLng])->whereBetween('latitude', [$minLat, $maxLat]);
            $links->where(function ($query) use ($minLng, $minLat, $maxLng, $maxLat) {
                $query->whereHas('sourceNode', fn ($q) => $q->whereBetween('longitude', [$minLng, $maxLng])->whereBetween('latitude', [$minLat, $maxLat]))
                    ->orWhereHas('targetNode', fn ($q) => $q->whereBetween('longitude', [$minLng, $maxLng])->whereBetween('latitude', [$minLat, $maxLat]));
            });
        }

        $nodeRows = $nodes->where('status', '!=', 'inactive')->get();
        $linkRows = $links->where('status', '!=', 'inactive')->get();

        return [
            'nodes' => [
                'type' => 'FeatureCollection',
                'features' => $nodeRows->map(fn ($node) => [
                    'type' => 'Feature',
                    'id' => $node->id,
                    'geometry' => ['type' => 'Point', 'coordinates' => [(float) $node->longitude, (float) $node->latitude]],
                    'properties' => [
                        'id' => $node->id,
                        'code' => $node->code,
                        'name' => $node->name,
                        'status' => $node->status,
                        'device_type' => $node->deviceType?->code,
                        'color' => $node->deviceType?->color ?? '#2f74c0',
                    ],
                ])->values(),
            ],
            'links' => [
                'type' => 'FeatureCollection',
                'features' => $linkRows->map(function ($link) {
                    $geometry = $link->path_geojson ?: [
                        'type' => 'LineString',
                        'coordinates' => [
                            [(float) $link->sourceNode->longitude, (float) $link->sourceNode->latitude],
                            [(float) $link->targetNode->longitude, (float) $link->targetNode->latitude],
                        ],
                    ];

                    return [
                        'type' => 'Feature',
                        'id' => $link->id,
                        'geometry' => $geometry,
                        'properties' => [
                            'id' => $link->id,
                            'code' => $link->code,
                            'name' => $link->name,
                            'status' => $link->status,
                            'link_type' => $link->link_type,
                            'core_count' => $link->core_count,
                        ],
                    ];
                })->values(),
            ],
        ];
    }

    public function trace(TopologyNode $node, string $direction): array
    {
        $visited = [];
        $levels = [[$node->id]];
        while ($levels[count($levels) - 1] && count($levels) <= 100) {
            $ids = $levels[count($levels) - 1];
            $visited = [...$visited, ...$ids];
            $column = $direction === 'upstream' ? 'target_node_id' : 'source_node_id';
            $select = $direction === 'upstream' ? 'source_node_id' : 'target_node_id';
            $next = TopologyLink::where('tenant_id', $node->tenant_id)
                ->whereIn($column, $ids)
                ->where('status', 'active')
                ->pluck($select)
                ->reject(fn ($id) => in_array($id, $visited))
                ->unique()
                ->values()
                ->all();
            if (! $next) {
                break;
            }
            $levels[] = $next;
        }

        $nodes = TopologyNode::with('deviceType:id,code,name,color')
            ->whereIn('id', collect($levels)->flatten()->unique())
            ->get()->keyBy('id');

        return collect($levels)->map(fn ($ids, $level) => [
            'level' => $level,
            'nodes' => collect($ids)->map(fn ($id) => $nodes->get($id))->filter()->values(),
        ])->all();
    }

    public function connectCustomer(array $data, User $actor, ?int $tenantId): TopologyCustomerConnection
    {
        $customer = Customer::findOrFail($data['customer_id']);
        $node = TopologyNode::with('deviceType')->findOrFail($data['odp_node_id']);
        $effectiveTenant = $actor->isMasterAdmin() ? $customer->tenant_id : $tenantId;
        if ((int) $customer->tenant_id !== (int) $effectiveTenant || (int) $node->tenant_id !== (int) $effectiveTenant || $node->deviceType?->code !== 'ODP') {
            throw ValidationException::withMessages(['odp_node_id' => 'Customer dan ODP harus berasal dari tenant yang sama dan perangkat harus bertipe ODP.']);
        }
        if (! empty($data['odp_port_id']) && ! TopologyPort::whereKey($data['odp_port_id'])->where('node_id', $node->id)->exists()) {
            throw ValidationException::withMessages(['odp_port_id' => 'Port bukan milik ODP yang dipilih.']);
        }

        TopologyCustomerConnection::where('customer_id', $customer->id)->where('status', 'active')
            ->update(['status' => 'inactive', 'disconnected_at' => now()]);
        $customer->update(['topology_id' => $node->id]);

        return TopologyCustomerConnection::create([
            ...$data,
            'tenant_id' => $effectiveTenant,
            'connected_at' => $data['connected_at'] ?? now(),
            'status' => 'active',
        ])->load(['customer:id,customer_code,name', 'odpNode:id,code,name', 'odpPort:id,name,port_number']);
    }

    public function customerPath(Customer $customer): array
    {
        $connection = TopologyCustomerConnection::with(['odpNode.deviceType', 'odpPort'])
            ->where('customer_id', $customer->id)
            ->where('status', 'active')
            ->first();
        if (! $connection) {
            throw ValidationException::withMessages(['customer_id' => 'Customer belum terhubung ke ODP.']);
        }

        return [
            'customer' => $customer->only(['id', 'customer_code', 'name']),
            'connection' => $connection,
            'path' => $this->trace($connection->odpNode, 'upstream'),
        ];
    }

    private function prepare(string $resource, array $data, User $actor, ?int $tenantId, ?Model $existing = null): array
    {
        if ($resource === 'device-type') {
            $data['tenant_id'] = $actor->isMasterAdmin() ? ($data['tenant_id'] ?? null) : $tenantId;
            $data['is_system'] = $existing?->is_system ?? false;

            return $data;
        }

        $data['tenant_id'] = $actor->isMasterAdmin() ? ($data['tenant_id'] ?? $tenantId) : $tenantId;
        if (! $data['tenant_id']) {
            throw ValidationException::withMessages(['tenant_id' => 'Tenant wajib dipilih.']);
        }
        if ($resource === 'node') {
            $type = TopologyDeviceType::whereKey($data['device_type_id'] ?? $existing?->device_type_id)
                ->where(fn ($q) => $q->whereNull('tenant_id')->orWhere('tenant_id', $data['tenant_id']))
                ->first();
            if (! $type) {
                throw ValidationException::withMessages(['device_type_id' => 'Kategori perangkat tidak tersedia untuk tenant ini.']);
            }
            if (! empty($data['parent_node_id']) && ! TopologyNode::whereKey($data['parent_node_id'])->where('tenant_id', $data['tenant_id'])->exists()) {
                throw ValidationException::withMessages(['parent_node_id' => 'Parent perangkat bukan milik tenant yang sama.']);
            }
        }
        if ($resource === 'link') {
            $this->validateLink($data, $existing);
        }
        $data['updated_by'] = $actor->id;
        if (! $existing) {
            $data['created_by'] = $actor->id;
        }

        return $data;
    }

    private function validateLink(array $data, ?Model $existing): void
    {
        $sourceId = (int) ($data['source_node_id'] ?? $existing?->source_node_id);
        $targetId = (int) ($data['target_node_id'] ?? $existing?->target_node_id);
        $tenantId = (int) $data['tenant_id'];
        $nodes = TopologyNode::with('deviceType')->whereIn('id', [$sourceId, $targetId])->where('tenant_id', $tenantId)->get()->keyBy('id');
        if ($sourceId === $targetId || $nodes->count() !== 2) {
            throw ValidationException::withMessages(['target_node_id' => 'Node asal dan tujuan harus berbeda dan berasal dari tenant yang sama.']);
        }
        $rule = TopologyConnectionRule::where(fn ($q) => $q->whereNull('tenant_id')->orWhere('tenant_id', $tenantId))
            ->where('source_device_type_id', $nodes[$sourceId]->device_type_id)
            ->where('target_device_type_id', $nodes[$targetId]->device_type_id)
            ->orderByDesc('tenant_id')
            ->first();
        if ($rule && ! $rule->allowed) {
            throw ValidationException::withMessages(['target_node_id' => 'Hubungan antar kategori perangkat ini tidak diizinkan.']);
        }
        if ($rule?->max_connections) {
            $connectionCount = TopologyLink::where('source_node_id', $sourceId)
                ->when($existing, fn ($q) => $q->whereKeyNot($existing->id))
                ->count();
            if ($connectionCount >= $rule->max_connections) {
                throw ValidationException::withMessages(['source_node_id' => 'Batas maksimal koneksi perangkat asal sudah tercapai.']);
            }
        }
        $duplicate = TopologyLink::where('source_node_id', $sourceId)
            ->where('target_node_id', $targetId)
            ->when($existing, fn ($q) => $q->whereKeyNot($existing->id))
            ->where(function ($query) use ($data, $existing) {
                $sourcePortId = $data['source_port_id'] ?? $existing?->source_port_id;
                $targetPortId = $data['target_port_id'] ?? $existing?->target_port_id;
                $sourcePortId ? $query->where('source_port_id', $sourcePortId) : $query->whereNull('source_port_id');
                $targetPortId ? $query->where('target_port_id', $targetPortId) : $query->whereNull('target_port_id');
            })
            ->exists();
        if ($duplicate) {
            throw ValidationException::withMessages(['target_node_id' => 'Jalur dengan endpoint yang sama sudah tersedia.']);
        }
        foreach (['source_port_id' => $sourceId, 'target_port_id' => $targetId] as $field => $nodeId) {
            $portId = $data[$field] ?? $existing?->{$field};
            if ($portId) {
                $port = TopologyPort::whereKey($portId)->where('node_id', $nodeId)->where('tenant_id', $tenantId)->first();
                if (! $port) {
                    throw ValidationException::withMessages([$field => 'Port tidak sesuai dengan node yang dipilih.']);
                }
                $used = TopologyLink::where(fn ($q) => $q->where('source_port_id', $portId)->orWhere('target_port_id', $portId))
                    ->when($existing, fn ($q) => $q->whereKeyNot($existing->id))
                    ->count();
                if ($used >= $port->capacity) {
                    throw ValidationException::withMessages([$field => 'Kapasitas port sudah terpakai penuh.']);
                }
            }
        }
        if ($this->hasDirectedPath($targetId, $sourceId, $tenantId, $existing?->id)) {
            throw ValidationException::withMessages(['target_node_id' => 'Jalur ini membentuk loop topologi.']);
        }
    }

    private function hasDirectedPath(int $from, int $to, int $tenantId, ?int $ignoreLink): bool
    {
        $queue = [$from];
        $visited = [];
        while ($queue) {
            $current = array_shift($queue);
            if ($current === $to) {
                return true;
            }
            if (isset($visited[$current])) {
                continue;
            }
            $visited[$current] = true;
            $next = TopologyLink::where('tenant_id', $tenantId)
                ->where('source_node_id', $current)
                ->when($ignoreLink, fn ($q) => $q->whereKeyNot($ignoreLink))
                ->pluck('target_node_id')
                ->all();
            $queue = [...$queue, ...$next];
        }

        return false;
    }

    private function scopeTenant($query, string $resource, User $actor, ?int $tenantId): void
    {
        if ($resource === 'device-type') {
            if ($actor->isMasterAdmin() && ! $tenantId) {
                return;
            }
            $query->where(function ($q) use ($tenantId) {
                $q->whereNull('tenant_id')
                    ->orWhere('tenant_id', $tenantId);
            });

            return;
        }
        $query->when(! $actor->isMasterAdmin(), fn ($q) => $q->where('tenant_id', $tenantId))
            ->when($actor->isMasterAdmin() && $tenantId, fn ($q) => $q->where('tenant_id', $tenantId));
    }

    private function model(string $resource): string
    {
        return match ($resource) {
            'device-type' => TopologyDeviceType::class,
            'node' => TopologyNode::class,
            'link' => TopologyLink::class,
            default => throw ValidationException::withMessages(['resource' => 'Resource topologi tidak valid.']),
        };
    }
}
