<?php

namespace App\Livewire\Programs;

use App\Domain\HR\Services\TrainerWorkloadService;
use App\Domain\Inventory\Models\Product;
use App\Domain\Pricing\Models\BasePrice;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\Activity;
use App\Domain\Training\Models\TrainingProgram;
use App\Domain\Training\Services\TrainingProgramService;
use App\Domain\Shared\Exceptions\DomainException;
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Component;

#[Layout('layouts.app')]
#[Title('إدارة البرنامج')]
class ProgramForm extends Component
{
    use UsesBranchScope;

    // Which programme is being edited, and whether this is an edit at all, are
    // decided by the route and mount(). Neither is bound to anything in the
    // view, so a value arriving from the browser could only be tampering.
    #[Locked]
    public ?TrainingProgram $program = null;

    #[Locked]
    public bool $editing = false;

    public string $name = '';
    public string $name_ar = '';
    public string $slug = '';
    public ?int $activity_id = null;
    public ?int $branch_id = null;
    public ?int $default_trainer_id = null;
    public string $description = '';
    public string $description_ar = '';
    public string $skill_level = 'all';
    public ?int $age_min = null;
    public ?int $age_max = null;
    public string $gender = 'all';
    public int $sessions_per_week = 2;
    public int $session_duration_minutes = 60;
    public ?int $total_sessions = null;
    public ?int $program_duration_weeks = null;
    public int $min_participants = 1;
    public int $max_participants = 20;
    public bool $allow_waitlist = true;
    public ?int $attendance_requirement_percent = null;
    public bool $assessment_required = false;
    public bool $registration_open = true;
    public ?string $registration_deadline = null;
    public ?string $program_start_date = null;
    public ?string $program_end_date = null;
    public string $renewal_policy = 'manual_renew';
    public ?string $billing_cycle = 'monthly';
    public ?int $billing_day = 1;
    public string $cancellation_policy = '';
    public string $refund_policy = '';

    // The form has no status field — new programmes open active and an edit
    // keeps whatever mount() read. Left settable it would be an unvalidated
    // enum posted straight into a column with a CHECK constraint.
    #[Locked]
    public string $status = 'active';

    public bool $featured = false;

    // Pricing
    public string $member_price = '';
    public string $non_member_price = '';

    /**
     * Product ids that come bundled with this programme. A player cannot be
     * considered properly enrolled without them, and the group view flags
     * anyone missing one.
     *
     * @var array<int>
     */
    public array $bundled_product_ids = [];

    public function mount(?TrainingProgram $program = null): void
    {
        if ($program && $program->exists) {
            // The route middleware gates the page; this gates the component,
            // which Livewire can also reach without passing through it.
            $this->authorize('programs.update');

            $this->program = $program;
            $this->editing = true;
            $this->bundled_product_ids = $program->bundledProducts()->pluck('products.id')->all();
            $this->name = $program->name ?? '';
            $this->name_ar = $program->name_ar;
            $this->slug = $program->slug ?? '';
            $this->activity_id = $program->activity_id;
            $this->branch_id = $program->branch_id;
            $this->default_trainer_id = $program->default_trainer_id;
            $this->description = $program->description ?? '';
            $this->description_ar = $program->description_ar ?? '';
            $this->skill_level = $program->skill_level ?? 'all';
            $this->age_min = $program->age_min;
            $this->age_max = $program->age_max;
            $this->gender = $program->gender ?? 'all';
            $this->sessions_per_week = $program->sessions_per_week;
            $this->session_duration_minutes = $program->session_duration_minutes;
            $this->total_sessions = $program->total_sessions;
            $this->program_duration_weeks = $program->program_duration_weeks;
            $this->min_participants = $program->min_participants;
            $this->max_participants = $program->max_participants;
            $this->allow_waitlist = $program->allow_waitlist;
            $this->attendance_requirement_percent = $program->attendance_requirement_percent;
            $this->assessment_required = $program->assessment_required;
            $this->registration_open = $program->registration_open;
            $this->registration_deadline = $program->registration_deadline?->format('Y-m-d');
            $this->program_start_date = $program->program_start_date?->format('Y-m-d');
            $this->program_end_date = $program->program_end_date?->format('Y-m-d');
            $this->renewal_policy = $program->renewal_policy->value ?? $program->renewal_policy;
            $this->billing_cycle = $program->billing_cycle;
            $this->billing_day = $program->billing_day;
            $this->cancellation_policy = $program->cancellation_policy ?? '';
            $this->refund_policy = $program->refund_policy ?? '';
            $this->status = $program->status->value ?? $program->status;
            $this->featured = $program->featured;

            // Load existing prices
            $prices = BasePrice::where('priceable_type', TrainingProgram::class)
                ->where('priceable_id', $program->id)
                ->where('is_active', true)
                ->get();

            foreach ($prices as $price) {
                $meta = $price->metadata ?? [];
                if (($meta['membership_type'] ?? null) === 'member') {
                    $this->member_price = number_format($price->amount / 100, 2, '.', '');
                } elseif (($meta['membership_type'] ?? null) === 'non_member') {
                    $this->non_member_price = number_format($price->amount / 100, 2, '.', '');
                }
            }
        } else {
            $this->authorize('programs.create');

            // A programme belongs to exactly one branch — a branch runs its
            // own programmes at its own prices. In all-branches mode there is
            // no active branch to inherit, so the picker asks and the rule
            // below refuses to save without an answer: a programme with no
            // branch is not academy-wide, it is invisible from every branch.
            $this->branch_id = $this->getActiveBranchId();
        }
    }

