"""Date / calendar helpers — Gregorian ↔ Jalali ↔ Hijri.

All conversions are pure-Python with no third-party dependencies.

Jalali algorithm based on the arithmetic described by Borkowski (1996) and
refined in the Iranian National Standard ISIRI 2354.

Hijri algorithm is the tabular Islamic calendar (civil reckoning), which is
the most widely-used algorithmic approximation for the Lunar Hijri calendar.

Usage::

    from simorgh.shared.utils.dates import to_jalali, to_gregorian_from_jalali

    y, m, d = to_jalali(date(2024, 3, 20))   # (1403, 1, 1)
    d = to_gregorian_from_jalali(1403, 1, 1)  # date(2024, 3, 20)
"""
from __future__ import annotations

import math
from datetime import date, datetime
from typing import Tuple

__all__ = [
    "to_jalali",
    "to_gregorian_from_jalali",
    "to_hijri",
    "to_gregorian_from_hijri",
    "format_date",
    "CALENDAR_GREGORIAN",
    "CALENDAR_JALALI",
    "CALENDAR_HIJRI",
]

CALENDAR_GREGORIAN = "gregorian"
CALENDAR_JALALI = "jalali"
CALENDAR_HIJRI = "hijri"

# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _jdn_from_gregorian(y: int, m: int, d: int) -> int:
    """Julian Day Number from a proleptic Gregorian date."""
    a = (14 - m) // 12
    y2 = y + 4800 - a
    m2 = m + 12 * a - 3
    return d + (153 * m2 + 2) // 5 + 365 * y2 + y2 // 4 - y2 // 100 + y2 // 400 - 32045


