<?php

namespace App\Livewire\Pricing;

use App\Domain\Participant\Models\Participant;
use App\Domain\Pricing\Enums\AdjustmentType;
use App\Domain\Pricing\Enums\PricingRuleType;
use App\Domain\Pricing\Models\PricingRule;
use App\Domain\Pricing\Services\PricingService;
use App\Domain\Pricing\Support\ConditionSchema;
use App\Domain\Pricing\Support\DiscountRecipe;
use App\Domain\Pricing\Support\Percent;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Traits\UsesBranchScope;
use App\Domain\Training\Models\TrainingProgram;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Component;

/**
 * Discount builder — recipe gallery, then a sentence you can read out loud.
 *
 * Replaces the five-step column editor. The author never meets rule_type,
 * basis points, morph classes or a conditions JSON blob.
 */
#[Layout('layouts.app')]
#[Title('إنشاء خصم')]
class CreatePricingRuleWizard extends Component
{
    use UsesBranchScope;

    public string $stage = 'recipes';   // recipes | build | done

    public ?string $recipeKey = null;

    #[Locked]
    public ?int $createdRuleId = null;

    // ─── The sentence ─────────────────────────────────────────────
    public string $nameAr = '';
    public string $ruleType = '';
    public string $adjustmentType = 'percentage_discount';
    public string $amount = '';               // percent, or pounds for fixed types
    public array $conditions = [];
    public array $branchIds = [];

    /**
     * Nothing in create-pricing-rule-wizard.blade.php binds either of these —
     * the wizard has no target picker — yet save() persists them straight onto
     * the rule. Unlocked, they were the one field on this screen a crafted
     * payload could point at another branch's programme.
     */
    #[Locked]
    public ?string $targetType = null;

    #[Locked]
    public ?int $targetId = null;

    // ─── Behaviour ────────────────────────────────────────────────
    public bool $isStackable = false;         // best-of-one is the normal case
    public bool $isPinned = false;
    public ?string $pinIcon = null;
    public int $displayOrder = 0;
    public bool $isManual = false;
    public bool $requiresApproval = false;
    public ?int $minRoleLevel = null;

    public string $effectiveFrom = '';
    public ?string $effectiveTo = null;
    public bool $isActive = true;
    public ?int $usageLimit = null;
    public ?int $maxDiscountPercent = null;

    // ─── Simulator ────────────────────────────────────────────────
    public ?int $simParticipantId = null;
    public ?int $simProgramId = null;
    public array $simulation = [];
    public array $audience = [];

    public function mount(): void
    {
        $this->authorize('pricing.create');
        $this->effectiveFrom = now()->toDateString();
        // Default to the branch you are standing in — academy-wide should be
        // a deliberate act, not what you get by not thinking about it.
        $this->branchIds = array_values(array_filter([$this->getActiveBranchId()]));
    }

    // ─── Stage 1: recipes ─────────────────────────────────────────

    public function chooseRecipe(string $key): void
    {
        $fill = DiscountRecipe::prefill($key);
        if (! $fill) {
            return;
        }

        $this->recipeKey        = $fill['recipe_key'];
        $this->nameAr           = $fill['name_ar'];
        $this->ruleType         = $fill['rule_type'];
        $this->adjustmentType   = $fill['adjustment_type'];
        $this->amount           = (string) $fill['percent'];
        $this->conditions       = $fill['conditions'];
        $this->isStackable      = $fill['is_stackable'];
        $this->isPinned         = $fill['is_pinned'];
        $this->pinIcon          = $fill['pin_icon'];
        $this->isManual         = $fill['is_manual'];
        $this->requiresApproval = $fill['requires_approval'];

        $this->stage = 'build';
        $this->refreshPreview();
    }

    public function startBlank(): void
    {
        $this->recipeKey = null;
        $this->nameAr = '';
        $this->ruleType = 'age';
        $this->conditions = [];
        $this->amount = '';
        $this->stage = 'build';
    }

    public function backToRecipes(): void
    {
        $this->stage = 'recipes';
        $this->simulation = [];
        $this->audience = [];
    }

