<?php

namespace App\Modules\HR\Departments\Services;

use App\Modules\HR\Departments\DTOs\DepartmentWriteData;
use App\Modules\HR\Departments\Events\DepartmentCreated;
use App\Modules\HR\Departments\Events\DepartmentDeleted;
use App\Modules\HR\Departments\Events\DepartmentUpdated;
use App\Modules\HR\Departments\Models\Department;
use App\Modules\HR\Departments\Repositories\DepartmentRepository;
use App\Support\SystemReferenceAssigner;
use App\Support\SystemReferenceGenerator;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class DepartmentService
{
    public function __construct(
        private readonly DepartmentRepository $repository,
        private readonly Dispatcher $events,
        private readonly SystemReferenceAssigner $references,
    ) {}

    public function list(): Collection
    {
        return $this->repository->allWithParent();
    }

    public function paginateForIndex(Request $request): LengthAwarePaginator
    {
        return $this->repository->paginateForIndex($request);
    }

    /**
     * Return parent-select options, excluding the given department and its descendants
     * to prevent circular hierarchies.
     *
     * @return array<int, string>
     */
    public function parentOptions(?int $excludeId = null): array
    {
        $options = $this->repository->listForSelect();

        if ($excludeId === null) {
            return $options;
        }

        $blocked = array_merge([$excludeId], $this->repository->descendantIds($excludeId));

        return array_diff_key($options, array_flip($blocked));
    }

    public function store(DepartmentWriteData $data): Department
    {
        $this->assertValidParent($data->parentId, null);

        $department = $this->repository->create($data->toArray());
        $this->references->assign($department, SystemReferenceGenerator::DEPARTMENT, 'code');
        $department = $this->repository->findWithParent($department->id);
        $this->events->dispatch(new DepartmentCreated($department));

        return $department;
    }

    public function update(Department $department, DepartmentWriteData $data): Department
    {
        $this->assertValidParent($data->parentId, $department);

        $this->repository->update($department, $data->toArray());
        $department->refresh();
        $department = $this->repository->findWithParent($department->id);
        $this->events->dispatch(new DepartmentUpdated($department));

        return $department;
    }

    public function destroy(Department $department): void
    {
        if ($this->repository->hasChildren($department->id)) {
            throw ValidationException::withMessages([
                'department' => [__('This department has sub-departments and cannot be deleted.')],
            ]);
        }

        $this->events->dispatch(new DepartmentDeleted($department));
        $this->repository->delete($department);
    }

    /**
     * @param  list<int>  $ids
     */
    public function bulkDestroy(array $ids): int
    {
        if ($ids === []) {
            return 0;
        }

        $unique = array_values(array_unique($ids));

        if ($this->repository->countByIds($unique) !== count($unique)) {
            throw ValidationException::withMessages([
                'ids' => [__('One or more departments were not found.')],
            ]);
        }

        foreach ($unique as $id) {
            if ($this->repository->hasChildren($id)) {
                throw ValidationException::withMessages([
                    'ids' => [__('One or more selected departments have sub-departments and cannot be deleted.')],
                ]);
            }
        }

        $deleted = 0;

        foreach ($unique as $id) {
            $department = $this->repository->findOrFail($id);
            $this->events->dispatch(new DepartmentDeleted($department));
            $this->repository->delete($department);
            $deleted++;
        }

        return $deleted;
    }

    private function assertValidParent(?int $parentId, ?Department $current): void
    {
        if ($parentId === null) {
            return;
        }

        if ($current !== null && $parentId === $current->id) {
            throw ValidationException::withMessages([
                'parent_id' => [__('A department cannot be its own parent.')],
            ]);
        }

        $this->repository->findOrFail($parentId);

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

        $descendants = $this->repository->descendantIds($current->id);
        if (in_array($parentId, $descendants, true)) {
            throw ValidationException::withMessages([
                'parent_id' => [__('A department cannot have a sub-department as its parent.')],
            ]);
        }
    }
}
