<?php

namespace App\Domain\Pricing\Support;

use App\Domain\Pricing\Enums\PricingRuleType;

/**
 * The single owner of the `pricing_rules.conditions` vocabulary.
 *
 * The authoring UI, the engine, the simulator and the migration all read their
 * keys from here. Previously each had its own spelling (min_age vs min vs
 * min_family_size), so the engine silently matched every participant.
 *
 * NOTHING may invent a condition key outside this class.
 */
final class ConditionSchema
{
    public const KIND_RANGE  = 'range';
    public const KIND_LIST   = 'list';
    public const KIND_TIMING = 'timing';
    public const KIND_MONTHS = 'months';
    public const KIND_SCHED  = 'schedule';
    public const KIND_NONE   = 'none';

    /**
     * Canonical shape per rule type.
     *
     * keys  — exactly what may appear in the conditions JSONB
     * unit  — Arabic unit shown beside range inputs
     *
     * `sibling_order` and `family_size` additionally accept an optional
     * `membership_type` list. A club whose sibling price differs for only one
     * tier — the second child of a non-resident pays less, a resident's price is
     * already the discounted one — cannot say that with a range alone, and
     * saying it with a second stacked rule would put that tier's base price in a
     * second place, to drift the next time fees change. The key is optional and
     * absent everywhere it was never authored, so an existing rule is unaffected.
     */
    private const SCHEMA = [
        'age'                 => ['kind' => self::KIND_RANGE,  'keys' => ['min', 'max'], 'unit' => 'سنة'],
        'membership_duration' => ['kind' => self::KIND_RANGE,  'keys' => ['min', 'max'], 'unit' => 'شهر'],
        'family_size'         => ['kind' => self::KIND_RANGE,  'keys' => ['min', 'max', 'membership_type'], 'unit' => 'أبناء'],
        'sibling_order'       => ['kind' => self::KIND_RANGE,  'keys' => ['min', 'max', 'membership_type'], 'unit' => 'الترتيب'],
        'enrollment_volume'   => ['kind' => self::KIND_RANGE,  'keys' => ['min', 'max'], 'unit' => 'برنامج'],
        'loyalty'             => ['kind' => self::KIND_RANGE,  'keys' => ['min', 'max'], 'unit' => 'شهر'],
        'membership_type'     => ['kind' => self::KIND_LIST,   'keys' => ['values'],     'unit' => null],
        'classification'      => ['kind' => self::KIND_LIST,   'keys' => ['values'],     'unit' => null],
        'gender'              => ['kind' => self::KIND_LIST,   'keys' => ['values'],     'unit' => null],
        'branch'              => ['kind' => self::KIND_LIST,   'keys' => ['values'],     'unit' => null],
        'enrollment_timing'   => ['kind' => self::KIND_TIMING, 'keys' => ['before_date', 'after_date', 'days_before_start'], 'unit' => null],
        'seasonal'            => ['kind' => self::KIND_MONTHS, 'keys' => ['months'],     'unit' => null],
        'day_time'            => ['kind' => self::KIND_SCHED,  'keys' => ['days', 'after_hour', 'before_hour'], 'unit' => null],
        'custom'              => ['kind' => self::KIND_NONE,   'keys' => [],             'unit' => null],
    ];

