"""Runtime formatting engine for the localization layer.

This module provides the runtime services that consume the data model
(Locale, Currency, Language) and produce formatted strings.

Every formatter accepts a ``locale`` (Locale instance) or ``locale_code``
string and fetches formatting patterns from the resolved locale.
"""

from __future__ import annotations

import math
from datetime import date, datetime, time
from decimal import Decimal
from typing import Any

from simorgh.apps.localization.models import CalendarSystem, Locale, NumeralSystem
from simorgh.apps.localization.selectors import get_currency_by_code, resolve_locale

# ---------------------------------------------------------------------------
# Pattern → strftime conversion tables
# ---------------------------------------------------------------------------

_DATE_PATTERN_MAP: dict[str, str] = {
    "YYYY": "%Y",
    "YY": "%y",
    "MM": "%m",
    "DD": "%d",
    "HH": "%H",
    "hh": "%I",
    "mm": "%M",
    "ss": "%S",
    "A": "%p",
    "ddd": "%a",
    "DDDD": "%A",
    "MMM": "%b",
    "MMMM": "%B",
}

_JALALI_MONTH_NAMES = [
    "Farvardin", "Ordibehesht", "Khordad", "Tir",
    "Mordad", "Shahrivar", "Mehr", "Aban",
    "Azar", "Dey", "Bahman", "Esfand",
]

_JALALI_MONTH_NAMES_SHORT = [
    "Frv", "Ord", "Khr", "Tir", "Mrd", "Shr", "Mhr", "Abn",
    "Azr", "Dey", "Bah", "Esf",
]

_JALALI_WEEKDAY_NAMES = [
    "Shanbeh", "Yekshanbeh", "Doshanbeh", "Seshanbeh",
    "Chaharshanbeh", "Panjshanbeh", "Jomeh",
]

_JALALI_WEEKDAY_NAMES_SHORT = [
    "Shn", "Yek", "Dos", "Ses", "Cha", "Pan", "Jom",
]

_HIJRI_MONTH_NAMES = [
    "Muharram", "Safar", "Rabi Al-Awwal", "Rabi Al-Thani",
    "Jumada Al-Ula", "Jumada Al-Akhirah", "Rajab", "Shaban",
    "Ramadan", "Shawwal", "Dhu Al-Qadah", "Dhu Al-Hijjah",
]

_HIJRI_MONTH_NAMES_SHORT = [
    "Muh", "Saf", "R.A1", "R.A2", "J.A1", "J.A2", "Raj", "Shb",
    "Ram", "Shw", "D.Qa", "D.Hj",
]

_HIJRI_WEEKDAY_NAMES = [
    "Al-Ahad", "Al-Ithnayn", "Ath-Thulatha", "Al-Arbiaa",
    "Al-Khamis", "Al-Jumuaa", "As-Sabt",
]

_HIJRI_WEEKDAY_NAMES_SHORT = [
    "Ahd", "Ith", "Thl", "Arb", "Khm", "Jum", "Sbt",
]


# ---------------------------------------------------------------------------
# Jalali (Solar Hijri) conversion
# ---------------------------------------------------------------------------

def gregorian_to_jalali(g_date: date) -> tuple[int, int, int]:
    """Convert a Gregorian date to Jalali (year, month, day).

    Uses the algorithm described in:
        https://www.fourmilab.ch/documents/calendar/
    """
    gy, gm, gd = g_date.year, g_date.month, g_date.day

    # Days from start of Gregorian calendar
    g_d_m = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
    gy2 = gy + 1 if gm > 2 else gy

    days = 355666 + (365 * gy) + math.floor((gy2 + 3) / 4) - math.floor((gy2 + 99) / 100) + math.floor((gy2 + 399) / 400) + gd + g_d_m[gm - 1]

    jy = -1595 + (33 * math.floor(days / 12053))
    days %= 12053
    jy += 4 * math.floor(days / 1461)
    days %= 1461

    if days > 365:
        jy += math.floor((days - 1) / 365)
        days = (days - 1) % 365

    if days < 186:
        jm = 1 + math.floor(days / 31)
        jd = 1 + (days % 31)
    else:
        jm = 7 + math.floor((days - 186) / 30)
        jd = 1 + ((days - 186) % 30)

    return jy, jm, jd


