<?php

namespace App\Domain\Training\Services;

use App\Domain\Scheduling\Models\Assignment;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Training\Models\Enrollment;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Training\Support\GuardsEnrolmentDeletion;
use App\Models\User;
use Illuminate\Support\Facades\DB;

class TrainingGroupService
{
    use GuardsEnrolmentDeletion;

    private const VALID_TRANSITIONS = [
        'forming' => ['active', 'cancelled'],
        'active' => ['full', 'on_hold', 'completed', 'cancelled'],
        'full' => ['active'],
        'on_hold' => ['active'],
        'completed' => [],
        'cancelled' => [],
    ];

    public function create(array $data, User $actor): TrainingGroup
    {
        return DB::transaction(function () use ($data, $actor) {
            $program = TrainingProgram::findOrFail($data['training_program_id']);

            // A group runs one branch's copy of a programme. Programmes are
            // shared — branch_id NULL is the academy-wide catalogue, and those
            // may be run anywhere — but a programme that belongs to one branch
            // cannot have a group in another, or the group shows up on two
            // branches' screens and its invoices are filed against a third.
            if ($program->branch_id && ! empty($data['branch_id'])
                && (int) $data['branch_id'] !== (int) $program->branch_id) {
                throw new DomainException('المجموعة والبرنامج في فرعين مختلفين');
            }

            return TrainingGroup::create(array_merge($data, [
                // Null falls through to the creating hook, which stamps the
                // branch the request is in.
                'branch_id' => $data['branch_id'] ?? $program->branch_id,
                'head_trainer_id' => $data['head_trainer_id'] ?? $program->default_trainer_id,
                'max_capacity' => $data['max_capacity'] ?? $program->max_participants,
                'status' => 'forming',
                'status_changed_at' => now(),
                'created_by' => $actor->id,
            ]));
        });
    }

    public function update(TrainingGroup $group, array $data): TrainingGroup
    {
        return DB::transaction(function () use ($group, $data) {
            if (isset($data['code']) && $data['code'] !== $group->code) {
                // The unique key is (academy_id, code), so the check has to be
                // academy-wide: scoped to one branch it misses a clash in
                // another and the update dies on the constraint instead of
                // showing this message.
                $exists = TrainingGroup::withoutBranchScope()
                    ->where('code', $data['code'])
                    ->where('id', '!=', $group->id)
                    ->exists();
                if ($exists) {
                    throw new DomainException('هذا الكود مستخدم بالفعل');
                }
            }
            $group->update($data);
            return $group->fresh();
        });
    }

    public function changeStatus(TrainingGroup $group, string $newStatus, User $actor): TrainingGroup
    {
        return DB::transaction(function () use ($group, $newStatus, $actor) {
            $currentStatus = $group->status->value ?? $group->status;
            $allowed = self::VALID_TRANSITIONS[$currentStatus] ?? [];

            if (!in_array($newStatus, $allowed)) {
                throw new DomainException("لا يمكن تغيير حالة المجموعة من '{$currentStatus}' إلى '{$newStatus}'");
            }

            $group->update([
                'status' => $newStatus,
                'status_changed_at' => now(),
            ]);

            return $group->fresh();
        });
    }

    /**
     * Delete a group nobody was ever enrolled in.
     *
     * A group holding enrolments is not deletable — see
     * GuardsEnrolmentDeletion for why, and for what it cost the last time it
     * was. Move the players (TransferGroup) or close the group (changeStatus →
     * completed / cancelled); both keep the rows that invoices and attendance
     * point at. Deletion stays for what it is actually for: a group created by
     * mistake.
     */
    public function delete(TrainingGroup $group): void
    {
        $this->guardEnrolmentsExist(
            Enrollment::withoutBranchScope()->where('training_group_id', $group->id),
            'المجموعة',
            'انقل اللاعبين إلى مجموعة أخرى أو غيّر حالة المجموعة إلى «مكتملة»',
        );

        DB::transaction(function () use ($group) {
            // Nullify transferred_to_id references from other enrollments.
            // Deliberately across branches: a transfer may have moved a
            // player to a group in another branch, and enrollments.
            // transferred_to_id has no ON DELETE clause — a reference left
            // behind because the scope hid it would abort the delete with a
            // foreign-key violation.
            Enrollment::withoutBranchScope()
                ->where('transferred_to_id', $group->id)
                ->update(['transferred_to_id' => null]);
            // Sessions cascade-delete their attendance records automatically
            $group->sessions()->forceDelete();
            $group->schedules()->delete();
            $group->waitlists()->delete();
            Assignment::where('assignable_type', TrainingGroup::class)
                ->where('assignable_id', $group->id)
                ->delete();
            $group->forceDelete();
        });
    }

    public function incrementCount(TrainingGroup $group): void
    {
        DB::transaction(function () use ($group) {
            $group->increment('current_count');
            if ($group->fresh()->current_count >= $group->max_capacity) {
                $group->update(['status' => 'full', 'status_changed_at' => now()]);
            }
        });
    }

    public function decrementCount(TrainingGroup $group): void
    {
        DB::transaction(function () use ($group) {
            $group->decrement('current_count');
            if ($group->status->value === 'full' && $group->fresh()->current_count < $group->max_capacity) {
                $group->update(['status' => 'active', 'status_changed_at' => now()]);
            }
        });
    }
}