    /** Allowed values for list-kind rule types. */
    public const CLASSIFICATIONS = ['regular', 'vip', 'scholarship', 'staff_child', 'trial'];
    public const MEMBERSHIP_TYPES = ['member', 'non_member'];
    public const GENDERS = ['male', 'female'];
    public const WEEKDAYS = ['saturday', 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday'];

    public static function kind(PricingRuleType|string $type): string
    {
        $key = $type instanceof PricingRuleType ? $type->value : $type;

        return self::SCHEMA[$key]['kind'] ?? self::KIND_NONE;
    }

    public static function keys(PricingRuleType|string $type): array
    {
        $key = $type instanceof PricingRuleType ? $type->value : $type;

        return self::SCHEMA[$key]['keys'] ?? [];
    }

    public static function unit(PricingRuleType|string $type): ?string
    {
        $key = $type instanceof PricingRuleType ? $type->value : $type;

        return self::SCHEMA[$key]['unit'] ?? null;
    }

    /**
     * Strip anything not in the canonical shape and coerce types.
     * Every write path runs through this — that is what keeps keys from drifting.
     */
    public static function normalize(PricingRuleType|string $type, array $input): array
    {
        $kind = self::kind($type);
        $out = [];

        switch ($kind) {
            case self::KIND_RANGE:
                foreach (['min', 'max'] as $k) {
                    if (isset($input[$k]) && $input[$k] !== '' && is_numeric($input[$k])) {
                        $out[$k] = (int) $input[$k];
                    }
                }
                // Only where the schema declares it — a tier filter on an age or
                // loyalty rule would be silently ignored by the engine, so it is
                // stripped here rather than stored as a lie.
                if (in_array('membership_type', self::keys($type), true)) {
                    $tiers = $input['membership_type'] ?? [];
                    $tiers = is_array($tiers) ? $tiers : [$tiers];
                    $tiers = array_values(array_intersect($tiers, self::MEMBERSHIP_TYPES));
                    if ($tiers) {
                        $out['membership_type'] = $tiers;
                    }
                }
                break;

            case self::KIND_LIST:
                $values = $input['values'] ?? [];
                $values = is_array($values) ? $values : [$values];
                $values = array_values(array_filter($values, fn ($v) => $v !== '' && $v !== null));
                if ($values) {
                    $out['values'] = $values;
                }
                break;

            case self::KIND_TIMING:
                foreach (['before_date', 'after_date'] as $k) {
                    if (! empty($input[$k])) {
                        $out[$k] = (string) $input[$k];
                    }
                }
                if (isset($input['days_before_start']) && is_numeric($input['days_before_start'])) {
                    $out['days_before_start'] = (int) $input['days_before_start'];
                }
                break;

            case self::KIND_MONTHS:
                $months = $input['months'] ?? [];
                $months = array_values(array_filter(
                    array_map('intval', is_array($months) ? $months : [$months]),
                    fn ($m) => $m >= 1 && $m <= 12
                ));
                if ($months) {
                    $out['months'] = $months;
                }
                break;

            case self::KIND_SCHED:
                $days = $input['days'] ?? [];
                $days = array_values(array_intersect(
                    array_map(fn ($d) => strtolower((string) $d), is_array($days) ? $days : [$days]),
                    self::WEEKDAYS
                ));
                if ($days) {
                    $out['days'] = $days;
                }
                foreach (['after_hour', 'before_hour'] as $k) {
                    if (isset($input[$k]) && $input[$k] !== '' && is_numeric($input[$k])) {
                        $out[$k] = max(0, min(23, (int) $input[$k]));
                    }
                }
                break;
        }

        return $out;
    }

    /**
     * Validation rules for the builder, keyed by `conditions.*`.
     */
    public static function validationRules(PricingRuleType|string $type): array
    {
        return match (self::kind($type)) {
            self::KIND_RANGE => array_merge([
                'conditions.min' => 'nullable|integer|min:0|max:200',
                'conditions.max' => 'nullable|integer|min:0|max:200|gte:conditions.min',
            ], in_array('membership_type', self::keys($type), true) ? [
                'conditions.membership_type'   => 'nullable|array',
                'conditions.membership_type.*' => 'string|in:' . implode(',', self::MEMBERSHIP_TYPES),
            ] : []),
            self::KIND_LIST => [
                'conditions.values'   => 'required|array|min:1',
                'conditions.values.*' => 'required|string|max:50',
            ],
            self::KIND_TIMING => [
                'conditions.before_date'       => 'nullable|date',
                'conditions.after_date'        => 'nullable|date',
                'conditions.days_before_start' => 'nullable|integer|min:1|max:365',
            ],
            self::KIND_MONTHS => [
                'conditions.months'   => 'required|array|min:1',
                'conditions.months.*' => 'required|integer|min:1|max:12',
            ],
            self::KIND_SCHED => [
                'conditions.days'         => 'nullable|array',
                'conditions.days.*'       => 'string|in:' . implode(',', self::WEEKDAYS),
                'conditions.after_hour'   => 'nullable|integer|min:0|max:23',
                'conditions.before_hour'  => 'nullable|integer|min:0|max:23',
            ],
            default => [],
        };
    }

    /**
     * A rule must actually constrain something, or it is an academy-wide discount
     * wearing a specific name. Range and list kinds are required to be non-empty.
     */
    public static function isMeaningful(PricingRuleType|string $type, array $conditions): bool
    {
        return match (self::kind($type)) {
            self::KIND_NONE => true,
            default => ! empty($conditions),
        };
    }

    /**
     * The Arabic fragment for the sentence builder, e.g. "للأعمار من ٦ إلى ١٠ سنة".
     */
    public static function describe(PricingRuleType|string $type, array $c): string
    {
        $key = $type instanceof PricingRuleType ? $type->value : $type;
        $unit = self::unit($key);

        $range = function (string $noun) use ($c, $unit) {
            $min = $c['min'] ?? null;
            $max = $c['max'] ?? null;
            if ($min !== null && $max !== null) {
                return "{$noun} من {$min} إلى {$max}" . ($unit ? " {$unit}" : '');
            }
            if ($min !== null) {
                return "{$noun} {$min}" . ($unit ? " {$unit}" : '') . ' فأكثر';
            }
            if ($max !== null) {
                return "{$noun} حتى {$max}" . ($unit ? " {$unit}" : '');
            }

            return $noun;
        };

        $sentence = match ($key) {
            'age'                 => $range('للأعمار'),
            'membership_duration' => $range('لمدة ' . term('membership_indefinite')),
            'family_size'         => $range('لأسرة عدد أبنائها'),
            'sibling_order'       => $range('للابن رقم'),
            'enrollment_volume'   => $range('لعدد برامج'),
            'loyalty'             => $range('ل' . term('membership_indefinite')),
            'membership_type'     => 'لـ' . implode('، ', array_map(
                fn ($v) => self::membershipTypeLabel($v),
                $c['values'] ?? []
            )),
            'classification'      => 'لتصنيف: ' . implode('، ', array_map(
                fn ($v) => self::classificationLabel($v),
                $c['values'] ?? []
            )),
            'gender'              => 'للـ' . implode('، ', array_map(
                fn ($v) => $v === 'male' ? 'ذكور' : 'إناث',
                $c['values'] ?? []
            )),
            'branch'              => 'لفروع محددة',
            'enrollment_timing'   => isset($c['days_before_start'])
                ? "للتسجيل قبل البداية بـ {$c['days_before_start']} يوم"
                : 'في فترة تسجيل محددة',
            'seasonal'            => 'في شهور: ' . implode('، ', array_map(
                fn ($m) => self::monthLabel((int) $m),
                $c['months'] ?? []
            )),
            'day_time'            => self::describeSchedule($c),
            'custom'              => 'يدوي — يُطبق بالاختيار فقط',
            default               => '',
        };

        // The optional tier filter, appended rather than folded into the noun so
        // the sentence still reads as one clause: "للابن رقم ٢ فأكثر لغير المقيم".
        if (! empty($c['membership_type']) && in_array('membership_type', self::keys($key), true)) {
            $sentence .= ' ل' . implode('، ', array_map(
                fn ($v) => self::membershipTypeLabel($v),
                $c['membership_type']
            ));
        }

        return $sentence;
    }

    private static function describeSchedule(array $c): string
    {
        $parts = [];
        if (! empty($c['days'])) {
            $parts[] = 'أيام ' . implode('، ', array_map(fn ($d) => self::dayLabel($d), $c['days']));
        }
        if (isset($c['after_hour']) && isset($c['before_hour'])) {
            $parts[] = "من الساعة {$c['after_hour']} إلى {$c['before_hour']}";
        } elseif (isset($c['after_hour'])) {
            $parts[] = "بعد الساعة {$c['after_hour']}";
        } elseif (isset($c['before_hour'])) {
            $parts[] = "قبل الساعة {$c['before_hour']}";
        }

        return $parts ? implode(' ', $parts) : 'في مواعيد محددة';
    }

    /**
     * The branch's own word for the tier — عضو in a club, مقيم in a compound,
     * مشترك on a beach. Read through term() rather than hardcoded, so a rule
     * authored in one branch still reads correctly in the vocabulary of the
     * branch looking at it.
     */
    public static function membershipTypeLabel(string $v): string
    {
        return match ($v) {
            'member'     => term('member'),
            'non_member' => term('non_member'),
            default      => $v,
        };
    }

    public static function classificationLabel(string $v): string
    {
        return match ($v) {
            'regular'     => 'عادي',
            'vip'         => 'مميز',
            'scholarship' => 'منحة',
            'staff_child' => 'ابن عامل',
            'trial'       => 'تجريبي',
            default       => $v,
        };
    }

    public static function dayLabel(string $d): string
    {
        return match (strtolower($d)) {
            'saturday'  => 'السبت',
            'sunday'    => 'الأحد',
            'monday'    => 'الإثنين',
            'tuesday'   => 'الثلاثاء',
            'wednesday' => 'الأربعاء',
            'thursday'  => 'الخميس',
            'friday'    => 'الجمعة',
            default     => $d,
        };
    }

    public static function monthLabel(int $m): string
    {
        return [1 => 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو',
            'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'][$m] ?? (string) $m;
    }
}