def jalali_to_gregorian(jy: int, jm: int, jd: int) -> date:
    """Convert a Jalali date to Gregorian."""
    jy += 1595
    days = -355668 + (365 * jy) + (math.floor(jy / 33) * 8) + math.floor(((jy % 33) + 3) / 4)

    if jm < 7:
        days += (jm - 1) * 31
    else:
        days += ((jm - 7) * 30) + 186

    days += jd - 1

    gy = 400 * math.floor(days / 146097)
    days %= 146097

    if days > 36524:
        days -= 1
        gy += 100 * math.floor(days / 36524)
        days %= 36524
        if days >= 365:
            days += 1

    gy += 4 * math.floor(days / 1461)
    days %= 1461

    if days > 365:
        gy += math.floor((days - 1) / 365)
        days = (days - 1) % 365

    g_days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    if (gy % 4 == 0 and gy % 100 != 0) or (gy % 400 == 0):
        g_days_in_month[2] = 29

    gm = 1
    while days >= g_days_in_month[gm]:
        days -= g_days_in_month[gm]
        gm += 1

    return date(gy, gm, int(days) + 1)


# ---------------------------------------------------------------------------
# Hijri (Lunar) conversion — approximate (Kuwaiti algorithm)
# ---------------------------------------------------------------------------

def gregorian_to_hijri(g_date: date) -> tuple[int, int, int]:
    """Approximate Gregorian → Hijri conversion (Kuwaiti algorithm).

    Accurate to ±1 day for most dates. For exact religious dates a
    dedicated library (hijri-converter) should be used.
    """
    jd = _gregorian_to_jd(g_date.year, g_date.month, g_date.day)
    raw = jd - 1948440 + 10632
    n = math.floor((raw - 1) / 10631)
    raw = raw - 10631 * n + 354
    j = (math.floor((10985 - raw) / 5316)) * (math.floor((50 * raw) / 17719)) + (math.floor(raw / 5670)) * (math.floor((43 * raw) / 15238))
    raw = raw - (math.floor((30 - j) / 15)) * (math.floor((17719 * j) / 50)) - (math.floor(j / 16)) * (math.floor((15238 * j) / 43)) + 29
    hm = math.floor((24 * raw) / 709)
    hd = raw - math.floor((709 * hm) / 24)
    hy = 30 * n + j - 30
    return hy, hm, hd


def _gregorian_to_jd(gy: int, gm: int, gd: int) -> int:
    """Convert Gregorian date to Julian Day Number."""
    if gm <= 2:
        gy -= 1
        gm += 12
    a = math.floor(gy / 100)
    b = 2 - a + math.floor(a / 4)
    return math.floor(365.25 * (gy + 4716)) + math.floor(30.6001 * (gm + 1)) + gd + b - 1524


# ---------------------------------------------------------------------------
# Pattern → formatted string
# ---------------------------------------------------------------------------

def _resolve_format_template(fmt: str | None, fallback: str) -> str:
    return fmt if fmt else fallback


def _format_date_core(
    y: int, m: int, d: int,
    weekday: int,
    format_str: str,
    month_names_full: list[str],
    month_names_short: list[str],
    weekday_names_full: list[str],
    weekday_names_short: list[str],
) -> str:
    """Core date formatter — calendar-agnostic once (y,m,d,weekday) is resolved."""
    result = format_str
    result = result.replace("YYYY", str(y).zfill(4))
    result = result.replace("YY", str(y % 100).zfill(2))
    result = result.replace("MM", str(m).zfill(2))
    result = result.replace("DD", str(d).zfill(2))
    if month_names_full and 1 <= m <= 12:
        result = result.replace("MMMM", month_names_full[m - 1])
    if month_names_short and 1 <= m <= 12:
        result = result.replace("MMM", month_names_short[m - 1])
    if weekday_names_full and 0 <= weekday <= 6:
        result = result.replace("DDDD", weekday_names_full[weekday])
    if weekday_names_short and 0 <= weekday <= 6:
        result = result.replace("ddd", weekday_names_short[weekday])
    return result