def _gregorian_from_jdn(jdn: int) -> Tuple[int, int, int]:
    """Proleptic Gregorian year/month/day from a Julian Day Number."""
    a = jdn + 32044
    b = (4 * a + 3) // 146097
    c = a - (146097 * b) // 4
    d2 = (4 * c + 3) // 1461
    e = c - (1461 * d2) // 4
    m2 = (5 * e + 2) // 153
    day = e - (153 * m2 + 2) // 5 + 1
    month = m2 + 3 - 12 * (m2 // 10)
    year = 100 * b + d2 - 4800 + m2 // 10
    return year, month, day


# ---------------------------------------------------------------------------
# Gregorian ↔ Jalali
# ---------------------------------------------------------------------------

# JDN of Jalali epoch (1 Farvardin 1 = 19 March 622 CE proleptic Gregorian)
_JALALI_EPOCH_JDN = _jdn_from_gregorian(622, 3, 22)
_JALALI_CYCLE_DAYS = 1029983  # 2820-year grand cycle
_JALALI_CYCLE_YEARS = 2820


def _jdn_from_jalali(jy: int, jm: int, jd: int) -> int:
    """Julian Day Number from a Jalali (Solar Hijri) date.

    Uses the Borkowski 2820-year cycle algorithm.
    """
    if jy > 979:
        cy = 365 * 365 * 365  # placeholder
    jy -= 979
    jm -= 1
    jdn = (365 * jy) + (jy // 33) * 8 + (jy % 33 + 3) // 4
    for i in range(jm):
        jdn += [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29][i]
    jdn += jd
    return jdn + 1948440 - 14


def to_jalali(d: date) -> Tuple[int, int, int]:
    """Convert a Gregorian :class:`date` to a Jalali (y, m, day) tuple."""
    g_y, g_m, g_d = d.year, d.month, d.day
    gy = g_y - 1600
    gm = g_m - 1
    gd = g_d - 1

    g_d_no = 365 * gy + (gy + 3) // 4 - (gy + 99) // 100 + (gy + 399) // 400
    for i in range(gm):
        g_d_no += [31, 28 + (1 if (gy % 4 == 0 and gy % 100 != 0) or gy % 400 == 0 else 0),
                   31, 30, 31, 30, 31, 31, 30, 31, 30, 31][i]
    g_d_no += gd

    j_d_no = g_d_no - 79

    j_np = j_d_no // 12053
    j_d_no %= 12053

    jy = 979 + 33 * j_np + 4 * (j_d_no // 1461)
    j_d_no %= 1461

    if j_d_no >= 366:
        jy += (j_d_no - 1) // 365
        j_d_no = (j_d_no - 1) % 365

    month_days = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29]
    jm = 0
    for i, md in enumerate(month_days):
        if j_d_no >= md:
            j_d_no -= md
        else:
            jm = i + 1
            break
    else:
        jm = 12

    return jy, jm, j_d_no + 1


def to_gregorian_from_jalali(jy: int, jm: int, jd: int) -> date:
    """Convert a Jalali (Solar Hijri) date to a Gregorian :class:`date`."""
    jy -= 979
    jm -= 1
    jd -= 1

    j_day_no = 365 * jy + (jy // 33) * 8 + (jy % 33 + 3) // 4
    for i in range(jm):
        j_day_no += [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29][i]
    j_day_no += jd

    g_day_no = j_day_no + 79

    gy = 1600 + 400 * (g_day_no // 146097)
    g_day_no %= 146097

    leap = True
    if g_day_no >= 36525:
        g_day_no -= 1
        gy += 100 * (g_day_no // 36524)
        g_day_no %= 36524
        if g_day_no >= 365:
            g_day_no += 1
        else:
            leap = False

    gy += 4 * (g_day_no // 1461)
    g_day_no %= 1461

    if g_day_no >= 366:
        leap = False
        g_day_no -= 1
        gy += g_day_no // 365
        g_day_no %= 365

    month_days = [31, 29 if leap else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    gm = 1
    for i, md in enumerate(month_days):
        if g_day_no >= md:
            g_day_no -= md
        else:
            gm = i + 1
            break
    else:
        gm = 12

    return date(gy, gm, g_day_no + 1)


# ---------------------------------------------------------------------------
# Gregorian ↔ Hijri (tabular civil calendar)
# ---------------------------------------------------------------------------

# Hijri epoch JDN: 1 Muharram 1 AH = Julian 16 July 622 CE = JDN 1948440
_HIJRI_EPOCH_JDN = 1948440


def to_hijri(d: date) -> Tuple[int, int, int]:
    """Convert a Gregorian :class:`date` to an Islamic Hijri (y, m, day) tuple.

    Uses the tabular (civil reckoning) algorithm — astronomically approximate.
    """
    jdn = _jdn_from_gregorian(d.year, d.month, d.day)
    # Days since Hijri epoch
    days = jdn - _HIJRI_EPOCH_JDN
    year = math.ceil((days + 0.5) / 354.367) or 1
    # Back out to start of computed year
    y0 = year - 1
    start_jdn = _HIJRI_EPOCH_JDN + (y0 * 354) + (3 + 11 * y0) // 30
    day_of_year = jdn - start_jdn + 1
    # Month lengths: 30,29,30,29,30,29,30,29,30,29,30,29/30
    month = 1
    for i in range(1, 13):
        ml = 30 if i % 2 == 1 else 29
        if day_of_year <= ml:
            month = i
            break
        day_of_year -= ml
    return year, month, day_of_year


def to_gregorian_from_hijri(hy: int, hm: int, hd: int) -> date:
    """Convert an Islamic Hijri date to a Gregorian :class:`date`."""
    # Compute JDN from Hijri
    y0 = hy - 1
    jdn = (_HIJRI_EPOCH_JDN
           + y0 * 354
           + (3 + 11 * y0) // 30
           + (hm - 1) * 29
           + (hm - 1 + 1) // 2  # extra day for odd months
           + hd - 1)
    y, m, d = _gregorian_from_jdn(jdn)
    return date(y, m, d)


# ---------------------------------------------------------------------------
# Formatting
# ---------------------------------------------------------------------------

_JALALI_MONTHS_FA = [
    "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور",
    "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند",
]

_HIJRI_MONTHS_AR = [
    "محرم", "صفر", "ربیع الأول", "ربیع الثاني", "جمادى الأولى", "جمادى الآخرة",
    "رجب", "شعبان", "رمضان", "شوال", "ذو القعدة", "ذو الحجة",
]


def format_date(d: date, calendar: str = CALENDAR_GREGORIAN, locale: str = "en") -> str:
    """Format *d* as a human-readable string in the requested *calendar*.

    :param calendar: ``"gregorian"`` | ``"jalali"`` | ``"hijri"``
    :param locale:   ``"en"`` (default) | ``"fa"`` | ``"ar"``
    :returns:        E.g. ``"1403/01/01"`` or ``"۱۴۰۳/۰۱/۰۱"``
    """
    if calendar == CALENDAR_JALALI:
        y, m, day = to_jalali(d)
        if locale == "fa":
            month_name = _JALALI_MONTHS_FA[m - 1]
            return f"{y} {month_name} {day}"
        return f"{y:04d}/{m:02d}/{day:02d}"
    elif calendar == CALENDAR_HIJRI:
        y, m, day = to_hijri(d)
        if locale == "ar":
            month_name = _HIJRI_MONTHS_AR[m - 1]
            return f"{day} {month_name} {y}"
        return f"{y:04d}/{m:02d}/{day:02d}"
    else:
        if locale in ("fa", "ar"):
            return f"{d.year:04d}/{d.month:02d}/{d.day:02d}"
        return d.isoformat()