    // ─── Live preview ─────────────────────────────────────────────

    public function updated(string $field): void
    {
        if (str_starts_with($field, 'conditions') || in_array($field, ['ruleType', 'amount', 'adjustmentType'], true)) {
            $this->refreshPreview();
        }

        if (in_array($field, ['simParticipantId', 'simProgramId'], true)) {
            $this->refreshPreview();
        }
    }

    public function updatedRuleType(): void
    {
        $this->conditions = [];
    }

    /** The whole rule as one readable Arabic sentence. */
    public function getSentenceProperty(): string
    {
        $value = $this->amount !== '' ? $this->amount : '—';
        $verb = match ($this->adjustmentType) {
            'percentage_increase' => "زِد السعر {$value}%",
            'fixed_discount'      => "امنح خصم {$value} ج.م",
            'fixed_price'         => "اجعل السعر {$value} ج.م",
            default               => "امنح خصم {$value}%",
        };

        $target = $this->targetId ? 'على برنامج محدد' : 'على كل البرامج';
        $who = $this->ruleType ? ConditionSchema::describe($this->ruleType, $this->conditions) : '';
        // Branch is not itself branch-scoped, so whereIn() on a browser-supplied
        // array would print any branch's name into the summary. Read the names
        // off the same collection that fed the checkboxes instead — no query,
        // and nothing outside this branch can appear.
        $names = $this->selectableBranches()
            ->whereIn('id', array_map('intval', $this->branchIds))
            ->pluck('name_ar')
            ->implode('، ');

        $where = $names !== '' ? 'في ' . $names : 'في كل الفروع';

        $when = 'من ' . ($this->effectiveFrom ?: '—')
            . ($this->effectiveTo ? " إلى {$this->effectiveTo}" : ' بدون نهاية');

        return trim("{$verb} {$target} {$who} {$where} {$when}");
    }

    /** Run the draft rule against a real participant + program. */
    public function refreshPreview(): void
    {
        $this->simulation = [];
        $this->audience = [];

        if (! $this->ruleType || $this->amount === '') {
            return;
        }

        $draft = $this->draftRule();
        $service = app(PricingService::class);

        $previewBranchId = $this->previewBranchId();

        try {
            $this->audience = $service->audience($draft, $previewBranchId);
        } catch (\Throwable $e) {
            $this->audience = [];
        }

        if (! $this->simParticipantId || ! $this->simProgramId) {
            return;
        }

        $participant = Participant::with(['person', 'guardians'])->find($this->simParticipantId);
        $program = TrainingProgram::find($this->simProgramId);

        if (! $participant || ! $program) {
            return;
        }

        try {
            $live = $service->calculate($program, $participant, $previewBranchId);

            // What the draft rule alone would do to the current price.
            $qualifies = ! empty($draft->conditions)
                ? $service->wouldApply($draft, $participant)
                : true;

            $this->simulation = [
                'participant' => $participant->full_name,
                'program'     => $program->name_ar ?? $program->name,
                'base'        => $live->baseAmount,
                'current'     => $live->finalAmount,
                'applied'     => $live->appliedRules,
                'qualifies'   => $qualifies,
                'draft_name'  => $this->nameAr ?: 'الخصم الجديد',
            ];
        } catch (DomainException $e) {
            $this->simulation = ['error' => $e->getMessage()];
        }
    }

    /**
     * The branch the live preview prices against.
     *
     * $branchIds is a public array the browser sets and refreshPreview() runs
     * on every keystroke, well before validate() ever sees it — so an id out of
     * it cannot be handed to PricingService unchecked. Empty still means "this
     * draft is academy-wide", which lets calculate() fall back to the simulated
     * participant's own branch.
     */
    private function previewBranchId(): ?int
    {
        if ($this->branchIds === []) {
            return null;
        }

        $allowed = $this->selectableBranches()
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->all();

        foreach ($this->branchIds as $id) {
            if (in_array((int) $id, $allowed, true)) {
                return (int) $id;
            }
        }

        return $this->getActiveBranchId();
    }

