<?php

namespace App\Domain\Website\Services;

use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Website\Blocks\BlockRegistry;
use App\Domain\Website\Models\WebsiteBlock;
use App\Domain\Website\Models\WebsitePage;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class WebsiteBlockService
{
    /** Guards against a pathological nesting depth in the builder UI. */
    public const MAX_DEPTH = 5;

    public function __construct(private readonly BlockRegistry $registry) {}

    public function add(
        WebsitePage $page,
        string $type,
        ?WebsiteBlock $parent = null,
        string $slot = 'default',
        ?int $position = null,
    ): WebsiteBlock {
        $definition = $this->registry->resolve($type);

        if ($parent) {
            $this->assertCanNest($parent, $definition->key());
        }

        if ($definition->isSingleton() && $this->existsOnPage($page, $type)) {
            throw new DomainException('لا يمكن إضافة أكثر من عنصر واحد من هذا النوع في الصفحة.');
        }

        return DB::transaction(function () use ($page, $definition, $parent, $slot, $position) {
            $variants = array_keys($definition->variants());

            $block = WebsiteBlock::create([
                'website_page_id' => $page->id,
                'parent_id' => $parent?->id,
                'slot' => $slot,
                'type' => $definition->key(),
                'variant' => $variants[0] ?? 'default',
                'sort_order' => $position ?? $this->nextSortOrder($page, $parent, $slot),
                'is_enabled' => true,
                'data' => $definition->defaultData(),
                'style' => [],
            ]);

            if ($position !== null) {
                $this->normalizeSiblings($page, $parent, $slot);
            }

            return $block;
        });
    }

    public function update(WebsiteBlock $block, array $attributes): WebsiteBlock
    {
        return DB::transaction(function () use ($block, $attributes) {
            $payload = [];

            if (array_key_exists('data', $attributes)) {
                // Merge rather than replace: the builder saves one panel at a time.
                $payload['data'] = array_replace_recursive(
                    $block->data ?? [],
                    $attributes['data'],
                );
            }

            foreach (['variant', 'style', 'is_enabled', 'slot'] as $key) {
                if (array_key_exists($key, $attributes)) {
                    $payload[$key] = $attributes[$key];
                }
            }

            if (isset($payload['variant'])) {
                $definition = $block->definition();
                if ($definition && ! array_key_exists($payload['variant'], $definition->variants())) {
                    throw new DomainException('نمط العرض المختار غير متاح لهذا العنصر.');
                }
            }

            $block->update($payload);

            return $block->refresh();
        });
    }

    /**
     * Replaces a repeater's rows wholesale. Repeater rows are positional, so
     * merging them would blend a deleted row's values into its successor.
     */
    public function setRepeater(WebsiteBlock $block, string $field, array $rows): WebsiteBlock
    {
        return DB::transaction(function () use ($block, $field, $rows) {
            $data = $block->data ?? [];
            $data[$field] = array_values($rows);
            $block->update(['data' => $data]);

            return $block->refresh();
        });
    }

    public function delete(WebsiteBlock $block): void
    {
        DB::transaction(fn () => $block->delete());
    }

    public function duplicate(WebsiteBlock $block): WebsiteBlock
    {
        return DB::transaction(function () use ($block) {
            $copy = $this->copyTree($block, $block->website_page_id, $block->parent_id);
            $copy->update(['sort_order' => $block->sort_order + 1]);

            $this->normalizeSiblings(
                $block->page ?? WebsitePage::find($block->website_page_id),
                $block->parent,
                $block->slot,
            );

            return $copy->refresh();
        });
    }

    /** Recursively copies a block and its descendants. */
    public function copyTree(WebsiteBlock $block, ?int $pageId, ?int $parentId): WebsiteBlock
    {
        $copy = $block->replicate(['uuid']);
        $copy->uuid = (string) Str::uuid();
        $copy->website_page_id = $pageId;
        $copy->parent_id = $parentId;
        $copy->save();

        foreach ($block->children as $child) {
            $this->copyTree($child, $pageId, $copy->id);
        }

        return $copy;
    }

    /** Moves a block to a new parent/slot and reorders its new siblings. */
    public function move(WebsiteBlock $block, ?WebsiteBlock $parent, string $slot, int $position): WebsiteBlock
    {
        if ($parent) {
            $this->assertCanNest($parent, $block->type);

            if ($this->isDescendantOf($parent, $block)) {
                throw new DomainException('لا يمكن نقل العنصر إلى داخل نفسه.');
            }
        }

        return DB::transaction(function () use ($block, $parent, $slot, $position) {
            $block->update([
                'parent_id' => $parent?->id,
                'slot' => $slot,
                'sort_order' => $position,
            ]);

            $page = WebsitePage::find($block->website_page_id);
            $this->normalizeSiblings($page, $parent, $slot, $block->id, $position);

            return $block->refresh();
        });
    }

    public function reorder(array $orderedIds): void
    {
        DB::transaction(function () use ($orderedIds) {
            foreach ($orderedIds as $i => $id) {
                WebsiteBlock::where('id', $id)->update(['sort_order' => $i]);
            }
        });
    }

    private function existsOnPage(WebsitePage $page, string $type): bool
    {
        return WebsiteBlock::where('website_page_id', $page->id)
            ->where('type', $type)
            ->exists();
    }

    private function nextSortOrder(WebsitePage $page, ?WebsiteBlock $parent, string $slot): int
    {
        return (int) WebsiteBlock::where('website_page_id', $page->id)
            ->where('parent_id', $parent?->id)
            ->where('slot', $slot)
            ->max('sort_order') + 1;
    }

    /** Rewrites sibling sort_order to a dense 0..n sequence. */
    private function normalizeSiblings(
        ?WebsitePage $page,
        ?WebsiteBlock $parent,
        string $slot,
        ?int $movedId = null,
        ?int $movedPosition = null,
    ): void {
        if (! $page) {
            return;
        }

        $siblings = WebsiteBlock::where('website_page_id', $page->id)
            ->where('parent_id', $parent?->id)
            ->where('slot', $slot)
            ->orderBy('sort_order')
            ->orderBy('id')
            ->get();

        if ($movedId !== null && $movedPosition !== null) {
            $moved = $siblings->firstWhere('id', $movedId);
            if ($moved) {
                $siblings = $siblings->reject(fn ($b) => $b->id === $movedId)->values();
                $siblings->splice(min($movedPosition, $siblings->count()), 0, [$moved]);
            }
        }

        foreach ($siblings->values() as $i => $sibling) {
            if ($sibling->sort_order !== $i) {
                $sibling->updateQuietly(['sort_order' => $i]);
            }
        }
    }

    private function assertCanNest(WebsiteBlock $parent, string $childType): void
    {
        $definition = $parent->definition();

        if (! $definition || ! $definition->allowsChildren()) {
            throw new DomainException('هذا العنصر لا يقبل عناصر بداخله.');
        }

        $allowed = $definition->allowedChildTypes();
        if ($allowed !== null && ! in_array($childType, $allowed, true)) {
            throw new DomainException('نوع العنصر غير مسموح داخل هذه الحاوية.');
        }

        if ($this->depthOf($parent) + 1 >= self::MAX_DEPTH) {
            throw new DomainException('تم بلوغ الحد الأقصى لتداخل العناصر.');
        }
    }

    private function depthOf(WebsiteBlock $block): int
    {
        $depth = 0;
        $cursor = $block;

        while ($cursor->parent_id && $depth < self::MAX_DEPTH + 1) {
            $cursor = $cursor->parent;
            if (! $cursor) {
                break;
            }
            $depth++;
        }

        return $depth;
    }

    private function isDescendantOf(WebsiteBlock $candidate, WebsiteBlock $ancestor): bool
    {
        if ($candidate->id === $ancestor->id) {
            return true;
        }

        $cursor = $candidate;
        $guard = 0;

        while ($cursor->parent_id && $guard++ < self::MAX_DEPTH + 1) {
            if ($cursor->parent_id === $ancestor->id) {
                return true;
            }
            $cursor = $cursor->parent;
            if (! $cursor) {
                break;
            }
        }

        return false;
    }
}
