"""P4 authored teaching arithmetic. Stdlib; stdout only; no hardware access."""
import argparse
import json
import re
import stat
import sys
from decimal import Decimal, ROUND_HALF_UP, localcontext
from fractions import Fraction
from pathlib import Path

SCHEMA = "p4-toy-1"
IDS = ([f"CAM{i:02d}" for i in range(1, 6)]
       + [f"DATA{i:02d}" for i in range(1, 8)]
       + [f"PAY{i:02d}" for i in range(1, 6)])
CAMERA = {"target_mm", "gsd_mm_per_px", "min_samples", "image_count",
          "bytes_per_image", "capacity_bytes"}
DATA = {"age_s", "payload_bytes", "bitrate_bps", "deadline_age_s",
        "ttl_s", "connected", "plan_version"}
PAYLOAD = {"components", "mass_limit_kg", "cg_limit_m", "capacity_wh",
           "reserve_fraction", "required_wh"}


def require(condition, message):
    if not condition:
        raise ValueError(message)


def number_token(token):
    # Bound lexeme and exponent before Decimal/Fraction can allocate powers.
    require(len(token) <= 32, "numeric token longer than 32 characters")
    match = re.fullmatch(r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE]([+-]?[0-9]+))?", token)
    require(match is not None, "invalid number")
    require(match.group(1) is None or abs(int(match.group(1))) <= 12,
            "numeric exponent outside -12..12")
    value = Decimal(token)
    require(value.is_finite(), "nonfinite number")
    return value


def no_constant(_):
    raise ValueError("nonfinite constant")


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        require(key not in result, "duplicate JSON key")
        result[key] = value
    return result


def exact_keys(obj, keys):
    require(type(obj) is dict and set(obj) == keys, "missing or unknown fields")


def numeric(value, low, high, *, nullable=False, positive=False, integer=False):
    if value is None:
        require(nullable, "null not allowed")
        return
    require(type(value) is Decimal, "number required (booleans are not numbers)")
    require(low <= value <= high and (not positive or value > 0), "number outside parser bounds")
    require(not integer or value == value.to_integral_value(), "integer required")


def validate_case(case, expected_id):
    exact_keys(case, {"case_id", "model", "parameters"})
    require(case["case_id"] == expected_id, "case IDs/order must match the 17-case contract")
    model = "camera" if expected_id.startswith("CAM") else "data" if expected_id.startswith("DATA") else "payload"
    require(case["model"] == model, "model does not match case ID")
    p = case["parameters"]
    exact_keys(p, {"camera": CAMERA, "data": DATA, "payload": PAYLOAD}[model])
    if model == "camera":
        for key in ("target_mm", "gsd_mm_per_px", "min_samples"):
            numeric(p[key], 0, 10000, positive=True, nullable=key == "gsd_mm_per_px")
        numeric(p["image_count"], 0, 10000, positive=True, integer=True)
        for key in ("bytes_per_image", "capacity_bytes"):
            numeric(p[key], 0, 1000000000, positive=True, integer=True)
    elif model == "data":
        numeric(p["age_s"], 0, 1000000)
        for key in ("deadline_age_s", "ttl_s"):
            numeric(p[key], 0, 1000000, positive=True)
        numeric(p["payload_bytes"], 0, 1000000000, positive=True, integer=True)
        numeric(p["bitrate_bps"], 0, 1000000, positive=True, nullable=True)
        require(type(p["connected"]) is bool, "connected must be boolean")
        require(p["plan_version"] in ("PLAN0", "PLAN1"), "plan version must be PLAN0 or PLAN1")
    else:
        require(type(p["components"]) is list and len(p["components"]) == 3,
                "exactly three components required")
        for component, identifier in zip(p["components"], ("FRAME", "BATTERY", "PAYLOAD")):
            exact_keys(component, {"id", "mass_kg", "x_m"})
            require(component["id"] == identifier, "component IDs/order must be FRAME BATTERY PAYLOAD")
            numeric(component["mass_kg"], 0, 10000, positive=True, nullable=identifier == "PAYLOAD")
            numeric(component["x_m"], -1000, 1000)
        numeric(p["mass_limit_kg"], 0, 10000, positive=True)
        numeric(p["cg_limit_m"], 0, 1000, positive=True)
        for key in ("capacity_wh", "required_wh"):
            numeric(p[key], 0, 1000000, positive=True)
        numeric(p["reserve_fraction"], 0, 1)
        require(p["reserve_fraction"] < 1, "reserve must be less than one")


