"""Validate direct owner measurements before applying them to the CAD study.

This module has no CAD dependency so a measurement log can be checked before
installing CadQuery or generating geometry.
"""

from __future__ import annotations

import json
import math
from pathlib import Path


LIMITS = {
    "x": (80, 200, "rocker_cover_width"),
    "y": (35, 110, "cover_height"),
    "z": (250, 500, "rocker_cover_length"),
    "studCenterSpacing": (50, 300, "cover_stud_spacing"),
    "capSeatDiameter": (20, 50, "filler_opening_diameter"),
}
PULLEY_LIMITS = {
    "crank_pulley_seal_comparison_provisional": (110, 200, "crank_pulley_outer_diameter"),
    "water_pump_sheave_photo_reference_provisional": (55, 125, "water_pump_sheave_outer_diameter"),
    "alternator_pulley_catalog_diameter_provisional": (40, 100, "alternator_pulley_outer_diameter"),
}
RADIATOR_LIMITS = {
    "radiatorCoreWidth": (250, 600, "radiator_core_width"),
    "radiatorCoreHeight": (150, 450, "radiator_core_height"),
    "radiatorCoreDepth": (15, 100, "radiator_core_depth"),
}
FUEL_PUMP_LIMITS = {
    "pumpBarrelDiameter": (40, 105, "electric_fuel_pump_radius"),
    "pumpBarrelLength": (55, 165, "electric_fuel_pump_barrel_length"),
}
AIR_CLEANER_LIMITS = {
    "x": (60, 180, "air_cleaner_width"),
    "y": (70, 200, "air_cleaner_height"),
    "z": (120, 330, "air_cleaner_length"),
}
AIR_FILTER_LIMITS = {
    "x": (10, 65, "air_filter_width"),
    "y": (60, 180, "air_filter_height"),
    "z": (130, 315, "air_filter_length"),
}
BLOCK_OUTSIDE_LIMITS = {
    "deckCastingWidth": (142, 170, "deck_width"),
    "lowerCastingWidth": (165, 195, "block_width"),
}
CARB_COVER_LIMITS = {
    "domeOutsideDiameter": (55, 85, "carb_cover_dome_diameter"),
    "flangeOutsideWidth": (68, 110, "carb_cover_flange_width"),
    "flangeOutsideLength": (68, 110, "carb_cover_flange_length"),
}