    /** An unsaved PricingRule used only for preview — never persisted. */
    private function draftRule(): PricingRule
    {
        $rule = new PricingRule([
            'name_ar'         => $this->nameAr ?: 'مسودة',
            'rule_type'       => $this->ruleType,
            'adjustment_type' => $this->adjustmentType,
            'conditions'      => ConditionSchema::normalize($this->ruleType, $this->conditions),
            'is_stackable'    => $this->isStackable,
        ]);

        $rule->adjustment_value = $this->storedAmount();

        return $rule;
    }

    private function storedAmount(): int
    {
        return in_array($this->adjustmentType, ['fixed_discount', 'fixed_price'], true)
            ? (int) round((float) $this->amount * 100)
            : Percent::toBasisPoints($this->amount ?: 0);
    }

    // ─── Save ─────────────────────────────────────────────────────

    public function rules(): array
    {
        $rules = [
            'nameAr'             => 'required|string|min:3|max:255',
            'ruleType'           => 'required|in:' . implode(',', array_column(PricingRuleType::cases(), 'value')),
            'adjustmentType'     => 'required|in:' . implode(',', array_column(AdjustmentType::cases(), 'value')),
            'amount'             => 'required|numeric|min:0.01',
            'branchIds'          => 'array',
            // Not exists:branches,id — a raw table query accepts every branch
            // in the academy, and $branchIds is a plain public array that the
            // browser sets. save() syncs it straight onto pricing_rule_branches,
            // so this is the only thing standing between a discount built here
            // and another branch's price list.
            'branchIds.*'        => [Rule::in($this->selectableBranches()->pluck('id')->all())],
            'effectiveFrom'      => 'required|date',
            'effectiveTo'        => 'nullable|date|after:effectiveFrom',
            'usageLimit'         => 'nullable|integer|min:1',
            'maxDiscountPercent' => 'nullable|integer|min:1|max:100',
            'displayOrder'       => 'integer|min:0',
        ] + ConditionSchema::validationRules($this->ruleType ?: 'custom');

        // A `branch` rule's conditions.values IS a list of branch ids — the
        // engine matches it against the participant's branch_id
        // (PricingService::evaluateInList). ConditionSchema types it only as a
        // string, and conditions is a plain public array, so without this an
        // academy-wide rule authored here could hand a discount to another
        // branch's participants.
        if ($this->ruleType === PricingRuleType::Branch->value) {
            $rules['conditions.values.*'] = [
                'required',
                Rule::in($this->selectableBranches()->pluck('id')->all()),
            ];
        }

        // Same reasoning as the branch clause: conditions is a plain public
        // array. A tier the engine has never heard of would simply match nobody
        // — a discount that silently does nothing is worse than a rejected one.
        if ($this->ruleType === PricingRuleType::MembershipType->value) {
            $rules['conditions.values.*'] = ['required', Rule::in(ConditionSchema::MEMBERSHIP_TYPES)];
        }

        return $rules;
    }

    public function messages(): array
    {
        return [
            'nameAr.required'         => __('اسم الخصم مطلوب'),
            'nameAr.min'              => __('اسم الخصم قصير جداً'),
            'ruleType.required'       => __('نوع الشرط مطلوب'),
            'adjustmentType.required' => __('نوع التعديل مطلوب'),
            'amount.required'         => __('قيمة الخصم مطلوبة'),
            'amount.numeric'          => __('قيمة الخصم يجب أن تكون رقماً'),
            'amount.min'              => __('قيمة الخصم يجب أن تكون أكبر من صفر'),
            'effectiveFrom.required'  => __('تاريخ البداية مطلوب'),
            'effectiveTo.after'       => __('تاريخ النهاية يجب أن يكون بعد تاريخ البداية'),
            'conditions.values.required' => __('يجب اختيار قيمة واحدة على الأقل'),
            'conditions.months.required' => __('يجب اختيار شهر واحد على الأقل'),
            'conditions.max.gte'      => __('الحد الأعلى يجب أن يكون أكبر من الحد الأدنى'),
            'branchIds.*.in'          => __('الفرع المحدد غير صالح'),
            'conditions.values.*.in'  => __('الفرع المحدد غير صالح'),
        ];
    }