    public function rules(): array
    {
        return [
            'name' => 'required|string|max:255',
            'name_ar' => 'required|string|max:255',
            'slug' => 'nullable|string|max:100',
            'activity_id' => 'required|exists:activities,id',
            // Not exists:branches,id — a raw exists rule accepts every branch
            // in the academy, and this property is settable from the browser.
            // selectableBranches() is one entry unless the user is genuinely
            // in all-branches mode.
            // Required, not nullable: TrainingProgram is strictly scoped, so a
            // null branch would hide the programme from every branch at once.
            'branch_id' => ['required', Rule::in($this->selectableBranches()->pluck('id')->all())],
            'default_trainer_id' => ['nullable', Rule::in($this->trainerOptions()->pluck('id')->all())],
            'description' => 'nullable|string',
            'description_ar' => 'nullable|string',
            'skill_level' => 'required|in:all,beginner,intermediate,advanced,professional',
            'age_min' => 'nullable|integer|min:1|max:99',
            'age_max' => 'nullable|integer|min:1|max:99',
            'gender' => 'required|in:all,male,female',
            'sessions_per_week' => 'required|integer|min:1|max:7',
            'session_duration_minutes' => 'required|integer|min:15|max:240',
            'total_sessions' => 'nullable|integer|min:1',
            'program_duration_weeks' => 'nullable|integer|min:1|max:104',
            'min_participants' => 'required|integer|min:1',
            'max_participants' => 'required|integer|min:1',
            'allow_waitlist' => 'boolean',
            'attendance_requirement_percent' => 'nullable|integer|min:0|max:100',
            'assessment_required' => 'boolean',
            'registration_open' => 'boolean',
            'registration_deadline' => 'nullable|date',
            'program_start_date' => 'nullable|date',
            'program_end_date' => 'nullable|date|after_or_equal:program_start_date',
            'renewal_policy' => 'required|in:auto_renew,manual_renew,one_time',
            'billing_cycle' => 'nullable|required_unless:renewal_policy,one_time|in:monthly,quarterly,semi_annual,annual,per_duration',
            'billing_day' => 'nullable|integer|min:1|max:28',
            'cancellation_policy' => 'nullable|string',
            'refund_policy' => 'nullable|string',
            'featured' => 'boolean',
            'member_price' => 'nullable|numeric|min:0',
            'non_member_price' => 'nullable|numeric|min:0',
            // The checkbox list is filtered by Product's branch scope, but the
            // array itself is posted from the browser, so the ids are checked
            // again here, against the products this branch actually sells.
            'bundled_product_ids' => ['array'],
            'bundled_product_ids.*' => ['integer', $this->productExistsRule()],
        ];
    }

