<?php

namespace App\Domain\Training\Services;

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\Models\TrainingSchedule;
use App\Domain\Training\Support\GuardsEnrolmentDeletion;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class TrainingProgramService
{
    use GuardsEnrolmentDeletion;

    public function create(array $data, User $actor, bool $skipDefaultGroup = false): TrainingProgram
    {
        return DB::transaction(function () use ($data, $actor, $skipDefaultGroup) {
            $slug = Str::limit($data['slug'] ?? Str::slug($data['name']), 90, '');
            $exists = TrainingProgram::withTrashed()->where('slug', $slug)->exists();
            if ($exists) {
                $slug = Str::limit($slug, 85, '') . '-' . Str::random(4);
            }

            $program = TrainingProgram::create(array_merge($data, [
                'slug' => $slug,
                'created_by' => $actor->id,
            ]));

            if (!$skipDefaultGroup) {
                $this->createDefaultGroup($program, $actor);
            }

            return $program;
        });
    }

    private function createDefaultGroup(TrainingProgram $program, User $actor): TrainingGroup
    {
        $programName = $program->name_ar ?: $program->name;
        $baseCode = strtoupper(substr(preg_replace('/[^a-zA-Z0-9]/', '', $program->name ?: $program->name_ar), 0, 6)) . '01';

        $code = $baseCode;
        $attempt = 0;
        while (TrainingGroup::withTrashed()->where('academy_id', $program->academy_id)->where('code', $code)->exists()) {
            $attempt++;
            $code = $baseCode . '-' . $attempt;
        }

        return TrainingGroup::create([
            'academy_id' => $program->academy_id,
            'training_program_id' => $program->id,
            'branch_id' => $program->branch_id,
            'head_trainer_id' => $program->default_trainer_id,
            'name' => "Group {$program->name} 1",
            'name_ar' => "مجموعة {$programName} 1",
            'code' => $code,
            'max_capacity' => $program->max_participants,
            'current_count' => 0,
            'waitlist_count' => 0,
            'status' => 'active',
            'status_changed_at' => now(),
            'start_date' => $program->program_start_date ?? now()->toDateString(),
            'end_date' => $program->program_end_date,
            'created_by' => $actor->id,
        ]);
    }

    /**
     * Put a plain weekly timetable on the programme's default group: which days
     * it trains and between which hours, and nothing else.
     *
     * This is the deliberate counterpart to the visual builder. A programme
     * that meets three afternoons a week does not need a facility, a space, or
     * a placement on the grid to say so, and demanding one is why small clubs
     * end up with no recorded timetable at all. facility_id stays null; the
     * grid can place these rows later without them being recreated.
     *
     * Only facility-less rows are touched. A row the grid has already placed
     * belongs to the grid, and clearing a checkbox here must not silently strip
     * a space reservation off it.
     *
     * Days that fall out of the selection are deactivated rather than deleted —
     * sessions already generated from them point at the row, and attendance
     * hangs off those sessions.
     *
     * @param  array<int>  $days  day_of_week values, 0 = Sunday
     */
    public function syncDefaultGroupSchedule(
        TrainingProgram $program,
        array $days,
        ?string $startTime,
        ?string $endTime,
        ?int $trainerId = null,
    ): void {
        $group = $program->defaultGroup();

        if (! $group) {
            return;
        }

        $days = array_values(array_unique(array_map('intval', $days)));

        DB::transaction(function () use ($group, $program, $days, $startTime, $endTime, $trainerId) {
            $managed = $group->schedules()->whereNull('facility_id')->get();

            foreach ($days as $day) {
                if ($startTime === null || $endTime === null) {
                    continue;
                }

                $row = $managed->firstWhere('day_of_week', $day)
                    ?? new TrainingSchedule([
                        'academy_id' => $group->academy_id,
                        'training_group_id' => $group->id,
                        'day_of_week' => $day,
                    ]);

                $row->fill([
                    'academy_id' => $group->academy_id,
                    'training_group_id' => $group->id,
                    'day_of_week' => $day,
                    'start_time' => $startTime,
                    'end_time' => $endTime,
                    'trainer_id' => $trainerId,
                    'effective_from' => $row->effective_from
                        ?? ($program->program_start_date ?? now()->toDateString()),
                    'effective_until' => $program->program_end_date,
                    'is_active' => true,
                ])->save();
            }

            $managed->whereNotIn('day_of_week', $days)
                ->where('is_active', true)
                ->each(fn (TrainingSchedule $row) => $row->update(['is_active' => false]));
        });
    }

    public function update(TrainingProgram $program, array $data): TrainingProgram
    {
        return DB::transaction(function () use ($program, $data) {
            if (isset($data['slug']) && $data['slug'] !== $program->slug) {
                $exists = TrainingProgram::where('slug', $data['slug'])
                    ->where('id', '!=', $program->id)
                    ->exists();
                if ($exists) {
                    throw new DomainException('هذا المعرف مستخدم بالفعل');
                }
            }
            $program->update($data);
            return $program->fresh();
        });
    }

    public function changeStatus(TrainingProgram $program, string $newStatus): TrainingProgram
    {
        $allowed = match ($program->status->value ?? $program->status) {
            'draft' => ['active'],
            'active' => ['full', 'closed', 'archived'],
            'full' => ['active', 'closed'],
            'closed' => ['archived', 'active'],
            default => [],
        };

        if (!in_array($newStatus, $allowed)) {
            throw new DomainException("لا يمكن تغيير حالة البرنامج من '{$program->status->value}' إلى '{$newStatus}'");
        }

        $program->update(['status' => $newStatus]);
        return $program->fresh();
    }

    /**
     * Delete a programme nobody was ever enrolled in.
     *
     * A programme is not a label you can rewrite over — it is what a paid
     * subscription points at, and a programme holding enrolments is not
     * deletable. See GuardsEnrolmentDeletion for why, and for what it cost the
     * last time it was. Archive it instead (changeStatus → archived): that
     * takes it off every picker while leaving the enrolments, invoices and
     * attendance that reference it intact.
     *
     * The groups are queried without their branch scope for the same reason
     * the enrolment count is: a group in another branch is still a group whose
     * enrolments this delete would destroy, and one hidden by the scope would
     * walk straight past the guard.
     */
    public function delete(TrainingProgram $program): void
    {
        $groupIds = TrainingGroup::withoutBranchScope()
            ->where('training_program_id', $program->id)
            ->pluck('id');

        $this->guardEnrolmentsExist(
            Enrollment::withoutBranchScope()->where(function ($q) use ($program, $groupIds) {
                $q->where('training_program_id', $program->id);
                if ($groupIds->isNotEmpty()) {
                    $q->orWhereIn('training_group_id', $groupIds);
                }
            }),
            'البرنامج',
            'أرشِف البرنامج بدلاً من حذفه',
        );

        DB::transaction(function () use ($program, $groupIds) {
            // Nullify transferred_to_id references from other enrollments pointing to these groups
            if ($groupIds->isNotEmpty()) {
                Enrollment::withoutBranchScope()
                    ->whereIn('transferred_to_id', $groupIds)
                    ->update(['transferred_to_id' => null]);
            }

            foreach (TrainingGroup::withoutBranchScope()->whereIn('id', $groupIds)->get() as $group) {
                $group->sessions()->forceDelete();
                $group->schedules()->delete();
                $group->waitlists()->delete();
                \App\Domain\Scheduling\Models\Assignment::where('assignable_type', TrainingGroup::class)
                    ->where('assignable_id', $group->id)
                    ->delete();
                $group->forceDelete();
            }
            $program->forceDelete();
        });
    }
}
