<?php

declare(strict_types=1);

namespace App\Http\Requests\Masters;

use App\Models\AssetCategory;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;

/**
 * Hierarchy rules shared by the store and update category requests.
 *
 * These are the rules the database cannot express:
 *
 *   - sibling names must be unique, including at root level, where MySQL's
 *     unique index does not apply because it treats NULL parents as distinct
 *   - the hierarchy is capped at two levels
 *   - a category cannot be its own parent
 *   - a category that already has subcategories cannot itself become one
 *
 * They live in one place because store and update must never disagree about
 * what a valid hierarchy is.
 */
final class AssetCategoryRules
{
    /**
     * @return array<string, mixed>
     */
    public static function normalise(FormRequest $request): array
    {
        $parentId = $request->input('parent_id');
        $code = trim((string) $request->input('code'));

        return [
            'parent_id' => ($parentId === null || $parentId === '') ? null : (int) $parentId,
            'name' => trim((string) $request->input('name')),
            'code' => mb_strtoupper($code),
            'depreciation_years' => self::nullableInt($request->input('depreciation_years')),
            'default_warranty_months' => self::nullableInt($request->input('default_warranty_months')),
            'is_active' => $request->boolean('is_active'),
        ];
    }

    /**
     * @param  AssetCategory|null  $category  the record being edited, or null when creating
     */
    public static function validateHierarchy(
        Validator $validator,
        FormRequest $request,
        ?AssetCategory $category,
    ): void {
        $parentId = $request->input('parent_id');
        $parentId = ($parentId === null || $parentId === '') ? null : (int) $parentId;
        $name = trim((string) $request->input('name'));

        self::validateSiblingName($validator, $parentId, $name, $category);

        if ($parentId === null) {
            return;
        }

        if ($category !== null && $parentId === $category->getKey()) {
            $validator->errors()->add('parent_id', __('A category cannot be its own parent.'));

            return;
        }

        $parent = AssetCategory::query()->find($parentId);

        if ($parent === null) {
            return;
        }

        // Cap the hierarchy: a subcategory cannot itself have a parent.
        if (! $parent->isRoot()) {
            $validator->errors()->add(
                'parent_id',
                __('Categories may be nested only :depth levels deep. Choose a top-level category as the parent.', [
                    'depth' => AssetCategory::MAX_DEPTH,
                ]),
            );
        }

        // The other half of the same cap, from the child's side.
        if ($category !== null && $category->children()->exists()) {
            $validator->errors()->add(
                'parent_id',
                __('This category has subcategories of its own, so it cannot be moved under another category.'),
            );
        }
    }

    /**
     * Sibling names must be distinct.
     *
     * The database unique index on (parent_id, name) covers subcategories but
     * not roots, because MySQL treats NULL parents as distinct values — so two
     * root categories could both be called "Laptop". This closes that gap.
     */
    private static function validateSiblingName(
        Validator $validator,
        ?int $parentId,
        string $name,
        ?AssetCategory $category,
    ): void {
        if ($name === '') {
            return;
        }

        $exists = AssetCategory::query()
            ->where('name', $name)
            ->when($parentId === null,
                fn ($query) => $query->whereNull('parent_id'),
                fn ($query) => $query->where('parent_id', $parentId),
            )
            ->when($category !== null, fn ($query) => $query->whereKeyNot($category?->getKey()))
            ->withTrashed()
            ->exists();

        if (! $exists) {
            return;
        }

        $validator->errors()->add('name', $parentId === null
            ? __('A top-level category with this name already exists. It may be in the deleted list.')
            : __('This parent category already has a subcategory with this name. It may be in the deleted list.'));
    }

    private static function nullableInt(mixed $value): ?int
    {
        if ($value === null || $value === '') {
            return null;
        }

        return (int) $value;
    }
}