    /**
     * A product this branch may bundle: one of its own, or one from the
     * academy-wide catalogue.
     */
    private function productExistsRule(): \Illuminate\Validation\Rules\Exists
    {
        $rule = Rule::exists('products', 'id');
        $branchId = $this->getActiveBranchId();

        if ($branchId !== null) {
            $rule->where(fn ($q) => $q->where('branch_id', $branchId));
        }

        return $rule;
    }

    /**
     * Users who may be the default trainer of a programme in this branch.
     *
     * users has no branch_id — a user's branch lives on the role_user pivot —
     * so BranchScope cannot reach this query and the filter is written out. A
     * null pivot branch means the user is not pinned anywhere and works for
     * every branch; hard-filtering those would empty the picker in every
     * academy that never filled the pivot in.
     */
    private function branchTrainers(): Collection
    {
        $branchId = $this->getActiveBranchId();

        return User::query()
            ->where('status', 'active')
            ->whereHas('roles', function ($q) use ($branchId) {
                $q->whereIn('slug', ['trainer', 'head_trainer']);

                if ($branchId !== null) {
                    $q->where(fn ($w) => $w->where('role_user.branch_id', $branchId)
                        ->orWhereNull('role_user.branch_id'));
                }
            })
            ->orderBy('name_ar')
            ->get(['id', 'name', 'name_ar']);
    }

    /**
     * What the default-trainer <select> offers, and the exact set the posted
     * value is validated against.
     *
     * A trainer already saved on the programme stays in the list even when
     * they no longer match — assigned before the roles narrowed, or since
     * moved — so editing a programme's name does not force a change of coach.
     * Read from the persisted programme, never from the browser-settable
     * property, or the validation would just echo whatever was posted.
     */
    private function trainerOptions(): Collection
    {
        $options = $this->branchTrainers();

        $saved = $this->editing ? $this->program?->default_trainer_id : null;

        if ($saved && ! $options->contains('id', $saved)) {
            $current = User::find($saved, ['id', 'name', 'name_ar']);

            if ($current) {
                $options = $options->push($current)->values();
            }
        }

        return $options;
    }

    public function messages(): array
    {
        return [
            'name.required' => 'اسم البرنامج بالإنجليزية مطلوب',
            'name.max' => 'اسم البرنامج بالإنجليزية يجب ألا يتجاوز 255 حرف',
            'name_ar.required' => 'اسم البرنامج بالعربية مطلوب',
            'name_ar.max' => 'اسم البرنامج بالعربية يجب ألا يتجاوز 255 حرف',
            'activity_id.required' => 'النشاط مطلوب',
            'activity_id.exists' => 'النشاط المحدد غير موجود',
            'branch_id.required' => 'يجب اختيار الفرع',
            'branch_id.in' => 'الفرع المحدد غير متاح',
            'default_trainer_id.in' => 'المدرب المحدد غير متاح في هذا الفرع',
            'bundled_product_ids.*.exists' => 'أحد المنتجات المحددة غير متاح في هذا الفرع',
            'skill_level.required' => 'مستوى المهارة مطلوب',
            'skill_level.in' => 'مستوى المهارة غير صالح',
            'age_min.min' => 'الحد الأدنى للعمر يجب أن يكون 1 على الأقل',
            'age_max.min' => 'الحد الأقصى للعمر يجب أن يكون 1 على الأقل',
            'gender.required' => 'الجنس مطلوب',
            'gender.in' => 'قيمة الجنس غير صالحة',
            'sessions_per_week.required' => 'عدد الحصص أسبوعياً مطلوب',
            'sessions_per_week.min' => 'عدد الحصص أسبوعياً يجب أن يكون 1 على الأقل',
            'sessions_per_week.max' => 'عدد الحصص أسبوعياً يجب ألا يتجاوز 7',
            'session_duration_minutes.required' => 'مدة الحصة مطلوبة',
            'session_duration_minutes.min' => 'مدة الحصة يجب أن تكون 15 دقيقة على الأقل',
            'session_duration_minutes.max' => 'مدة الحصة يجب ألا تتجاوز 240 دقيقة',
            'min_participants.required' => 'الحد الأدنى للمشتركين مطلوب',
            'min_participants.min' => 'الحد الأدنى للمشتركين يجب أن يكون 1 على الأقل',
            'max_participants.required' => 'الحد الأقصى للمشتركين مطلوب',
            'max_participants.min' => 'الحد الأقصى للمشتركين يجب أن يكون 1 على الأقل',
            'program_end_date.after_or_equal' => 'تاريخ انتهاء البرنامج يجب أن يكون بعد تاريخ البداية',
            'renewal_policy.required' => 'سياسة التجديد مطلوبة',
            'renewal_policy.in' => 'سياسة التجديد غير صالحة',
            'billing_cycle.required_unless' => 'دورة الفوترة مطلوبة عند تفعيل التجديد',
            'billing_cycle.in' => 'دورة الفوترة غير صالحة',
            'billing_day.integer' => 'يوم الفوترة يجب أن يكون رقم',
            'billing_day.min' => 'يوم الفوترة يجب أن يكون 1 على الأقل',
            'billing_day.max' => 'يوم الفوترة يجب ألا يتجاوز 28',
            'member_price.numeric' => 'سعر العضو يجب أن يكون رقم',
            'member_price.min' => 'سعر العضو يجب ألا يكون سالب',
            'non_member_price.numeric' => 'سعر غير العضو يجب أن يكون رقم',
            'non_member_price.min' => 'سعر غير العضو يجب ألا يكون سالب',
        ];
    }