def load_fixture(path):
    # Reject network-like paths and non-regular inputs before opening.
    raw_path = str(path)
    require(not raw_path.startswith(("//", "\\\\")) and "://" not in raw_path,
            "fixture must be a local path")
    require(path.suffix.lower() == ".json", "fixture must have .json extension")
    info = path.stat()
    require(stat.S_ISREG(info.st_mode) and info.st_size <= 65536,
            "fixture must be a regular JSON file of at most 64 KiB")
    with path.open("rb") as handle:
        content = handle.read(65537)
    require(len(content) <= 65536, "fixture exceeds 64 KiB")
    doc = json.loads(content.decode("utf-8"), parse_int=number_token,
                     parse_float=number_token, parse_constant=no_constant,
                     object_pairs_hook=unique_object)
    exact_keys(doc, {"schema_version", "evidence_basis", "cases"})
    require(doc["schema_version"] == SCHEMA, "wrong schema version")
    require(doc["evidence_basis"] == "authored_teaching_parameters", "wrong evidence basis")
    require(type(doc["cases"]) is list and len(doc["cases"]) == 17, "exactly 17 cases required")
    for case, identifier in zip(doc["cases"], IDS):
        validate_case(case, identifier)
    return doc


def rounded(value):
    if value is None or type(value) is bool:
        return value
    fraction = Fraction(value)
    if fraction.denominator == 1:
        return fraction.numerator
    with localcontext() as context:
        context.prec = 64
        decimal = (Decimal(fraction.numerator) / Decimal(fraction.denominator))
        return float(decimal.quantize(Decimal("0.000000001"), rounding=ROUND_HALF_UP))


def calculate(case):
    p = case["parameters"]
    def f(key):
        return Fraction(p[key])
    if case["model"] == "camera":
        samples = None if p["gsd_mm_per_px"] is None else f("target_mm") / f("gsd_mm_per_px")
        total = f("image_count") * f("bytes_per_image")
        sampling_ok = None if samples is None else samples >= f("min_samples")
        storage_ok = total <= f("capacity_bytes")
        metrics = {"samples": samples, "sampling_ok": sampling_ok,
                   "total_bytes": total, "storage_ok": storage_ok}
        disposition = "unknown" if samples is None else "numeric_fit" if sampling_ok and storage_ok else "outside_bounds"
    elif case["model"] == "data":
        duration = None if p["bitrate_bps"] is None else f("payload_bytes") * 8 / f("bitrate_bps")
        arrival = None if duration is None else f("age_s") + duration
        metrics = {"serialization_s": duration, "arrival_age_s": arrival}
        if duration is None:
            disposition = "unknown"
        elif not p["connected"]:
            disposition = "offline_hold"
        elif p["plan_version"] != "PLAN1":
            disposition = "stale_plan"
        elif arrival >= f("ttl_s"):
            disposition = "expired"
        elif arrival > f("deadline_age_s"):
            disposition = "deadline_missed"
        else:
            disposition = "numeric_fit"
    else:
        known = all(item["mass_kg"] is not None for item in p["components"])
        total = sum(Fraction(item["mass_kg"]) for item in p["components"]) if known else None
        cg = sum(Fraction(item["mass_kg"]) * Fraction(item["x_m"]) for item in p["components"]) / total if known else None
        usable = f("capacity_wh") * (1 - f("reserve_fraction"))
        mass_ok = None if total is None else total <= f("mass_limit_kg")
        cg_ok = None if cg is None else abs(cg) <= f("cg_limit_m")
        energy_ok = f("required_wh") <= usable
        metrics = {"total_mass_kg": total, "cg_m": cg, "mass_ok": mass_ok,
                   "cg_ok": cg_ok, "usable_wh": usable, "energy_ok": energy_ok}
        disposition = "unknown" if not known else "numeric_fit" if mass_ok and cg_ok and energy_ok else "outside_bounds"
    return {"case_id": case["case_id"], "model": case["model"],
            "disposition": disposition,
            "metrics": {key: rounded(value) for key, value in metrics.items()}}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--fixture", type=Path, default=Path(__file__).with_name("fixture.json"))
    args = parser.parse_args()
    try:
        fixture = load_fixture(args.fixture)
        results = [calculate(case) for case in fixture["cases"]]
        output = {"schema_version": SCHEMA, "evidence_level": "host_calculation",
                  "results": results, "physical": "NOT_RUN", "learner": "NOT_RUN"}
        print(json.dumps(output, ensure_ascii=True, allow_nan=False, indent=2))
    except (OSError, ValueError, TypeError, RecursionError, ArithmeticError):
        print("Invalid fixture: use the documented local p4-toy-1 schema and parser bounds.",
              file=sys.stderr)
        return 2
    return 0


if __name__ == "__main__":
    sys.exit(main())
