<?php
namespace App\Utils;

class DateNormalizer
{
    private const JALALI_MIN_YEAR = 1300;
    private const JALALI_MAX_YEAR = 1599;
    private const GREGORIAN_MIN_YEAR = 1900;
    private const GREGORIAN_MAX_YEAR = 2500;

    public static function normalizeGregorianOrJalaliDate($value): ?string
    {
        $value = self::normalizeDigits(trim((string)$value));
        if ($value === '' || $value === '0000-00-00' || $value === '0000-00-00 00:00:00') {
            return null;
        }

        $value = str_replace('.', '/', $value);
        if (preg_match('/^\d{8}$/', $value)) {
            $value = substr($value, 0, 4) . '/' . substr($value, 4, 2) . '/' . substr($value, 6, 2);
        }
        if (!preg_match('/^(\d{4})[-\/](\d{1,2})[-\/](\d{1,2})(?:[ T].*)?$/', $value, $m)) {
            return null;
        }

        $year = (int)$m[1];
        $month = (int)$m[2];
        $day = (int)$m[3];

        if ($year >= self::JALALI_MIN_YEAR && $year <= self::JALALI_MAX_YEAR) {
            if (!self::isValidJalaliDate($year, $month, $day)) {
                return null;
            }
            [$gy, $gm, $gd] = self::jalaliToGregorian($year, $month, $day);
            return checkdate($gm, $gd, $gy) ? sprintf('%04d-%02d-%02d', $gy, $gm, $gd) : null;
        }

        if ($year >= self::GREGORIAN_MIN_YEAR && $year <= self::GREGORIAN_MAX_YEAR && checkdate($month, $day, $year)) {
            return sprintf('%04d-%02d-%02d', $year, $month, $day);
        }

        return null;
    }

    public static function normalizeRequiredDate($value, string $fieldName = 'date'): string
    {
        $date = self::normalizeGregorianOrJalaliDate($value);
        if ($date === null) {
            throw new \InvalidArgumentException($fieldName . ' نامعتبر است');
        }
        return $date;
    }

    public static function normalizeDateRange($dateFrom, $dateTo, ?array $fallbackRange = null): array
    {
        $rawFrom = trim((string)$dateFrom);
        $rawTo = trim((string)$dateTo);
        $from = $rawFrom === '' ? null : self::normalizeGregorianOrJalaliDate($rawFrom);
        $to = $rawTo === '' ? null : self::normalizeGregorianOrJalaliDate($rawTo);

        if ($rawFrom !== '' && $from === null) {
            throw new \InvalidArgumentException('تاریخ شروع نامعتبر است');
        }
        if ($rawTo !== '' && $to === null) {
            throw new \InvalidArgumentException('تاریخ پایان نامعتبر است');
        }

        if (($from === null || $to === null) && $fallbackRange !== null) {
            $from = $from ?: (string)($fallbackRange['date_from'] ?? '');
            $to = $to ?: (string)($fallbackRange['date_to'] ?? '');
        }

        if ($from !== null && $to !== null && $to < $from) {
            [$from, $to] = [$to, $from];
        }

        return [$from, $to];
    }

    public static function currentJalaliMonthRange(?string $referenceGregorianDate = null): array
    {
        $reference = $referenceGregorianDate
            ? self::normalizeGregorianOrJalaliDate($referenceGregorianDate)
            : (new \DateTimeImmutable('now', new \DateTimeZone('Asia/Tehran')))->format('Y-m-d');
        if ($reference === null) {
            $reference = (new \DateTimeImmutable('now', new \DateTimeZone('Asia/Tehran')))->format('Y-m-d');
        }
        [$jy, $jm] = self::gregorianToJalaliParts($reference);
        return self::jalaliMonthRange($jy, $jm);
    }

    public static function currentJalaliMonth(): string
    {
        $range = self::currentJalaliMonthRange();
        return (string)$range['jalali_month'];
    }

    public static function normalizeBillingMonthRange(string $billingMonth): array
    {
        $value = self::normalizeDigits(trim($billingMonth));
        $value = str_replace('/', '-', $value);
        if (!preg_match('/^(\d{4})-(\d{1,2})$/', $value, $m)) {
            throw new \InvalidArgumentException('ماه محاسبه نامعتبر است');
        }

        $year = (int)$m[1];
        $month = (int)$m[2];
        if ($month < 1 || $month > 12) {
            throw new \InvalidArgumentException('ماه محاسبه نامعتبر است');
        }

        if ($year >= self::JALALI_MIN_YEAR && $year <= self::JALALI_MAX_YEAR) {
            return self::jalaliMonthRange($year, $month) + [
                'input_month' => sprintf('%04d-%02d', $year, $month),
                'calendar' => 'jalali',
            ];
        }

        if ($year >= self::GREGORIAN_MIN_YEAR && $year <= self::GREGORIAN_MAX_YEAR) {
            $from = sprintf('%04d-%02d-01', $year, $month);
            $to = date('Y-m-t', strtotime($from));
            [$jy, $jm] = self::gregorianToJalaliParts($from);
            return [
                'date_from' => $from,
                'date_to' => $to,
                'billing_month' => sprintf('%04d-%02d', $year, $month),
                'input_month' => sprintf('%04d-%02d', $year, $month),
                'jalali_month' => sprintf('%04d-%02d', $jy, $jm),
                'calendar' => 'gregorian',
            ];
        }

        throw new \InvalidArgumentException('ماه محاسبه نامعتبر است');
    }