    public function save(TrainingProgramService $service): void
    {
        $this->validate();

        // Re-assert the branch where the write is built, not only in rules().
        // Outside all-branches mode the picker is not rendered at all, so the
        // only legitimate value is the one mount() loaded — a null posted by
        // hand would otherwise turn a branch's programme into an academy-wide
        // one, and a null on a new programme would be stamped with whatever
        // branch the actor happens to sit in.
        if (! $this->isAllBranches()) {
            $this->branch_id = $this->editing
                ? $this->program?->branch_id
                : $this->getActiveBranchId();
        }

        try {
            $data = [
                'name' => $this->name,
                'name_ar' => $this->name_ar,
                'slug' => $this->slug ?: null,
                'activity_id' => $this->activity_id,
                'branch_id' => $this->branch_id,
                'default_trainer_id' => $this->default_trainer_id,
                'status' => $this->status,
                'description' => $this->description ?: null,
                'description_ar' => $this->description_ar ?: null,
                'skill_level' => $this->skill_level,
                'age_min' => $this->age_min,
                'age_max' => $this->age_max,
                'gender' => $this->gender,
                'sessions_per_week' => $this->sessions_per_week,
                'session_duration_minutes' => $this->session_duration_minutes,
                'total_sessions' => $this->total_sessions,
                'program_duration_weeks' => $this->program_duration_weeks,
                'min_participants' => $this->min_participants,
                'max_participants' => $this->max_participants,
                'allow_waitlist' => $this->allow_waitlist,
                'attendance_requirement_percent' => $this->attendance_requirement_percent,
                'assessment_required' => $this->assessment_required,
                'registration_open' => $this->registration_open,
                'registration_deadline' => $this->registration_deadline ?: null,
                'program_start_date' => $this->program_start_date ?: null,
                'program_end_date' => $this->program_end_date ?: null,
                'renewal_policy' => $this->renewal_policy,
                'billing_cycle' => $this->renewal_policy !== 'one_time' ? $this->billing_cycle : null,
                'billing_day' => $this->billing_cycle === 'monthly' ? $this->billing_day : null,
                'cancellation_policy' => $this->cancellation_policy ?: null,
                'refund_policy' => $this->refund_policy ?: null,
                'featured' => $this->featured,
            ];

            if ($this->editing) {
                $service->update($this->program, $data);
                $this->savePrices($this->program);
                $this->saveBundledProducts($this->program);
                session()->flash('success', __('تم تحديث البرنامج بنجاح'));
            } else {
                $program = $service->create($data, auth()->user());


                $this->savePrices($program);
                $this->saveBundledProducts($program);
                session()->flash('success', __('تم إنشاء البرنامج بنجاح'));
            }

            $this->redirect(route('programs.list'), navigate: true);
        } catch (DomainException $e) {
            session()->flash('error', $e->getMessage());
        }
    }

