"""Μοντέλο εμβέλειας του χάρτη #range-map (spec 2026-10-20-range-map-design.md §3). Καθαρές συναρτήσεις, χωρίς I/O.

Για κάθε αεροσκάφος, μήκος διαδρόμου L και συνθήκη (isa / isa15):
1. TOW = min(MTOW, βάρος που επιτρέπει η καμπύλη απογείωσης του κατασκευαστή στο L). Κάτω από την αρχή της
   καμπύλης: «εκτός στοιχείων» (καμία προέκταση)· πέρα από το τέλος της: το τελευταίο βάρος της καμπύλης.
2. Φορτίο = όλες οι θέσεις × (επιβάτης + αποσκευή) κατά τις πρότυπες μάζες EASA.
3. Καύσιμο = min(χωρητικότητα, TOW − OEW − φορτίο).
4. Εμβέλεια: Breguet R = k·ln(W0 / (W0 − (F − Fr))), με k και αποθέματα Fr βαθμονομημένα ώστε η καμπύλη να περνά
   από τα δύο γωνιακά σημεία του διαγράμματος φορτίου-εμβέλειας στο MTOW (τέλος plateau, σημείο μέγιστου καυσίμου).
"""
import math

from build import fuel_model

NM_KM = 1.852
# Κάτω από αυτή την εμβέλεια (κοντινότερο αεροδρόμιο του χάρτη ~100 km) το αποτέλεσμα παρουσιάζεται ως «δεν φεύγει γεμάτο»:
# τα καύσιμα που περισσεύουν μετά τα αποθέματα δεν φτάνουν πουθενά (έλεγχος Fable r2, 737-800 · ~1.300 μ. · ISA: 40 km).
MIN_RANGE_KM = 100
COND_CURVE = {"isa": "tofl_isa", "isa15": "tofl_isa15"}
LIMIT = {"runway": "όριο διαδρόμου", "mtow": "μέγιστο βάρος απογείωσης", "fuel": "γεμάτες δεξαμενές",
         "chart": "τέλος της καμπύλης του κατασκευαστή"}


def t1(kg):
    """kg → «73,5 t»"""
    return f"{kg / 1000:.1f}".replace(".", ",") + " t"


def num(n):
    """1799 → «1.799»"""
    return f"{round(n):,}".replace(",", ".")


def _interp(x, x1, y1, x2, y2):
    return y1 if x2 == x1 else y1 + (y2 - y1) * (x - x1) / (x2 - x1)


def tow_limit(curve, L, mtow):
    """Μέγιστο βάρος απογείωσης για διαθέσιμο μήκος L, από καμπύλη [[kg, m], ...]. None = εκτός στοιχείων."""
    if not curve:
        return None
    c = sorted(curve, key=lambda p: (p[1], p[0]))
    if len(c) == 1:                                   # ένα σημείο (fact sheet): ισχύει μόνο αν χωράει το MTOW
        w, m = c[0]
        return mtow if (L >= m and w >= mtow) else None
    if L < c[0][1]:
        return None
    if L >= c[-1][1]:
        return min(mtow, max(p[0] for p in c))
    for (w1, m1), (w2, m2) in zip(c, c[1:]):
        if m1 <= L <= m2:
            return min(mtow, _interp(L, m1, w1, m2, w2))
    return None


def chart_range_nm(pr, payload):
    """Εμβέλεια (ν.μ.) για φορτίο payload πάνω στο διάγραμμα φορτίου-εμβέλειας στο MTOW (γραμμική παρεμβολή)."""
    pts = sorted(pr, key=lambda p: p[1])
    if payload > max(p[0] for p in pts):
        return None
    best = None
    for (p1, r1), (p2, r2) in zip(pts, pts[1:]):
        if min(p1, p2) <= payload <= max(p1, p2):
            best = max(best or 0, _interp(payload, p1, r1, p2, r2) if p1 != p2 else max(r1, r2))
    return best


def calibrate(mtow, oew, pr, fuel_cap):
    """(k, Fr) από τα γωνιακά σημεία του διαγράμματος στο MTOW, ή None αν το διάγραμμα έχει < 2 σημεία."""
    pts = [p for p in pr if p[1] > 0]
    if len(pts) < 2:
        return None
    pmax = max(p[0] for p in pts)
    pa = max((p for p in pts if p[0] >= 0.98 * pmax), key=lambda p: p[1])          # τέλος plateau
    rest = [p for p in pts if p[0] < 0.98 * pmax and mtow - oew - p[0] <= fuel_cap * 1.05]
    if not rest:
        return None
    pb = max(rest, key=lambda p: p[0])                                             # σημείο μέγιστου καυσίμου
    fa, fb = mtow - oew - pa[0], mtow - oew - pb[0]

    def k_of(fr, f, r):
        return r / math.log(mtow / (mtow - (f - fr)))

    lo, hi = 0.0, min(fa, fb) * 0.95
    g = lambda fr: k_of(fr, fa, pa[1]) - k_of(fr, fb, pb[1])
    for _ in range(200):
        mid = (lo + hi) / 2
        if (g(mid) > 0) == (g(lo) > 0):
            lo = mid
        else:
            hi = mid
    fr = (lo + hi) / 2
    return k_of(fr, fa, pa[1]), fr


