<?php

namespace App\Domain\Scheduling\Services;

use App\Domain\Facility\Models\Facility;
use App\Domain\Facility\Models\SpaceReservation;
use App\Domain\Scheduling\Models\Assignment;
use App\Domain\Training\Models\TrainingGroup;
use App\Domain\Training\Models\TrainingSchedule;
use App\Domain\Training\Models\TrainingSession;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;

class ScheduleConflictService
{
    /**
     * Check ALL conflicts for a trainer at a given date+time.
     * Returns array of conflict objects describing what blocks them.
     *
     * Pass $branchId — the branch the assignment is being made in — to detect
     * conflicts across every branch. A trainer cannot be in two branches at
     * once, so a booking that clashes with one at another branch is a real
     * clash and hiding it books a person who is already teaching. What must not
     * cross the boundary is the other branch's *content*: the conflicting
     * group's name is replaced with a neutral label, so the scheduler learns
     * "busy elsewhere" without learning who.
     *
     * Without $branchId the reads stay branch-scoped, because there would be no
     * way to tell which rows to redact.
     */
    public function checkTrainerConflicts(
        int $trainerId,
        string $date,
        string $startTime,
        string $endTime,
        ?int $excludeScheduleId = null,
        ?int $excludeSessionId = null,
        ?int $branchId = null
    ): array {
        if ($branchId === null) {
            return $this->collectTrainerConflicts(
                $trainerId, $date, $startTime, $endTime, $excludeScheduleId, $excludeSessionId, null
            );
        }

        return TrainingSchedule::acrossBranches(fn () => $this->collectTrainerConflicts(
            $trainerId, $date, $startTime, $endTime, $excludeScheduleId, $excludeSessionId, $branchId
        ));
    }

    /**
     * The label to show for a conflicting row.
     *
     * Out-of-branch rows keep their time and their block, and lose their name.
     */
    private function conflictLabel(?Model $owner, ?int $branchId, string $fallback = ''): string
    {
        if ($owner === null) {
            return $fallback;
        }

        $ownerBranchId = $owner->getAttribute('branch_id');

        if ($branchId !== null && $ownerBranchId !== null && (int) $ownerBranchId !== (int) $branchId) {
            return __('مجموعة في فرع آخر');
        }

        return $owner->name_ar ?? $owner->name ?? $fallback;
    }