    /**
     * Sync the bundle. academy_id is written explicitly because the pivot has
     * no model and so no BelongsToAcademy hook to stamp it.
     */
    private function saveBundledProducts(TrainingProgram $program): void
    {
        $academyId = $program->academy_id ?? app('current_academy')?->id;

        $payload = [];
        foreach (array_filter($this->bundled_product_ids) as $productId) {
            $payload[(int) $productId] = [
                'academy_id' => $academyId,
                'is_required' => true,
                'quantity' => 1,
            ];
        }

        // A product from another branch's catalogue is invisible in this form,
        // so syncing from the visible checkboxes alone would silently unbundle
        // it — and the group screen would then stop flagging the players who
        // never bought it. Carry those rows through untouched. This is the one
        // deliberate cross-branch read here, and it only reads back what the
        // programme already has.
        $visibleIds = $program->bundledProducts()->pluck('products.id')->all();

        $hidden = $this->acrossBranches(
            fn () => $program->bundledProducts()->whereNotIn('products.id', $visibleIds)->get()
        );

        foreach ($hidden as $product) {
            $payload[$product->id] = [
                'academy_id' => $academyId,
                'is_required' => $product->pivot->is_required,
                'quantity' => $product->pivot->quantity,
            ];
        }

        $program->bundledProducts()->sync($payload);
    }

    private function savePrices(TrainingProgram $program): void
    {
        $types = [
            'member' => $this->member_price,
            'non_member' => $this->non_member_price,
        ];

        foreach ($types as $membershipType => $priceDisplay) {
            $amount = $priceDisplay ? (int) round((float) $priceDisplay * 100) : null;

            $existing = BasePrice::where('priceable_type', TrainingProgram::class)
                ->where('priceable_id', $program->id)
                ->where('metadata->membership_type', $membershipType)
                ->first();

            if ($amount && $amount > 0) {
                if ($existing) {
                    $existing->update(['amount' => $amount, 'is_active' => true]);
                } else {
                    $price = BasePrice::create([
                        'academy_id' => $program->academy_id,
                        'priceable_type' => TrainingProgram::class,
                        'priceable_id' => $program->id,
                        'branch_id' => $program->branch_id,
                        'name_ar' => $membershipType === 'member' ? 'سعر العضو' : 'سعر غير العضو',
                        'name' => $membershipType === 'member' ? 'Member Price' : 'Non-Member Price',
                        'amount' => $amount,
                        'currency' => 'EGP',
                        'effective_from' => now()->toDateString(),
                        'is_active' => true,
                        'priority' => $membershipType === 'member' ? 1 : 2,
                        'created_by' => auth()->id(),
                        'metadata' => ['membership_type' => $membershipType],
                    ]);

                    // The price is filed against the programme's branch, passed
                    // explicitly above rather than left to BelongsToBranch's
                    // stamp — the actor's active branch and the programme's are
                    // the same in practice, but a price that disagreed with the
                    // thing it prices would be invisible exactly where it is
                    // needed.
                }
            } elseif ($existing) {
                $existing->update(['is_active' => false]);
            }
        }
    }

    public function render()
    {
        $trainers = $this->trainerOptions();

        $workloads = app(TrainerWorkloadService::class)->getWorkloadsForUsers($trainers->pluck('id')->toArray());

        return view('livewire.programs.program-form', [
            // No branch clause on either of these: Product carries BranchScope
            // in shared mode, so this is the branch's own products plus the
            // academy-wide catalogue. Activities are academy-level.
            'availableProducts' => Product::where('is_active', true)
                ->orderBy('name_ar')->get(['id', 'name_ar', 'selling_price']),
            'activities' => Activity::where('is_active', true)->orderBy('name_ar')->get(['id', 'name_ar']),
            'branches' => $this->selectableBranches(),
            'trainers' => $trainers,
            'trainerWorkloads' => $workloads,
        ]);
    }
}