def load_measurement_overrides(path: str | Path) -> tuple[dict[str, object], dict]:
    """Return supported direct block, rocker cover, carburetor, pulley, fan, radiator, intake and pump dimensions.

    X/Z are the outside cover body envelope. Y runs from the lower lip through
    the highest stamped body ridge, excluding the filler neck, stud seats,
    fasteners and cap. The cover gasket, cap fit, installed position and
    collision clearances remain independent.
    """
    source = Path(path)
    if source.stat().st_size > 100_000:
        raise ValueError("Measurement log exceeds 100 KB.")
    data = json.loads(source.read_text())
    if not isinstance(data, dict) or data.get("units") != "mm":
        raise ValueError("Expected a CAD measurement log in millimeters.")
    entries = data.get("entries", {})
    if not isinstance(entries, dict):
        raise ValueError("The measurement entries are malformed.")
    overrides: dict[str, object] = {}
    accepted: dict[str, dict[str, object]] = {}
    accepted_counts: dict[str, dict[str, int]] = {}
    recorded_only: dict[str, dict[str, float]] = {}
    access_observations: dict[str, dict[str, str]] = {}
    notes: dict[str, str] = {}

    def direct_entry(name: str):
        entry = entries.get(name)
        if not isinstance(entry, dict) or entry.get("method") != "direct":
            return None
        basis = entry.get("basis")
        if not isinstance(basis, str) or not basis.strip():
            raise ValueError(f"{name} needs a measurement-point and source note.")
        notes[name] = basis.strip()
        return entry

    def validated(value, low: float, high: float, label: str) -> float:
        if (isinstance(value, bool) or not isinstance(value, (int, float))
                or not math.isfinite(value) or not low <= value <= high):
            raise ValueError(f"{label} must be a number from {low} to {high} mm.")
        return float(value)

    entry = direct_entry("rocker_cover_photo_scaled")
    if entry:
        raw = {key: entry.get(key) for key in ("x", "y", "z")}
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError("The cover feature measurements are malformed.")
        raw["studCenterSpacing"] = features.get("studCenterSpacing")
        raw["capSeatDiameter"] = features.get("capSeatDiameter")
        for key, value in raw.items():
            if value is None:
                continue
            low, high, parameter = LIMITS[key]
            measured = validated(value, low, high, key)
            accepted.setdefault("rocker_cover_photo_scaled", {})[key] = measured
            overrides[parameter] = measured - 2.5 if key == "y" else measured

    block_name = "block_provisional_casting"
    entry = direct_entry(block_name)
    if entry:
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError("The block measurements are malformed.")
        for key, (low, high, parameter) in BLOCK_OUTSIDE_LIMITS.items():
            if features.get(key) is None:
                continue
            measured = validated(features[key], low, high, key)
            accepted.setdefault(block_name, {})[key] = measured
            # The old provisional lower section was P["block_width"] - 20.
            # Preserve that relationship for the separately measured section.
            overrides[parameter] = measured + 20 if key == "lowerCastingWidth" else measured
        keys = ("bore1to2", "bore2to3", "bore3to4")
        if any(features.get(key) is not None for key in keys):
            if any(features.get(key) is None for key in keys):
                raise ValueError("All three adjacent bore-center distances are needed to move the CAD bores.")
            spacings = [validated(features[key], 77, 100, key) for key in keys]
            # The provisional outer main journals remain at +/-160 mm.
            # Beyond this span the outer crankpins approach those journals;
            # the crank layout needs a new measured axial drawing first.
            if sum(spacings) > 260:
                raise ValueError("The measured bore-center span exceeds the current crank layout; verify its main-bearing stations before building.")
            accepted.setdefault(block_name, {}).update(dict(zip(keys, spacings)))
            overrides.update(dict(zip(("bore_pitch_12", "bore_pitch_23", "bore_pitch_34"), spacings)))

    # Each pulley group is concentric about its own shaft in the current
    # model, so its direct X envelope is its outside diameter. A recorded Y
    # or Z envelope cannot determine the groove profile or belt alignment.
    for name, (low, high, parameter) in PULLEY_LIMITS.items():
        entry = direct_entry(name)
        if entry and entry.get("x") is not None:
            measured = validated(entry["x"], low, high, f"{name} X outside diameter")
            accepted[name] = {"outsideDiameter": measured}
            overrides[parameter] = measured
    fan_name = "mechanical_fan_photo_reference_provisional"
    entry = direct_entry(fan_name)
    if entry:
        if entry.get("x") is not None:
            diameter = validated(entry["x"], 240, 340, "fan outside diameter")
            accepted.setdefault(fan_name, {})["outsideDiameter"] = diameter
            overrides["fan_radius"] = diameter / 2
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError("The fan feature measurements are malformed.")
        if features.get("bladeCount") is not None:
            count = features["bladeCount"]
            if isinstance(count, bool) or not isinstance(count, (int, float)) or not math.isfinite(count) or not count == int(count) or not 4 <= count <= 16:
                raise ValueError("Direct fan blade count must be a whole number from 4 to 16.")
            accepted_counts[fan_name] = {"bladeCount": int(count)}
            overrides["fan_blade_count"] = int(count)
        if features.get("adjacentHoleCenters") is not None:
            spacing = validated(features["adjacentHoleCenters"], 50, 75,
                                "fan adjacent mounting-hole centers")
            accepted.setdefault(fan_name, {})["adjacentHoleCenters"] = spacing
            # One interval does not locate the other three holes. Keep it as
            # evidence, but do not turn it into a fitted square pattern.
        hole_keys = [f"mountHole{number}{axis}" for number in range(1, 5) for axis in "XY"]
        if any(features.get(key) is not None for key in hole_keys):
            if any(features.get(key) is None for key in hole_keys):
                raise ValueError("All four fan mounting-hole X/Y centers are needed to move the CAD pattern.")
            points = [(validated(features[f"mountHole{number}X"], -55, 55, f"fan hole {number} X"),
                       validated(features[f"mountHole{number}Y"], -55, 55, f"fan hole {number} Y"))
                      for number in range(1, 5)]
            if any(not 25 <= math.hypot(x, y) <= 55 for x, y in points):
                raise ValueError("Each fan hole center must lie 25–55 mm from the shaft center in this study.")
            if any(math.dist(points[i], points[j]) < 15 for i in range(4) for j in range(i + 1, 4)):
                raise ValueError("Fan mounting-hole centers must be at least 15 mm apart.")
            if "adjacentHoleCenters" in accepted.get(fan_name, {}):
                recorded = accepted[fan_name]["adjacentHoleCenters"]
                neighbors = [math.dist(points[i], points[(i + 1) % 4]) for i in range(4)]
                if min(abs(distance - recorded) for distance in neighbors) > 3:
                    raise ValueError("The recorded adjacent fan-hole spacing disagrees with all four neighboring X/Y center pairs by more than 3 mm. Recheck the view, numbering and measurement datums.")
            accepted.setdefault(fan_name, {})["mountHoleCenters"] = points
            overrides["fan_mount_points"] = points
        if features.get("bladeTipChord") is not None:
            chord = validated(features["bladeTipChord"], 20, 60,
                              "fan intact blade tip chord")
            accepted.setdefault(fan_name, {})["bladeTipChord"] = chord
            overrides["fan_tip_chord"] = chord
    radiator_name = "radiator_replacement_core_comparison_provisional"
    entry = direct_entry(radiator_name)
    if entry:
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError("The radiator core measurements are malformed.")
        for key, (low, high, parameter) in RADIATOR_LIMITS.items():
            if features.get(key) is None:
                continue
            measured = validated(features[key], low, high, key)
            accepted.setdefault(radiator_name, {})[key] = measured
            overrides[parameter] = measured
    fuel_pump_name = "electric_fuel_pump_photo_reference_provisional"
    entry = direct_entry(fuel_pump_name)
    if entry:
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError("The electric fuel-pump measurements are malformed.")
        for key, (low, high, parameter) in FUEL_PUMP_LIMITS.items():
            if features.get(key) is None:
                continue
            measured = validated(features[key], low, high, key)
            accepted.setdefault(fuel_pump_name, {})[key] = measured
            overrides[parameter] = measured / 2 if key == "pumpBarrelDiameter" else measured
    air_cleaner_name = "air_cleaner_housing_photo_reference_provisional"
    entry = direct_entry(air_cleaner_name)
    if entry:
        for key, (low, high, parameter) in AIR_CLEANER_LIMITS.items():
            if entry.get(key) is None:
                continue
            measured = validated(entry[key], low, high, f"air cleaner {key} outside envelope")
            accepted.setdefault(air_cleaner_name, {})[key] = measured
            overrides[parameter] = measured
    air_filter_name = "air_cleaner_filter_catalog_study_provisional"
    entry = direct_entry(air_filter_name)
    if entry:
        if entry.get("axisSchema") != "vertical-panel-v2":
            raise ValueError("The air-filter axes changed to a vertical panel. Recheck and save X thickness, Y height and Z length in the current CAD inspector before building an owner STEP.")
        for key, (low, high, parameter) in AIR_FILTER_LIMITS.items():
            if entry.get(key) is None:
                continue
            measured = validated(entry[key], low, high, f"air filter {key} outside envelope")
            accepted.setdefault(air_filter_name, {})[key] = measured
            overrides[parameter] = measured
        # The public case is only a comparison envelope. A directly measured
        # element must not be silently clipped to fit it in an owner build.
        case = (overrides.get("air_cleaner_width", 100.0),
                overrides.get("air_cleaner_height", 150.0),
                overrides.get("air_cleaner_length", 280.0))
        element = (overrides.get("air_filter_width", 30.0),
                   overrides.get("air_filter_height", 120.0),
                   overrides.get("air_filter_length", 250.0))
        margins = (16.0, 30.0, 20.0)
        if any(size > outside - margin for size, outside, margin in zip(element, case, margins)):
            raise ValueError("Measured filter exceeds the current case study clearance. Measure the fitted case as well, or check the filter axes; the builder will not shrink a direct measurement.")
    carb_cover_name = "zenith_stromberg_dashpot_cover_photo_reference_provisional"
    entry = direct_entry(carb_cover_name)
    if entry:
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError("The carburetor-cover measurements are malformed.")
        for key, (low, high, parameter) in CARB_COVER_LIMITS.items():
            if features.get(key) is None:
                continue
            measured = validated(features[key], low, high, key)
            accepted.setdefault(carb_cover_name, {})[key] = measured
            overrides[parameter] = measured
        dome = overrides.get("carb_cover_dome_diameter", 68.0)
        width = overrides.get("carb_cover_flange_width", 73.0)
        length = overrides.get("carb_cover_flange_length", 71.0)
        if width < dome + 2 or length < dome + 2:
            raise ValueError("The cover flange must exceed the dome diameter by at least 2 mm in both measured directions. Recheck the three measurement datums; if only the dome was measured, measure the flange too before rebuilding.")
    carb_cap_name = "carb_damper_cap_photo_reference_provisional"
    entry = direct_entry(carb_cap_name)
    if entry:
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError("The damper-cap measurements are malformed.")
        if features.get("capOutsideDiameter") is not None:
            measured = validated(features["capOutsideDiameter"], 25, 46,
                                  "capOutsideDiameter")
            accepted[carb_cap_name] = {"capOutsideDiameter": measured}
            overrides["carb_damper_cap_outer_diameter"] = measured
    # A measured front circular casting diameter can resize that exterior
    # section. Ear stations and the pulley/belt plane remain independent.
    # The remaining survey values cannot safely be substituted into the
    # comparison mounting layout without resolving those interfaces together.
    for name, keys in {
        "alternator_photo_reference_provisional": (
            "frontCaseOutsideDiameter", "frontCastingAxialLength",
            "shaftToPivotCenter", "frontEarThickness"),
        "alternator_rear_cover_photo_reference_provisional": (
            "rearCastingAxialLength", "rearShroudAxialLength",
            "rearEarThickness", "insideEarGap"),
        "alternator_rear_connector_housing_catalog_reference_provisional": (
            "connectorProjection",),
    }.items():
        entry = direct_entry(name)
        if not entry:
            continue
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError(f"{name} feature measurements are malformed.")
        for key in keys:
            if features.get(key) is not None:
                if name == "alternator_photo_reference_provisional" and key == "frontCaseOutsideDiameter":
                    measured = validated(features[key], 112, 140, key)
                    accepted.setdefault(name, {})[key] = measured
                    overrides["alternator_front_case_diameter"] = measured
                else:
                    recorded_only.setdefault(name, {})[key] = validated(
                        features[key], 1, 1500, f"{name} {key}")
    pivot_name = "alternator_pivot_bolt_catalog_length_provisional"
    entry = direct_entry(pivot_name)
    if entry:
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError("The pivot-bolt access observations are malformed.")
        side = features.get("boltHeadSide")
        if side is not None:
            if side not in ("front", "rear"):
                raise ValueError("The fitted pivot-bolt head side must be front or rear.")
            access_observations[pivot_name] = {"boltHeadSide": side}
        for key, low, high in (
            ("headSideStraightClearance", 0, 300),
            ("actualUnderHeadLength", 50, 180),
            ("straightRearClearance", 0, 1500),
        ):
            if features.get(key) is not None:
                recorded_only.setdefault(pivot_name, {})[key] = validated(
                    features[key], low, high, f"{pivot_name} {key}")
    pump_name = "water_pump_catalog_reference_provisional"
    entry = direct_entry(pump_name)
    if entry:
        features = entry.get("features", {})
        if not isinstance(features, dict):
            raise ValueError("The water-pump access measurements are malformed.")
        if features.get("fanToCoreFaceGap") is not None:
            measured = validated(features["fanToCoreFaceGap"], 0, 200, "fanToCoreFaceGap")
            accepted.setdefault(pump_name, {})["fanToCoreFaceGap"] = measured
            overrides["fan_to_core_face_gap"] = measured
    if not overrides:
        if recorded_only or access_observations:
            raise ValueError("Alternator measurements or access observations are recorded, but this STEP builder can apply only a direct front-case outside diameter from 112 to 140 mm. Keep the exported log; add that supported dimension or another supported direct measurement to generate an owner STEP.")
        if accepted.get(fan_name, {}).get("adjacentHoleCenters") is not None:
            raise ValueError("One adjacent fan-hole spacing is recorded, but cannot move the CAD pattern. Add all four X/Y centers or another supported direct dimension.")
        raise ValueError("No supported direct block, rocker cover, carburetor cover or cap, pulley, fan, radiator, air-cleaner, filter, fuel-pump or alternator front-case dimensions were recorded.")
    return overrides, {
        "source_file": source.name,
        "method": "measured directly on this car",
        "source_notes": notes,
        "accepted_dimensions_mm": accepted,
        "accepted_counts": accepted_counts,
        "recorded_but_not_applied_mm": recorded_only,
        "recorded_access_observations": access_observations,
        "applied_parameters_mm": {key: value for key, value in overrides.items() if key != "fan_blade_count"},
        "applied_counts": {"fan_blade_count": overrides["fan_blade_count"]} if "fan_blade_count" in overrides else {},
        "excluded": ["unmeasured block casting contours and main-bearing stations", "rocker-cover cap locking profile and fitted cap variant", "bonnet and lift clearance", "gasket geometry", "pulley groove profiles", "pulley stations and alignment", "fan fixing pattern" if "fan_mount_points" not in overrides else "fan mounting-hole bore and thread geometry", "fan blade count" if "fan_blade_count" not in overrides else "fan blade spacing", "fan blade pitch and blade-root contour", "legacy radiator-or-shroud gap", "radiator tanks, cowl and mounts", "air-cleaner filter seat, joint, clip and inlet details", "carburetor cover height, wall thickness, screw count/thread, piston clearance and fitted variant", "electric fuel-pump model, fittings and body mounting station", "alternator ear stations, case section lengths, connector shape, shaft and pulley plane remain unverified"],
        "limits": "Direct bore-center intervals move bore-related stations; direct deck and lower-casting widths resize only their cross sections and the connected provisional sump width. Direct carburetor dome and flange measurements resize only the cover, its unmeasured mating seat and two approximate screw markers; a direct damper-cap diameter scales only the cap silhouette. Supported outside envelopes, directly counted fan blades, four directly measured fan-hole X/Y centers, intact fan tip chord and the measured fan-to-core datum change their named geometry. A direct front alternator casting diameter resizes only its circular front frame, face and window pattern in a separate owner STEP; it does not locate the ears, shaft or belt. A direct blade count changes repetition only, using the provisional RKC92-style paddle for every blade; a single fan-hole interval is recorded but does not move the pattern. Main-bearing stations, full block casting, cover topology, pulley positions, fan variant, belt fit and radiator mounting remain unverified.",
    }