def _format_time_core(h: int, mi: int, s: int, format_str: str) -> str:
    result = format_str
    result = result.replace("HH", str(h).zfill(2))
    result = result.replace("hh", str(((h - 1) % 12) + 1).zfill(2))
    result = result.replace("mm", str(mi).zfill(2))
    result = result.replace("ss", str(s).zfill(2))
    result = result.replace("A", "AM" if h < 12 else "PM")
    return result


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def format_date(
    value: date | datetime | None,
    *,
    locale: Locale | None = None,
    locale_code: str | None = None,
    user: Any = None,
    tenant: Any = None,
) -> str:
    """Format a date according to the resolved locale's calendar and patterns.

    Args:
        value: A ``date`` or ``datetime``. ``None`` returns ``""``.
        locale: Explicit locale override.
        locale_code: BCP 47 code to resolve a locale, e.g. ``"fa-IR"``.
        user: Request user for locale inheritance.
        tenant: Tenant for locale inheritance.

    Returns:
        Formatted date string, e.g. ``"1405/03/23"`` for Jalali.
    """
    if value is None:
        return ""

    if isinstance(value, datetime):
        value = value.date()

    loc = locale or resolve_locale(
        locale_code=locale_code, user=user, tenant=tenant,
    )
    cal = loc.effective_calendar
    date_fmt = _resolve_format_template(loc.date_format, "YYYY-MM-DD")

    if cal == CalendarSystem.JALALI:
        jy, jm, jd = gregorian_to_jalali(value)
        weekday = jalali_weekday(value)
        return _format_date_core(
            jy, jm, jd, weekday, date_fmt,
            _JALALI_MONTH_NAMES, _JALALI_MONTH_NAMES_SHORT,
            _JALALI_WEEKDAY_NAMES, _JALALI_WEEKDAY_NAMES_SHORT,
        )

    if cal == CalendarSystem.HIJRI:
        hy, hm, hd = gregorian_to_hijri(value)
        weekday = value.weekday()
        return _format_date_core(
            hy, hm, hd, weekday, date_fmt,
            _HIJRI_MONTH_NAMES, _HIJRI_MONTH_NAMES_SHORT,
            _HIJRI_WEEKDAY_NAMES, _HIJRI_WEEKDAY_NAMES_SHORT,
        )

    # Gregorian / Fiscal — use Python stdlib
    weekday = value.weekday()
    return _format_date_core(
        value.year, value.month, value.day, weekday, date_fmt,
        [], [], [], [],
    )


def format_datetime(
    value: datetime | None,
    *,
    locale: Locale | None = None,
    locale_code: str | None = None,
    user: Any = None,
    tenant: Any = None,
) -> str:
    """Format a datetime according to the resolved locale.

    The date portion respects the locale's calendar (Gregorian/Jalali/Hijri)
    and the time portion uses the locale's time format.
    """
    if value is None:
        return ""

    loc = locale or resolve_locale(
        locale_code=locale_code, user=user, tenant=tenant,
    )

    formatted_date = format_date(value, locale=loc)
    formatted_time = format_time(value.time(), locale=loc)

    return f"{formatted_date} {formatted_time}"


def format_time(
    value: time | None,
    *,
    locale: Locale | None = None,
    locale_code: str | None = None,
    user: Any = None,
    tenant: Any = None,
) -> str:
    """Format a time according to the resolved locale."""
    if value is None:
        return ""

    loc = locale or resolve_locale(
        locale_code=locale_code, user=user, tenant=tenant,
    )
    time_fmt = _resolve_format_template(loc.time_format, "HH:mm")
    return _format_time_core(value.hour, value.minute, value.second, time_fmt)