    private function collectTrainerConflicts(
        int $trainerId,
        string $date,
        string $startTime,
        string $endTime,
        ?int $excludeScheduleId,
        ?int $excludeSessionId,
        ?int $branchId
    ): array {
        $dayOfWeek = Carbon::parse($date)->dayOfWeek;
        $conflicts = [];

        // 1. Check TrainingSchedule: trainer is assigned (primary or assistant) at overlapping time
        $scheduleConflicts = TrainingSchedule::where('is_active', true)
            ->where('day_of_week', $dayOfWeek)
            ->where('start_time', '<', $endTime)
            ->where('end_time', '>', $startTime)
            ->where(function ($q) use ($trainerId) {
                $q->where('trainer_id', $trainerId)
                  ->orWhere('assistant_trainer_id', $trainerId);
            })
            ->when($excludeScheduleId, fn ($q) => $q->where('id', '!=', $excludeScheduleId))
            ->where(function ($q) use ($date) {
                $q->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_from')->orWhere('effective_from', '<=', $date);
                })->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_until')->orWhere('effective_until', '>=', $date);
                });
            })
            ->with('group')
            ->get();

        foreach ($scheduleConflicts as $schedule) {
            $role = $schedule->trainer_id === $trainerId ? 'مدرب أساسي' : 'مدرب مساعد';
            $label = $this->conflictLabel($schedule->group, $branchId, __('مجموعة غير معروفة'));
            $conflicts[] = [
                'type' => 'schedule',
                'source' => 'training_schedule',
                'id' => $schedule->id,
                'title' => $label,
                'role' => $role,
                'time' => substr($schedule->start_time, 0, 5) . ' - ' . substr($schedule->end_time, 0, 5),
                'message' => __('المدرب مسؤول عن') . ' "' . $label . '" ' . __('كـ') . $role,
            ];
        }

        // 2. Check TrainingSession: trainer assigned to a specific session on this date
        $sessionConflicts = TrainingSession::where('session_date', $date)
            ->whereIn('status', ['scheduled', 'in_progress'])
            ->where('start_time', '<', $endTime)
            ->where('end_time', '>', $startTime)
            ->where(function ($q) use ($trainerId) {
                $q->where('trainer_id', $trainerId)
                  ->orWhere('assistant_trainer_id', $trainerId);
            })
            ->when($excludeSessionId, fn ($q) => $q->where('id', '!=', $excludeSessionId))
            ->with('group')
            ->get();

        foreach ($sessionConflicts as $session) {
            $role = $session->trainer_id === $trainerId ? 'مدرب أساسي' : 'مدرب مساعد';
            $label = $this->conflictLabel($session->group, $branchId, __('جلسة'));
            $conflicts[] = [
                'type' => 'session',
                'source' => 'training_session',
                'id' => $session->id,
                'title' => $label,
                'role' => $role,
                'time' => substr($session->start_time, 0, 5) . ' - ' . substr($session->end_time, 0, 5),
                'message' => __('المدرب مكلّف بجلسة') . ' "' . $label . '" ' . __('في هذا الوقت'),
            ];
        }

        // 3. Check TrainingGroup.head_trainer_id — if trainer is head of a group
        //    that has schedules at this time
        $headGroupConflicts = TrainingSchedule::where('is_active', true)
            ->where('day_of_week', $dayOfWeek)
            ->where('start_time', '<', $endTime)
            ->where('end_time', '>', $startTime)
            ->when($excludeScheduleId, fn ($q) => $q->where('id', '!=', $excludeScheduleId))
            ->where(function ($q) use ($date) {
                $q->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_from')->orWhere('effective_from', '<=', $date);
                })->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_until')->orWhere('effective_until', '>=', $date);
                });
            })
            ->whereNull('trainer_id') // Only if schedule doesn't have its own trainer
            ->whereHas('group', fn ($q) => $q->where('head_trainer_id', $trainerId))
            ->with('group')
            ->get();

        foreach ($headGroupConflicts as $schedule) {
            $label = $this->conflictLabel($schedule->group, $branchId);
            $conflicts[] = [
                'type' => 'head_trainer',
                'source' => 'group_head_trainer',
                'id' => $schedule->id,
                'title' => $label,
                'role' => 'مدرب رئيسي للمجموعة',
                'time' => substr($schedule->start_time, 0, 5) . ' - ' . substr($schedule->end_time, 0, 5),
                'message' => __('المدرب هو المدرب الرئيسي لمجموعة') . ' "' . $label . '" ' . __('المجدولة في نفس الوقت'),
            ];
        }

        // 4. Check formal Assignment model
        $assignmentConflicts = Assignment::where('user_id', $trainerId)
            ->where('status', 'active')
            ->where(function ($q) use ($date) {
                $q->where(function ($q2) use ($date) {
                    $q2->whereNull('start_date')->orWhere('start_date', '<=', $date);
                })->where(function ($q2) use ($date) {
                    $q2->whereNull('end_date')->orWhere('end_date', '>=', $date);
                });
            })
            ->where(function ($q) use ($startTime, $endTime) {
                $q->where(function ($q2) use ($startTime, $endTime) {
                    $q2->whereNotNull('start_time')
                       ->where('start_time', '<', $endTime)
                       ->where('end_time', '>', $startTime);
                })->orWhereNull('start_time'); // Full-day assignments
            })
            ->where(function ($q) use ($dayOfWeek) {
                $q->whereNull('schedule_days')
                  ->orWhereJsonContains('schedule_days', $dayOfWeek);
            })
            ->get();

        foreach ($assignmentConflicts as $assignment) {
            $assignableName = $this->conflictLabel($assignment->assignable, $branchId);
            $conflicts[] = [
                'type' => 'assignment',
                'source' => 'formal_assignment',
                'id' => $assignment->id,
                'title' => $assignableName ?: $assignment->role_label,
                'role' => $assignment->role_label ?? $assignment->scope,
                'time' => $assignment->start_time
                    ? (substr($assignment->start_time, 0, 5) . ' - ' . substr($assignment->end_time, 0, 5))
                    : __('طوال اليوم'),
                'message' => __('تعيين رسمي:') . ' ' . $assignment->role_label . ' — ' . $assignableName,
            ];
        }

        return $conflicts;
    }

    /**
     * Check ALL conflicts for a training group at a given date+time+facility.
     * A group can't be in two places at once.
     */
    public function checkGroupConflicts(
        int $groupId,
        string $date,
        string $startTime,
        string $endTime,
        ?int $excludeScheduleId = null
    ): array {
        $dayOfWeek = Carbon::parse($date)->dayOfWeek;
        $conflicts = [];

        // 1. Check if group already has a schedule at this time (different facility or segments)
        $scheduleConflicts = TrainingSchedule::where('training_group_id', $groupId)
            ->where('is_active', true)
            ->where('day_of_week', $dayOfWeek)
            ->where('start_time', '<', $endTime)
            ->where('end_time', '>', $startTime)
            ->when($excludeScheduleId, fn ($q) => $q->where('id', '!=', $excludeScheduleId))
            ->where(function ($q) use ($date) {
                $q->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_from')->orWhere('effective_from', '<=', $date);
                })->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_until')->orWhere('effective_until', '>=', $date);
                });
            })
            ->with('facility')
            ->get();

        foreach ($scheduleConflicts as $schedule) {
            $conflicts[] = [
                'type' => 'group_schedule',
                'source' => 'training_schedule',
                'id' => $schedule->id,
                'facility' => $schedule->facility?->name_ar ?? '',
                'time' => substr($schedule->start_time, 0, 5) . ' - ' . substr($schedule->end_time, 0, 5),
                'message' => __('المجموعة مجدولة بالفعل في') . ' "' . ($schedule->facility?->name_ar ?? '') . '" ' . __('في نفس الوقت'),
            ];
        }

        // 2. Check if group has a session on this specific date at this time
        $sessionConflicts = TrainingSession::where('training_group_id', $groupId)
            ->where('session_date', $date)
            ->whereIn('status', ['scheduled', 'in_progress'])
            ->where('start_time', '<', $endTime)
            ->where('end_time', '>', $startTime)
            ->with('facility')
            ->get();

        foreach ($sessionConflicts as $session) {
            $conflicts[] = [
                'type' => 'group_session',
                'source' => 'training_session',
                'id' => $session->id,
                'facility' => $session->facility?->name_ar ?? '',
                'time' => substr($session->start_time, 0, 5) . ' - ' . substr($session->end_time, 0, 5),
                'message' => __('المجموعة لديها جلسة محددة في') . ' "' . ($session->facility?->name_ar ?? '') . '"',
            ];
        }

        // 3. Check confirmed space reservations for this group's schedule
        $reservationConflicts = SpaceReservation::where('reservation_date', $date)
            ->where('status', 'confirmed')
            ->where('start_time', '<', $endTime)
            ->where('end_time', '>', $startTime)
            ->where('reservable_type', TrainingSchedule::class)
            ->whereIn('reservable_id', function ($q) use ($groupId) {
                $q->select('id')
                  ->from('training_schedules')
                  ->where('training_group_id', $groupId);
            })
            ->with('facility')
            ->get();

        foreach ($reservationConflicts as $reservation) {
            // Only flag if it's a DIFFERENT facility than what we're assigning to
            $conflicts[] = [
                'type' => 'group_reservation',
                'source' => 'space_reservation',
                'id' => $reservation->id,
                'facility' => $reservation->facility?->name_ar ?? '',
                'time' => substr($reservation->start_time, 0, 5) . ' - ' . substr($reservation->end_time, 0, 5),
                'message' => __('المجموعة لديها حجز مؤكد في') . ' "' . ($reservation->facility?->name_ar ?? '') . '"',
            ];
        }

        return $conflicts;
    }

    /**
     * Get the default trainer(s) for a group at a given time.
     * Returns trainers who will be automatically committed when the group is scheduled.
     */
    public function getImpliedTrainers(
        int $groupId,
        string $date,
        string $startTime,
        string $endTime,
        ?int $branchId = null
    ): array {
        $dayOfWeek = Carbon::parse($date)->dayOfWeek;

        // The group id arrives from the scheduling form. Named explicitly so
        // the refusal holds outside a scoped request too: another branch's
        // group must not be schedulable from here.
        $group = TrainingGroup::with('headTrainer')
            ->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
            ->find($groupId);

        if (!$group) return [];

        $trainers = [];

        // Check if there's already a schedule with explicit trainer
        $schedule = TrainingSchedule::where('training_group_id', $groupId)
            ->where('is_active', true)
            ->where('day_of_week', $dayOfWeek)
            ->where('start_time', '<', $endTime)
            ->where('end_time', '>', $startTime)
            ->where(function ($q) use ($date) {
                $q->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_from')->orWhere('effective_from', '<=', $date);
                })->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_until')->orWhere('effective_until', '>=', $date);
                });
            })
            ->first();

        if ($schedule) {
            if ($schedule->trainer_id) {
                $trainers[] = [
                    'id' => $schedule->trainer_id,
                    'source' => 'schedule',
                    'role' => 'مدرب أساسي',
                ];
            }
            if ($schedule->assistant_trainer_id) {
                $trainers[] = [
                    'id' => $schedule->assistant_trainer_id,
                    'source' => 'schedule',
                    'role' => 'مدرب مساعد',
                ];
            }
        }

        // If no schedule-level trainer, fall back to group head trainer
        if (empty($trainers) && $group->head_trainer_id) {
            $trainers[] = [
                'id' => $group->head_trainer_id,
                'source' => 'group_head',
                'role' => 'مدرب رئيسي للمجموعة',
            ];
        }

        return $trainers;
    }

    /**
     * Full validation before assignment. Returns all issues (blocking + warnings).
     */
    public function validateAssignment(
        string $entityType, // 'group' or 'trainer'
        int $entityId,
        int $facilityId,
        string $date,
        string $startTime,
        string $endTime,
        array $segmentIds,
        ?int $excludeScheduleId = null,
        ?int $branchId = null
    ): array {
        $errors = [];
        $warnings = [];

        // The branch being scheduled in. Taken from the facility when the
        // caller did not say — a facility belongs to exactly one branch, and
        // that is the branch this assignment lands in. It is what turns the
        // trainer check into a cross-branch one (a trainer cannot be in two
        // branches at once) while keeping the other branch's group names out
        // of the message.
        $branchId ??= Facility::find($facilityId)?->branch_id;

        if ($entityType === 'group') {
            // Check group conflicts (can't be in two places)
            $groupConflicts = $this->checkGroupConflicts($entityId, $date, $startTime, $endTime, $excludeScheduleId);
            foreach ($groupConflicts as $c) {
                // If same facility, it might just be a re-assignment — warning not error
                if (isset($c['facility']) && $c['source'] === 'space_reservation') {
                    $warnings[] = $c;
                } else {
                    $errors[] = $c;
                }
            }

            // Check implied trainers for conflicts
            $impliedTrainers = $this->getImpliedTrainers($entityId, $date, $startTime, $endTime, $branchId);
            foreach ($impliedTrainers as $trainer) {
                $trainerConflicts = $this->checkTrainerConflicts(
                    $trainer['id'],
                    $date,
                    $startTime,
                    $endTime,
                    $excludeScheduleId,
                    null,
                    $branchId
                );
                foreach ($trainerConflicts as $tc) {
                    $warnings[] = array_merge($tc, [
                        'implied_trainer' => true,
                        'trainer_role' => $trainer['role'],
                        'trainer_id' => $trainer['id'],
                    ]);
                }
            }
        } elseif ($entityType === 'trainer') {
            $trainerConflicts = $this->checkTrainerConflicts(
                $entityId, $date, $startTime, $endTime, $excludeScheduleId, null, $branchId
            );
            foreach ($trainerConflicts as $c) {
                $errors[] = $c;
            }
        }

        return [
            'valid' => empty($errors),
            'errors' => $errors,
            'warnings' => $warnings,
        ];
    }

    /**
     * Get all occupied time ranges for a trainer on a given date.
     * Useful for showing trainer availability in the UI.
     */
    public function getTrainerOccupiedSlots(int $trainerId, string $date): array
    {
        $dayOfWeek = Carbon::parse($date)->dayOfWeek;
        $slots = [];

        // From schedules
        $schedules = TrainingSchedule::where('is_active', true)
            ->where('day_of_week', $dayOfWeek)
            ->where(function ($q) use ($trainerId) {
                $q->where('trainer_id', $trainerId)
                  ->orWhere('assistant_trainer_id', $trainerId);
            })
            ->where(function ($q) use ($date) {
                $q->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_from')->orWhere('effective_from', '<=', $date);
                })->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_until')->orWhere('effective_until', '>=', $date);
                });
            })
            ->with('group')
            ->get();

        foreach ($schedules as $s) {
            $slots[] = [
                'start' => substr($s->start_time, 0, 5),
                'end' => substr($s->end_time, 0, 5),
                'title' => $s->group?->name_ar ?? '',
                'type' => 'schedule',
            ];
        }

        // From head_trainer groups
        $headSchedules = TrainingSchedule::where('is_active', true)
            ->where('day_of_week', $dayOfWeek)
            ->whereNull('trainer_id')
            ->whereHas('group', fn ($q) => $q->where('head_trainer_id', $trainerId))
            ->where(function ($q) use ($date) {
                $q->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_from')->orWhere('effective_from', '<=', $date);
                })->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_until')->orWhere('effective_until', '>=', $date);
                });
            })
            ->with('group')
            ->get();

        foreach ($headSchedules as $s) {
            $slots[] = [
                'start' => substr($s->start_time, 0, 5),
                'end' => substr($s->end_time, 0, 5),
                'title' => $s->group?->name_ar ?? '',
                'type' => 'head_trainer',
            ];
        }

        // From sessions
        $sessions = TrainingSession::where('session_date', $date)
            ->whereIn('status', ['scheduled', 'in_progress'])
            ->where(function ($q) use ($trainerId) {
                $q->where('trainer_id', $trainerId)
                  ->orWhere('assistant_trainer_id', $trainerId);
            })
            ->with('group')
            ->get();

        foreach ($sessions as $s) {
            $slots[] = [
                'start' => substr($s->start_time, 0, 5),
                'end' => substr($s->end_time, 0, 5),
                'title' => $s->group?->name_ar ?? '',
                'type' => 'session',
            ];
        }

        // Sort by start time
        usort($slots, fn ($a, $b) => strcmp($a['start'], $b['start']));

        return $slots;
    }

    /**
     * Get all occupied time ranges for a group on a given date.
     */
    public function getGroupOccupiedSlots(int $groupId, string $date): array
    {
        $dayOfWeek = Carbon::parse($date)->dayOfWeek;
        $slots = [];

        $schedules = TrainingSchedule::where('training_group_id', $groupId)
            ->where('is_active', true)
            ->where('day_of_week', $dayOfWeek)
            ->where(function ($q) use ($date) {
                $q->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_from')->orWhere('effective_from', '<=', $date);
                })->where(function ($q2) use ($date) {
                    $q2->whereNull('effective_until')->orWhere('effective_until', '>=', $date);
                });
            })
            ->with('facility')
            ->get();

        foreach ($schedules as $s) {
            $slots[] = [
                'start' => substr($s->start_time, 0, 5),
                'end' => substr($s->end_time, 0, 5),
                'facility' => $s->facility?->name_ar ?? '',
                'type' => 'schedule',
            ];
        }

        $sessions = TrainingSession::where('training_group_id', $groupId)
            ->where('session_date', $date)
            ->whereIn('status', ['scheduled', 'in_progress'])
            ->with('facility')
            ->get();

        foreach ($sessions as $s) {
            $slots[] = [
                'start' => substr($s->start_time, 0, 5),
                'end' => substr($s->end_time, 0, 5),
                'facility' => $s->facility?->name_ar ?? '',
                'type' => 'session',
            ];
        }

        usort($slots, fn ($a, $b) => strcmp($a['start'], $b['start']));

        return $slots;
    }

    /**
     * Check if a time range overlaps with facility operating hours.
     */
    public function isWithinOperatingHours(string $startTime, string $endTime, string $opStart, string $opEnd): bool
    {
        return $startTime >= $opStart && $endTime <= $opEnd;
    }

    /**
     * Get minimum gap between consecutive sessions (configurable per academy, default 0).
     */
    public function getMinGapMinutes(): int
    {
        return (int) (app('current_academy')?->getSetting('min_session_gap_minutes') ?? 0);
    }
}