def _landing_at(curve, w):
    if not curve:
        return None
    c = sorted(curve)
    if w <= c[0][0]:
        return c[0][1]
    for (w1, m1), (w2, m2) in zip(c, c[1:]):
        if w1 <= w <= w2:
            return _interp(w, w1, m1, w2, m2)
    return c[-1][1]


def result(ac, L, cond, masses, policy):
    pay = masses["pax_kg"] + masses["bag_kg"]
    seats, mtow, oew = ac["seats"], ac["mtow_kg"], ac["oew_kg"]
    payload = seats * pay
    land = _landing_at(ac.get("landing_isa"), ac["mlw_kg"])
    base = {"pax": seats, "payload_kg": payload,
            "landing_m_at_mlw": round(land) if land else None,
            "lands_at_mlw": (land <= L) if land else None}
    curve = ac.get(COND_CURVE[cond])
    if cond == "isa15" and not curve:
        # Ο κατασκευαστής δεν δίνει καμπύλη για ζέστη (ATR, A319ceo): τα στοιχεία της πρότυπης θερμοκρασίας ως ανώτερο όριο.
        std = result(ac, L, "isa", masses, policy)
        if std["state"] == "range" and not std.get("min"):
            return {**std, "hot_fallback": True,
                    "method": "Ο κατασκευαστής δεν δίνει στοιχεία για ζέστη· χρησιμοποιούνται τα στοιχεία της πρότυπης "
                              "θερμοκρασίας ως ανώτερο όριο (το καλοκαίρι η εμβέλεια με όλες τις θέσεις είναι μικρότερη). " + std["method"]}
    tow = tow_limit(curve, L, mtow)
    if tow is None:
        # Φύλλο ATR: μήκος απογείωσης για πτήση 300 ν.μ. με όλες τις θέσεις (στάθμη θάλασσας, ISA). Το βάρος δεν δημοσιεύεται,
        # γι' αυτό η εμβέλεια δίνεται μόνο ως ελάχιστη (min: True) και μόνο στην πρότυπη θερμοκρασία. Καμία προέκταση.
        m300 = ac.get("full_pax_300nm_m")
        if curve and m300 and cond == "isa" and L >= m300:
            return {**base, "state": "range", "min": True, "range_km": round(300 * NM_KM / 10) * 10, "limited_by": "runway",
                    "method": f"Φύλλο του κατασκευαστή: για πτήση 300 ν.μ. (περίπου 556 km) με όλες τις θέσεις αρκούν {num(m300)} μ. "
                              f"(στάθμη θάλασσας, πρότυπη θερμοκρασία). Το βάρος απογείωσης δεν δημοσιεύεται, άρα η εμβέλεια "
                              f"δίνεται ως ελάχιστη· πόσο παραπάνω φτάνει με {num(L)} μ. δεν προκύπτει από τα στοιχεία."}
        why = ("ο κατασκευαστής δεν δίνει καμπύλη για αυτή τη θερμοκρασία." if not curve else
               f"η καμπύλη του κατασκευαστή αρχίζει στα {num(min(p[1] for p in curve))} μ.")
        return {**base, "state": "no_data", "reason": "temp" if not curve else "length",
                "method": f"Εκτός στοιχείων κατασκευαστή: {why}"}
    limited_by = "mtow" if tow >= mtow else ("chart" if tow >= max(p[0] for p in curve) else "runway")
    fm = fuel_params(ac, policy)
    if fm is None:
        return {**base, "state": "no_data", "reason": "length", "tow_kg": round(tow),
                "method": "Εκτός στοιχείων κατασκευαστή: δεν υπάρχουν στοιχεία καυσίμου για αυτόν τον τύπο."}
    k, o, v, hf, pol = fm
    fuel = min(ac["fuel_cap_kg"], tow - oew - payload)
    weights = {"oew": round(oew), "pax": round(seats * masses["pax_kg"]), "bags": round(seats * masses["bag_kg"]),
               "tow": round(tow), "mtow": mtow, "fuel_cap": ac["fuel_cap_kg"]}
    r_nm = fuel_model.max_range(tow, fuel, k, o, v, hf, pol) if fuel > 0 else None
    if r_nm is None:
        need = fuel_model.breakdown(tow, 0, k, o, v, hf, pol)
        return {**base, "state": "cannot", "tow_kg": round(tow), "limited_by": limited_by,
                "weights": {**weights, "fuel": round(max(fuel, 0)), "reserves_min": round(need["total"])},
                "method": f"Βάρος απογείωσης {t1(tow)}· με {seats} επιβάτες και τις αποσκευές τους μένουν για καύσιμα "
                          f"{t1(max(fuel, 0))}, λιγότερα από όσα χρειάζονται μόνο τα αποθέματα ασφαλείας ({t1(need['total'])})."}
    if r_nm * NM_KM < MIN_RANGE_KM:
        need = fuel_model.breakdown(tow, 0, k, o, v, hf, pol)
        return {**base, "state": "cannot", "tow_kg": round(tow), "limited_by": limited_by,
                "weights": {**weights, "fuel": round(fuel), "reserves_min": round(need["total"])},
                "method": f"Βάρος απογείωσης {t1(tow)} ({LIMIT[limited_by]})· με {seats} επιβάτες και τις αποσκευές τους μένουν για καύσιμα "
                          f"{t1(fuel)}· μετά τα αποθέματα ασφαλείας ({t1(need['total'])}) περισσεύουν καύσιμα για περίπου "
                          f"{num(r_nm * NM_KM)} km, απόσταση που δεν φτάνει έως κανένα αεροδρόμιο του χάρτη."}
    b = fuel_model.breakdown(tow, r_nm, k, o, v, hf, pol)
    if fuel >= ac["fuel_cap_kg"]:
        limited_by = "fuel"
    return {**base, "state": "range", "range_km": round(r_nm * NM_KM / 10) * 10, "tow_kg": round(tow),
            "fuel_kg": round(fuel), "limited_by": limited_by,
            "weights": {**weights, "fuel": round(fuel), **{x: round(b[x]) for x in ("trip", "contingency", "alternate", "final_reserve")}},
            "method": (f"Βάρος απογείωσης {t1(tow)} ({LIMIT[limited_by]})· φορτίο {seats} × {masses['pax_kg'] + masses['bag_kg']} kg = {t1(payload)}· "
                       f"καύσιμα {t1(fuel)}: καύσιμα ταξιδιού {t1(b['trip'])}, για απρόβλεπτα {t1(b['contingency'])}, "
                       f"για εναλλακτικό αεροδρόμιο {t1(b['alternate'])}, τελικό απόθεμα {t1(b['final_reserve'])}.")}