def format_number(
    value: int | float | Decimal | None,
    *,
    locale: Locale | None = None,
    locale_code: str | None = None,
    user: Any = None,
    tenant: Any = None,
    decimal_places: int | None = None,
) -> str:
    """Format a number with locale-aware decimal and thousand separators.

    Args:
        value: Number to format. ``None`` returns ``""``.
        decimal_places: Override the number of decimal places. If None,
            uses the number of decimal places in the value itself.
    """
    if value is None:
        return ""

    loc = locale or resolve_locale(
        locale_code=locale_code, user=user, tenant=tenant,
    )

    dec_sep = loc.number_decimal_separator or "."
    thou_sep = loc.number_thousand_separator or ","

    if isinstance(value, Decimal):
        num_str = format(value, "f")
    elif isinstance(value, float):
        num_str = f"{value:.{decimal_places}f}" if decimal_places is not None else f"{value:g}"
        if "e" in num_str.lower():
            num_str = f"{value:.6f}".rstrip("0").rstrip(".")
    else:
        num_str = str(value)

    # Split integer and fractional parts
    if "." in num_str:
        int_part, frac_part = num_str.split(".", 1)
    else:
        int_part, frac_part = num_str, ""

    # Apply thousand separator
    sign = ""
    if int_part.startswith("-"):
        sign = "-"
        int_part = int_part[1:]

    result_parts = []
    for i, ch in enumerate(reversed(int_part)):
        if i > 0 and i % 3 == 0:
            result_parts.append(thou_sep)
        result_parts.append(ch)
    formatted = sign + "".join(reversed(result_parts))

    # Apply numeral system
    formatted = _apply_numeral_system(formatted, loc.effective_numeral_system)
    sep_display = _apply_numeral_system(dec_sep, loc.effective_numeral_system)

    if frac_part:
        frac_display = _apply_numeral_system(frac_part, loc.effective_numeral_system)
        formatted = f"{formatted}{sep_display}{frac_display}"

    return formatted


def format_currency(
    amount: int | float | Decimal | None,
    *,
    locale: Locale | None = None,
    locale_code: str | None = None,
    user: Any = None,
    tenant: Any = None,
    currency_code: str | None = None,
    show_symbol: bool = True,
    show_code: bool = False,
) -> str:
    """Format a monetary amount with locale-aware separators and currency symbol.

    Args:
        amount: The amount. ``None`` returns ``""``.
        currency_code: Override the currency (ISO 4217). Defaults to the locale's currency.
        show_symbol: Append/prepend the currency symbol (e.g. ``$``).
        show_code: Append the currency code (e.g. ``USD``). Ignored if ``show_symbol=True``.

    Returns:
        Formatted string, e.g. ``"۱۲٬۳۴۵٬۶۷۸ ﷼"`` for IRR in fa-IR.
    """
    if amount is None:
        return ""

    loc = locale or resolve_locale(
        locale_code=locale_code, user=user, tenant=tenant,
    )

    cur_code = currency_code or loc.currency_code or "USD"
    currency = get_currency_by_code(cur_code)
    if currency is None:
        currency = get_currency_by_code("USD")
    decimal_places = currency.decimals if currency else 2

    number_str = format_number(
        amount, locale=loc, decimal_places=decimal_places,
    )

    if show_symbol and currency:
        symbol = currency.symbol or currency.code
        return f"{number_str} {symbol}"
    if show_code and currency:
        return f"{number_str} {currency.code}"

    return number_str


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_NUMERAL_MAP: dict[str, str] = {
    "0": "\u06f0", "1": "\u06f1", "2": "\u06f2", "3": "\u06f3",
    "4": "\u06f4", "5": "\u06f5", "6": "\u06f6", "7": "\u06f7",
    "8": "\u06f8", "9": "\u06f9",
}

_EASTERN_ARABIC_MAP: dict[str, str] = {
    "0": "\u0660", "1": "\u0661", "2": "\u0662", "3": "\u0663",
    "4": "\u0664", "5": "\u0665", "6": "\u0666", "7": "\u0667",
    "8": "\u0668", "9": "\u0669",
}


def _apply_numeral_system(text: str, system: str) -> str:
    if system == NumeralSystem.PERSIAN:
        return "".join(_NUMERAL_MAP.get(ch, ch) for ch in text)
    if system == NumeralSystem.EASTERN_ARABIC:
        return "".join(_EASTERN_ARABIC_MAP.get(ch, ch) for ch in text)
    return text


def jalali_weekday(g_date: date) -> int:
    """Return Jalali weekday: 0=Shanbeh, ..., 6=Jomeh."""
    return (g_date.weekday() + 2) % 7


def is_leap_jalali(year: int) -> bool:
    """Check if a Jalali year is a leap year."""
    if year <= 0:
        return False
    cycles = [1, 5, 9, 13, 17, 22, 26, 30]
    return (year % 33) in cycles