    public function save(): void
    {
        $this->authorize('pricing.create');
        $this->validate();

        $conditions = ConditionSchema::normalize($this->ruleType, $this->conditions);

        // A rule with no conditions is an academy-wide discount wearing a
        // specific name. Manual rules are the deliberate exception.
        if (! $this->isManual && ! ConditionSchema::isMeaningful($this->ruleType, $conditions)) {
            $this->addError('conditions', __('يجب تحديد شرط واحد على الأقل — وإلا سيُطبق الخصم على كل المشتركين'));

            return;
        }

        try {
            $rule = DB::transaction(function () use ($conditions) {
                $rule = PricingRule::create([
                    'academy_id'           => app('current_academy')->id,
                    'name_ar'              => $this->nameAr,
                    'rule_type'            => $this->ruleType,
                    'adjustment_type'      => $this->adjustmentType,
                    'adjustment_value'     => $this->storedAmount(),
                    'conditions'           => $conditions,
                    'target_type'          => $this->targetType ?: null,
                    'target_id'            => $this->targetId ?: null,
                    // Legacy single-branch column stays null; the pivot owns targeting.
                    'branch_id'            => null,
                    'priority'             => 10,
                    'is_stackable'         => $this->isStackable,
                    'max_discount_percent' => $this->maxDiscountPercent,
                    'effective_from'       => $this->effectiveFrom,
                    'effective_to'         => $this->effectiveTo ?: null,
                    'is_active'            => $this->isActive,
                    'usage_limit'          => $this->usageLimit,
                    'usage_count'          => 0,
                    'created_by'           => auth()->id(),
                    'is_pinned'            => $this->isPinned,
                    'pin_icon'             => $this->pinIcon,
                    'display_order'        => $this->displayOrder,
                    'recipe_key'           => $this->recipeKey,
                    'is_manual'            => $this->isManual,
                    'requires_approval'    => $this->requiresApproval,
                    'min_role_level'       => $this->minRoleLevel,
                ]);

                // The pivot below is where this wizard states its targeting,
                // so the legacy single-branch column has to stay empty or it
                // would silently become a second, contradictory target.
                // BelongsToBranch stamps the active branch onto any create that
                // leaves it null, hence undoing it here. PricingRuleBranchScope
                // reads the pivot, so a rule targeted this way is still visible
                // only at the branches it names.
                if ($rule->branch_id !== null) {
                    $rule->update(['branch_id' => null]);
                }

                if ($this->branchIds) {
                    // academy_id is stamped by hand: pricing_rule_branches has
                    // no model, so no BelongsToAcademy hook fills it, and the
                    // column is NOT NULL. A bare sync() therefore failed with a
                    // not-null violation — every branch-targeted discount
                    // authored in this wizard died on save.
                    $rule->branches()->sync(array_fill_keys(
                        $this->branchIds,
                        ['academy_id' => $rule->academy_id],
                    ));
                }

                return $rule;
            });

            $this->createdRuleId = $rule->id;
            $this->stage = 'done';
        } catch (DomainException $e) {
            session()->flash('error', $e->getMessage());
        }
    }

    public function render()
    {
        return view('livewire.pricing.create-pricing-rule-wizard', [
            'recipes'  => DiscountRecipe::all(),
            // Feeds both the targeting checkboxes and — for a `branch` rule —
            // the condition-fields partial. The topbar switcher is the only
            // branch selector; this returns one entry unless the user is
            // genuinely in all-branches mode.
            'branches' => $this->selectableBranches(),
            // No branch predicate: TrainingProgram carries BelongsToBranch in
            // SHARED mode, so BranchScope already narrows this to this branch's
            // programmes plus the academy-wide ones.
            'programs' => TrainingProgram::orderBy('name_ar')->get(['id', 'name_ar']),
            'schemaKind' => $this->ruleType ? ConditionSchema::kind($this->ruleType) : null,
            'schemaUnit' => $this->ruleType ? ConditionSchema::unit($this->ruleType) : null,
            'ruleTypes'  => PricingRuleType::cases(),
        ]);
    }
}
