<?php

declare(strict_types=1);

namespace Maestro\Core\Services;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Maestro\Core\Models\MaestroModule;
use Maestro\Core\Models\NavigationNode;

/**
 * Replaces navigation_nodes from a nested tree payload (superadmin UI).
 */
final class NavigationTreeSyncService
{
    /**
     * @param  array<int, mixed>  $treeRoots  Exactly one root node with key `root`
     *
     * @throws ValidationException
     */
    public function sync(array $treeRoots): void
    {
        $validator = Validator::make(['tree' => $treeRoots], [
            'tree' => ['required', 'array', 'size:1'],
            'tree.0.key' => ['required', 'string', 'in:root'],
            'tree.0.label' => ['required', 'string', 'max:255'],
            'tree.0.children' => ['nullable', 'array'],
        ]);

        if ($validator->fails()) {
            throw new ValidationException($validator);
        }

        $rootData = $treeRoots[0];
        $keysSeen = [];

        $this->validateNodeRecursive($rootData, 'tree.0', $keysSeen);

        DB::transaction(function () use ($rootData): void {
            Schema::disableForeignKeyConstraints();
            try {
                NavigationNode::query()->delete();
                $this->insertNode($rootData, null, 0);
            } finally {
                Schema::enableForeignKeyConstraints();
            }
        });
    }

    /**
     * @param  array<string, mixed>  $data
     * @param  array<string, true>  $keysSeen
     */
    private function validateNodeRecursive(array $data, string $prefix, array &$keysSeen): void
    {
        $validator = Validator::make($data, [
            'key' => ['required', 'string', 'max:255'],
            'label' => ['required', 'string', 'max:255'],
            'route' => ['nullable', 'string', 'max:2048'],
            'icon' => ['nullable', 'string', 'max:255'],
            'permission' => ['nullable', 'string', 'max:255'],
            'description' => ['nullable', 'string', 'max:5000'],
            'module_slug' => ['nullable', 'string', 'max:255'],
            'is_header' => ['sometimes', 'boolean'],
            'pinned_bottom' => ['sometimes', 'boolean'],
            'children' => ['nullable', 'array'],
        ]);

        if ($validator->fails()) {
            throw ValidationException::withMessages([
                $prefix => $validator->errors()->all(),
            ]);
        }

        $key = $data['key'];
        if (isset($keysSeen[$key])) {
            throw ValidationException::withMessages([
                $prefix.'.key' => ["Duplicate navigation key: {$key}"],
            ]);
        }
        $keysSeen[$key] = true;

        foreach ($data['children'] ?? [] as $index => $child) {
            if (! is_array($child)) {
                throw ValidationException::withMessages([
                    "{$prefix}.children.{$index}" => ['Each child must be an object.'],
                ]);
            }
            /** @var array<string, mixed> $child */
            $this->validateNodeRecursive($child, "{$prefix}.children.{$index}", $keysSeen);
        }
    }

    /**
     * @param  array<string, mixed>  $data
     */
    private function insertNode(array $data, ?int $parentId, int $sortOrder): void
    {
        $moduleId = null;
        $slug = $data['module_slug'] ?? null;
        if (is_string($slug) && $slug !== '') {
            $moduleId = MaestroModule::query()->where('slug', $slug)->value('id');
        }

        $node = NavigationNode::query()->create([
            'parent_id' => $parentId,
            'module_id' => $moduleId,
            'sort_order' => $sortOrder,
            'key' => $data['key'],
            'label' => $data['label'],
            'icon' => $this->nullableString($data['icon'] ?? null),
            'route' => $this->nullableString($data['route'] ?? null),
            'permission' => $this->nullableString($data['permission'] ?? null),
            'is_header' => (bool) ($data['is_header'] ?? false),
            'pinned_bottom' => (bool) ($data['pinned_bottom'] ?? false),
            'description' => $this->nullableString($data['description'] ?? null),
        ]);

        foreach ($data['children'] ?? [] as $index => $child) {
            /** @var array<string, mixed> $child */
            $this->insertNode($child, $node->id, $index);
        }
    }

    private function nullableString(mixed $value): ?string
    {
        if ($value === null || $value === '') {
            return null;
        }

        return is_string($value) ? $value : null;
    }
}