    public static function jalaliMonthRange(int $jalaliYear, int $jalaliMonth): array
    {
        if ($jalaliYear < self::JALALI_MIN_YEAR || $jalaliYear > self::JALALI_MAX_YEAR || $jalaliMonth < 1 || $jalaliMonth > 12) {
            throw new \InvalidArgumentException('ماه شمسی نامعتبر است');
        }
        $lastDay = self::jalaliMonthLength($jalaliYear, $jalaliMonth);
        [$fromGy, $fromGm, $fromGd] = self::jalaliToGregorian($jalaliYear, $jalaliMonth, 1);
        [$toGy, $toGm, $toGd] = self::jalaliToGregorian($jalaliYear, $jalaliMonth, $lastDay);
        $dateFrom = sprintf('%04d-%02d-%02d', $fromGy, $fromGm, $fromGd);
        $dateTo = sprintf('%04d-%02d-%02d', $toGy, $toGm, $toGd);
        return [
            'date_from' => $dateFrom,
            'date_to' => $dateTo,
            'billing_month' => substr($dateFrom, 0, 7),
            'jalali_month' => sprintf('%04d-%02d', $jalaliYear, $jalaliMonth),
        ];
    }

    public static function normalizeDigits(string $value): string
    {
        return strtr($value, [
            '۰' => '0', '۱' => '1', '۲' => '2', '۳' => '3', '۴' => '4',
            '۵' => '5', '۶' => '6', '۷' => '7', '۸' => '8', '۹' => '9',
            '٠' => '0', '١' => '1', '٢' => '2', '٣' => '3', '٤' => '4',
            '٥' => '5', '٦' => '6', '٧' => '7', '٨' => '8', '٩' => '9',
        ]);
    }

    public static function isValidJalaliDate(int $year, int $month, int $day): bool
    {
        return $year >= self::JALALI_MIN_YEAR
            && $year <= self::JALALI_MAX_YEAR
            && $month >= 1
            && $month <= 12
            && $day >= 1
            && $day <= self::jalaliMonthLength($year, $month);
    }

    public static function jalaliMonthLength(int $year, int $month): int
    {
        if ($month >= 1 && $month <= 6) {
            return 31;
        }
        if ($month >= 7 && $month <= 11) {
            return 30;
        }
        if ($month === 12) {
            return self::isJalaliLeapYear($year) ? 30 : 29;
        }
        return 0;
    }

    public static function isJalaliLeapYear(int $year): bool
    {
        [$gy1, $gm1, $gd1] = self::jalaliToGregorian($year, 1, 1);
        [$gy2, $gm2, $gd2] = self::jalaliToGregorian($year + 1, 1, 1);
        $from = new \DateTimeImmutable(sprintf('%04d-%02d-%02d', $gy1, $gm1, $gd1));
        $to = new \DateTimeImmutable(sprintf('%04d-%02d-%02d', $gy2, $gm2, $gd2));
        return ((int)$from->diff($to)->days) === 366;
    }

    public static function jalaliToGregorian(int $jy, int $jm, int $jd): array
    {
        $jy += 1595;
        $days = -355668 + (365 * $jy) + ((int)($jy / 33) * 8) + (int)((($jy % 33) + 3) / 4) + $jd;
        $days += $jm < 7 ? ($jm - 1) * 31 : (($jm - 7) * 30) + 186;

        $gy = 400 * (int)($days / 146097);
        $days %= 146097;

        if ($days > 36524) {
            $gy += 100 * (int)(--$days / 36524);
            $days %= 36524;
            if ($days >= 365) {
                $days++;
            }
        }

        $gy += 4 * (int)($days / 1461);
        $days %= 1461;

        if ($days > 365) {
            $gy += (int)(($days - 1) / 365);
            $days = ($days - 1) % 365;
        }

        $gd = $days + 1;
        $monthDays = [0, 31, self::isGregorianLeap($gy) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        for ($gm = 1; $gm <= 12 && $gd > $monthDays[$gm]; $gm++) {
            $gd -= $monthDays[$gm];
        }

        return [$gy, $gm, $gd];
    }

    public static function gregorianToJalaliParts(string $gregorianDate): array
    {
        $date = self::normalizeGregorianOrJalaliDate($gregorianDate);
        if ($date === null || !preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $m)) {
            throw new \InvalidArgumentException('تاریخ میلادی نامعتبر است');
        }

        $gy = (int)$m[1];
        $gm = (int)$m[2];
        $gd = (int)$m[3];
        $gDaysInMonth = [31, self::isGregorianLeap($gy) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
        $jDaysInMonth = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29];

        $gy -= 1600;
        $gm -= 1;
        $gd -= 1;

        $gDayNo = (365 * $gy)
            + (int)(($gy + 3) / 4)
            - (int)(($gy + 99) / 100)
            + (int)(($gy + 399) / 400);
        for ($i = 0; $i < $gm; $i++) {
            $gDayNo += $gDaysInMonth[$i];
        }
        $gDayNo += $gd;

        $jDayNo = $gDayNo - 79;
        $jNp = (int)($jDayNo / 12053);
        $jDayNo %= 12053;
        $jy = 979 + (33 * $jNp) + (4 * (int)($jDayNo / 1461));
        $jDayNo %= 1461;

        if ($jDayNo >= 366) {
            $jy += (int)(($jDayNo - 1) / 365);
            $jDayNo = ($jDayNo - 1) % 365;
        }

        for ($i = 0; $i < 11 && $jDayNo >= $jDaysInMonth[$i]; $i++) {
            $jDayNo -= $jDaysInMonth[$i];
        }

        return [$jy, $i + 1, $jDayNo + 1];
    }

    private static function isGregorianLeap(int $year): bool
    {
        return ($year % 4 === 0 && $year % 100 !== 0) || ($year % 400 === 0);
    }
}