_CAL = {}


def fuel_params(ac, policy):
    """(k, O, V, συντελεστής αναμονής, αποθέματα ΕΕ) για τον τύπο, ή None. Αποθηκεύεται ανά τύπο."""
    if ac["id"] in _CAL:
        return _CAL[ac["id"]]
    fm = ac.get("fuel_model") or {}
    v, hf = fm["speed_kt"], policy["hold_factor"][fm["kind"]]
    eu = {"cont_pct": policy["cont_pct"], "cont_min_hold": policy["cont_min_hold"],
          "alt_nm": policy["alt_nm"], "final_hold_min": policy["final_hold_min"]}
    mtow, oew = ac["mtow_kg"], ac["oew_kg"]
    cal = None
    if fm.get("trip_points"):
        zfw = ac.get("mzfw_kg") or mtow
        pts = [(min(mtow, zfw + f), r, f) for r, f in fm["trip_points"]]
        cal = fuel_model.calibrate(pts, v, hf, {})
    else:
        corners = chart_corners(ac["payload_range"], mtow, oew, ac["fuel_cap_kg"])
        if corners:
            pts = [(mtow, r, f) for r, f in corners]
            cal = fuel_model.calibrate(pts, v, hf, fm["chart_reserves"])
    _CAL[ac["id"]] = (cal[0], cal[1], v, hf, eu) if cal else None
    return _CAL[ac["id"]]


def chart_corners(pr, mtow, oew, fuel_cap):
    """[(εμβέλεια ν.μ., καύσιμο kg)] για το τέλος του plateau και το σημείο μέγιστου καυσίμου στο MTOW."""
    pts = [p for p in pr if p[1] > 0]
    if len(pts) < 2:
        return None
    pmax = max(p[0] for p in pts)
    pa = max((p for p in pts if p[0] >= 0.98 * pmax), key=lambda p: p[1])
    rest = [p for p in pts if p[0] < 0.98 * pmax and mtow - oew - p[0] <= fuel_cap * 1.05]
    if not rest:
        return None
    pb = max(rest, key=lambda p: p[0])
    return [(pa[1], min(fuel_cap, mtow - oew - pa[0])), (pb[1], min(fuel_cap, mtow - oew - pb[0]))]


def compute(doc):
    masses = doc["masses"]
    policy = policy_of(doc)
    lengths = [x["m"] for x in doc["lengths"]]
    return {"lengths": lengths,
            "results": {a["id"]: {str(L): {c: result(a, L, c, masses, policy) for c in COND_CURVE} for L in lengths}
                        for a in doc["aircraft"]}}


def policy_of(doc):
    """Αποθέματα ΕΕ (fuel_policy) + δικές μας παραδοχές (assumptions) σε ένα λεξικό."""
    fp, asm = doc["fuel_policy"], doc["assumptions"]
    return {"cont_pct": fp["cont_pct"], "cont_min_hold": fp["cont_min_hold"], "final_hold_min": fp["final_hold_min"],
            "alt_nm": asm["alt_nm"], "hold_factor": asm["hold_factor"]}
