"""
CA Automator — AD/BS date service. Single source of truth for every AD <->
Bikram Sambat conversion in the app (dashboard, notifications, contracts,
documents, appointments, reports, POS, blog, careers, audit/accounting
dates) — every module must call through here, never reimplement the math.

There is no clean formula for the Nepali calendar — month lengths vary
year to year by official notice, so a correct converter needs a real
reference table, not a fixed "~30 days a month" approximation. This module
is backed by the `nepali-datetime` package (MIT, zero dependencies), whose
BS 1975-2100 (~AD 1918-2044) reference table is the same official-calendar
data every mainstream Nepali-date library ships. That range comfortably
covers a CA practice's historical records and any realistic forward
horizon; a date outside it raises ValueError rather than guessing.

Canonical storage rule for callers: every date column in the database
stays plain ISO AD ("YYYY-MM-DD") — conversion happens only at the
presentation/input boundary (an API response, a rendered page, a form).
Never store a BS string as if it were the source of truth.
"""
from datetime import date

import nepali_datetime

NEPALI_MONTHS_CALENDAR = ["Baishakh", "Jestha", "Ashadh", "Shrawan", "Bhadra", "Ashwin",
                          "Kartik", "Mangsir", "Poush", "Magh", "Falgun", "Chaitra"]
NEPALI_MONTHS_FISCAL = ["Shrawan", "Bhadra", "Ashwin", "Kartik", "Mangsir", "Poush",
                        "Magh", "Falgun", "Chaitra", "Baishakh", "Jestha", "Ashadh"]

MIN_AD = date(1918, 4, 13)   # nepali_datetime's floor: BS 1975-01-01
MAX_AD = date(2044, 4, 12)   # nepali_datetime's ceiling: BS 2100-12-30ish


def _parse(ad_date):
    if isinstance(ad_date, date):
        return ad_date
    return date.fromisoformat(str(ad_date)[:10])


def bs_approx(ad_date):
    """
    Returns {"year", "month", "day", "month_name", "label"} for the exact
    Bikram Sambat date an AD date falls on. Kept as `bs_approx` (not
    renamed) so every existing caller across app.py/engines.py/db.py keeps
    working unchanged — the name is legacy, the math underneath is now
    exact, not approximate.

    Raises ValueError if ad_date is outside the supported BS 1975-2100
    range (roughly AD 1918-2044).
    """
    d = _parse(ad_date)
    try:
        bs = nepali_datetime.date.from_datetime_date(d)
    except OverflowError:
        raise ValueError(f"{d.isoformat()} is outside the supported BS 1975-2100 range")
    return {
        "year": bs.year, "month": bs.month, "day": bs.day,
        "month_name": NEPALI_MONTHS_CALENDAR[bs.month - 1],
        "label": f"{bs.year}-{bs.month:02d}-{bs.day:02d}",
    }


def bs_to_ad_approx(bs_year, bs_month, bs_day):
    """
    The inverse of bs_approx — exact, not the old equal-30-day guess.
    Raises ValueError if the BS date doesn't exist (e.g. day 31 in a
    30-day BS month, or a year outside 1975-2100) — callers that expose
    this over an API must catch ValueError and return a clean 400 rather
    than let it propagate as a 500.
    """
    bs = nepali_datetime.date(bs_year, bs_month, bs_day)
    return bs.to_datetime_date().isoformat()


def fy_label_for_ad(ad_date, fiscal_years=None):
    """
    fiscal_years: rows with start_ad/end_ad/label_bs (from db.query("SELECT *
    FROM fiscal_year")); pass None to always use the calendar-derived
    fallback below.
    """
    d = _parse(ad_date)
    for fy in (fiscal_years or []):
        try:
            if fy["start_ad"] and fy["end_ad"] and _parse(fy["start_ad"]) <= d <= _parse(fy["end_ad"]):
                return fy["label_bs"]
        except (TypeError, ValueError):
            continue
    # Fallback when no fiscal_year row covers this date: derive the Nepali
    # FY directly from the real BS date rather than an AD-side "mid-July"
    # guess. The Nepali FY runs Shrawan 1 through the following Ashadh end
    # — BS month index 4 (Shrawan) is the first month of a new FY.
    try:
        bs = nepali_datetime.date.from_datetime_date(d)
    except (ValueError, OverflowError):
        # Outside the supported BS range — fall back to the old heuristic
        # rather than raise, since this is a display label, not a hard stop.
        cutoff = date(d.year, 7, 16)
        bs_start_year = (d.year + 57) if d >= cutoff else (d.year + 56)
        return f"{bs_start_year}-{str(bs_start_year + 1)[-2:]}"
    bs_start_year = bs.year if bs.month >= 4 else bs.year - 1
    return f"{bs_start_year}-{str(bs_start_year + 1)[-2:]}"
