from __future__ import annotations

import copy
import json
import math
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
ASSEMBLY_DIR = ROOT / "preview_exports" / "assembly"
BASELINE_CATALOG = ASSEMBLY_DIR / "roof_mount_platform_concept_assembly.json"
PARAMETER_FILE = ASSEMBLY_DIR / "editable_parameters.json"
SUBMITTED_FILE = ASSEMBLY_DIR / "submitted_parameters.json"
ENGINE_CATALOG = ASSEMBLY_DIR / "roof_mount_platform_engine_assembly.json"
ENGINE_REPORT = ASSEMBLY_DIR / "standalone_engine_report.json"

BASELINE = {
    "platform_initial_length": 4800.0,
    "platform_length": 4966.0,
    "platform_width_truss": 6000.0,
    "screen_width": 7000.0,
    "screen_height": 2000.0,
    "screen_extension_height": 600.0,
    "pitch_angle": 5.0,
}


def read_json(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8"))


def write_json(path: Path, value: Any) -> None:
    path.write_text(json.dumps(value, indent=2), encoding="utf-8")


def parameter_map(payload: dict[str, Any] | None) -> dict[str, Any]:
    params = read_json(PARAMETER_FILE)["parameters"]
    values = {param["name"]: param.get("value") for param in params}
    if payload:
        for param in payload.get("parameters", []):
            values[param["name"]] = param.get("value")
    return values


def number(values: dict[str, Any], name: str, fallback: float) -> float:
    try:
        return float(values.get(name, fallback))
    except (TypeError, ValueError):
        return fallback


def boolean(values: dict[str, Any], name: str, fallback: bool) -> bool:
    value = values.get(name, fallback)
    if isinstance(value, bool):
        return value
    return str(value).strip().lower() in {"1", "true", "yes", "on"}


def parse_door_data(value: Any) -> dict[str, Any]:
    text = str(value or "0").strip().upper()
    if not text or text == "0":
        return {"enabled": False, "raw": "0"}
    pieces = [piece.strip() for piece in text.split(",")]
    if len(pieces) != 2:
        return {"enabled": False, "raw": text, "error": "Expected bay,L or bay,R"}
    try:
        bay = int(pieces[0])
    except ValueError:
        return {"enabled": False, "raw": text, "error": "Bay must be a number"}
    hand = pieces[1]
    if hand not in {"L", "R"}:
        return {"enabled": False, "raw": text, "error": "Hand must be L or R"}
    return {"enabled": True, "bay": max(1, bay), "hand": hand, "raw": f"{max(1, bay)},{hand}"}


def door_layout(values: dict[str, Any], platform_length: float, platform_width: float) -> dict[str, Any]:
    sides = {
        "front": ("FRONT_DOORS_DATA", "SCREEN_FRONT", platform_length),
        "back": ("BACK_DOORS_DATA", "SCREEN_BACK", platform_length),
        "left": ("LEFT_DOORS_DATA", "SCREEN_LEFT", platform_width),
        "right": ("RIGHT_DOORS_DATA", "SCREEN_RIGHT", platform_width),
    }
    layout: dict[str, Any] = {}
    for side, (door_param, screen_param, length) in sides.items():
        bay_count = max(1, min(8, math.floor(max(1200.0, length - 216.0) / 1134.0)))
        door = parse_door_data(values.get(door_param, "0"))
        if door.get("enabled"):
            door["bay"] = min(bay_count, max(1, int(door["bay"])))
            door["raw"] = f"{door['bay']},{door['hand']}"
        screen_enabled = boolean(values, screen_param, True)
        door["visible"] = bool(door.get("enabled") and screen_enabled)
        layout[side] = {
            "parameter": door_param,
            "screenParameter": screen_param,
            "screenEnabled": screen_enabled,
            "bayCount": bay_count,
            "door": door,
        }
    return layout


DOOR_BASE_X = -120.0
DOOR_BASE_Y = 266.0
DOOR_BAY_STEP = 1134.0
DOOR_BACK_X_OFFSET = 353.3
DOOR_FRONT_Y = -120.0
DOOR_BACK_Y_OFFSET = 120.0


def visible_door_entries(doors: dict[str, Any]) -> list[tuple[str, dict[str, Any]]]:
    entries: list[tuple[str, dict[str, Any]]] = []
    for side, side_data in doors.items():
        door = side_data.get("door", {})
        if door.get("visible"):
            entries.append((side, door))
    return entries


def map_door_point(
    point: tuple[float, float, float],
    side: str,
    bay: int,
    platform_length: float,
    screen_width: float,
) -> tuple[float, float, float]:
    x, y, z = point
    rel_x = x - DOOR_BASE_X
    rel_y = y - DOOR_BASE_Y
    bay_offset = (max(1, bay) - 1) * DOOR_BAY_STEP

    if side == "front":
        return (DOOR_BASE_X + rel_x, DOOR_BASE_Y + bay_offset + rel_y, z)
    if side == "back":
        return (platform_length + DOOR_BACK_X_OFFSET - rel_x, DOOR_BASE_Y + bay_offset + rel_y, z)
    if side == "left":
        return (DOOR_BASE_Y + bay_offset + rel_y, DOOR_FRONT_Y - rel_x, z)
    if side == "right":
        return (platform_length - x, DOOR_BASE_Y + bay_offset + rel_y, z)
    return point


def map_door_matrix(
    matrix: list[float],
    side: str,
    bay: int,
    platform_length: float,
    screen_width: float,
) -> list[float]:
    origin = (matrix[3], matrix[7], matrix[11])
    columns = [
        (matrix[0], matrix[4], matrix[8]),
        (matrix[1], matrix[5], matrix[9]),
        (matrix[2], matrix[6], matrix[10]),
    ]
    mapped_origin = map_door_point(origin, side, bay, platform_length, screen_width)
    mapped_columns: list[tuple[float, float, float]] = []
    for column in columns:
        endpoint = (origin[0] + column[0], origin[1] + column[1], origin[2] + column[2])
        mapped_endpoint = map_door_point(endpoint, side, bay, platform_length, screen_width)
        mapped_columns.append(
            (
                mapped_endpoint[0] - mapped_origin[0],
                mapped_endpoint[1] - mapped_origin[1],
                mapped_endpoint[2] - mapped_origin[2],
            )
        )

    return [
        mapped_columns[0][0], mapped_columns[1][0], mapped_columns[2][0], mapped_origin[0],
        mapped_columns[0][1], mapped_columns[1][1], mapped_columns[2][1], mapped_origin[1],
        mapped_columns[0][2], mapped_columns[1][2], mapped_columns[2][2], mapped_origin[2],
        0.0, 0.0, 0.0, 1.0,
    ]


def clone_door_occurrences(
    templates: list[dict[str, Any]],
    doors: dict[str, Any],
    screen_scale: list[float],
    platform_length: float,
    screen_width: float,
) -> list[dict[str, Any]]:
    entries = visible_door_entries(doors)
    if not entries:
        entries = [("front", {"bay": 1, "hand": "L", "infillOnly": True})]

    clones: list[dict[str, Any]] = []
    for side, door in entries:
        bay = int(door.get("bay", 1))
        infill_only = bool(door.get("infillOnly"))
        for template_index, template in enumerate(templates, start=1):
            source_path = template.get("sourcePath", "")
            is_infill = "door_slat" in source_path.lower()
            if infill_only and not is_infill:
                continue

            clone = dict(template)
            hand = str(door.get("hand", "L")).upper()
            clone["id"] = f"engine_door_{side}_{bay}_{hand}_{template_index:04d}"
            clone["name"] = f"{side.upper()}_BAY_{bay}_DOOR_{hand}_{template.get('name', clone['id'])}"
            clone["doorSide"] = side
            clone["doorBay"] = bay
            clone["doorHand"] = hand
            clone["doorInfillOnly"] = infill_only
            mapped_matrix = map_door_matrix(template["matrixRowMajor"], side, bay, platform_length, screen_width)
            clone["matrixRowMajor"] = matmul4(screen_scale, mapped_matrix)
            clones.append(clone)
    return clones


def matmul4(a: list[float], b: list[float]) -> list[float]:
    out = [0.0] * 16
    for row in range(4):
        for col in range(4):
            out[row * 4 + col] = sum(a[row * 4 + k] * b[k * 4 + col] for k in range(4))
    return out


def mirror_matrix_x(matrix: list[float], mirror_width: float) -> list[float]:
    mirrored = list(matrix)
    mirrored[0] = -mirrored[0]
    mirrored[1] = -mirrored[1]
    mirrored[2] = -mirrored[2]
    mirrored[3] = mirror_width - mirrored[3]
    return mirrored


def rotation_x(degrees: float) -> list[float]:
    angle = math.radians(degrees)
    c = math.cos(angle)
    s = math.sin(angle)
    return [
        1.0, 0.0, 0.0, 0.0,
        0.0, c, -s, 0.0,
        0.0, s, c, 0.0,
        0.0, 0.0, 0.0, 1.0,
    ]


def global_scale_matrix(x_scale: float, y_scale: float, z_scale: float) -> list[float]:
    return [
        x_scale, 0.0, 0.0, 0.0,
        0.0, y_scale, 0.0, 0.0,
        0.0, 0.0, z_scale, 0.0,
        0.0, 0.0, 0.0, 1.0,
    ]


def transform_corner(m: list[float], point: tuple[float, float, float]) -> list[float]:
    x, y, z = point
    return [
        m[0] * x + m[1] * y + m[2] * z + m[3],
        m[4] * x + m[5] * y + m[6] * z + m[7],
        m[8] * x + m[9] * y + m[10] * z + m[11],
    ]


def transformed_bounds(part: dict[str, Any], occurrence: dict[str, Any]) -> tuple[list[float], list[float]]:
    bounds = part.get("bounds", {})
    bmin = bounds.get("min", [0.0, 0.0, 0.0])
    bmax = bounds.get("max", [0.0, 0.0, 0.0])
    corners = [
        (x, y, z)
        for x in (bmin[0], bmax[0])
        for y in (bmin[1], bmax[1])
        for z in (bmin[2], bmax[2])
    ]
    points = [transform_corner(occurrence["matrixRowMajor"], corner) for corner in corners]
    return (
        [min(point[i] for point in points) for i in range(3)],
        [max(point[i] for point in points) for i in range(3)],
    )


def recompute_metadata(catalog: dict[str, Any]) -> None:
    folder_counts: dict[str, int] = {}
    part_counts: dict[str, int] = {part_id: 0 for part_id in catalog["parts"]}
    mins = [math.inf, math.inf, math.inf]
    maxs = [-math.inf, -math.inf, -math.inf]

    for occurrence in catalog["instances"]:
        folder_counts[occurrence["folder"]] = folder_counts.get(occurrence["folder"], 0) + 1
        part_counts[occurrence["partId"]] = part_counts.get(occurrence["partId"], 0) + 1
        part = catalog["parts"].get(occurrence["partId"])
        if not part:
            continue
        bmin, bmax = transformed_bounds(part, occurrence)
        for axis in range(3):
            mins[axis] = min(mins[axis], bmin[axis])
            maxs[axis] = max(maxs[axis], bmax[axis])

    active_parts = {part_id: part for part_id, part in catalog["parts"].items() if part_counts.get(part_id, 0)}
    for part_id, part in active_parts.items():
        part["count"] = part_counts.get(part_id, 0)

    catalog["parts"] = active_parts
    catalog["partCount"] = len(active_parts)
    catalog["instanceCount"] = len(catalog["instances"])
    catalog["folderInstanceCounts"] = dict(sorted(folder_counts.items()))
    catalog["estimatedInstancedTriangles"] = sum(
        catalog["parts"].get(occurrence["partId"], {}).get("triangleCount", 0)
        for occurrence in catalog["instances"]
    )
    if all(math.isfinite(value) for value in mins + maxs):
        catalog["bounds"] = {
            "min": mins,
            "max": maxs,
            "size": [maxs[i] - mins[i] for i in range(3)],
        }
    else:
        catalog["bounds"] = {"min": [0, 0, 0], "max": [0, 0, 0], "size": [0, 0, 0]}


def screen_side(source_path: str) -> str:
    lowered = source_path.lower()
    if "post_back" in lowered or "back_slat" in lowered or "\\b_slat" in lowered or "/b_slat" in lowered:
        return "back"
    if "\\01 front\\" in lowered or "/01 front/" in lowered:
        return "front"
    if "\\02 back\\" in lowered or "/02 back/" in lowered:
        return "back"
    if "\\03 left\\" in lowered or "/03 left/" in lowered:
        return "left"
    if "\\04 right\\" in lowered or "/04 right/" in lowered:
        return "right"
    return "common"


def screen_side_enabled(source_path: str, values: dict[str, Any]) -> bool:
    side = screen_side(source_path)
    if side == "front":
        return boolean(values, "SCREEN_FRONT", True)
    if side == "back":
        return boolean(values, "SCREEN_BACK", True)
    if side == "left":
        return boolean(values, "SCREEN_LEFT", True)
    if side == "right":
        return boolean(values, "SCREEN_RIGHT", True)
    return any(
        boolean(values, name, fallback)
        for name, fallback in {
            "SCREEN_FRONT": True,
            "SCREEN_BACK": True,
            "SCREEN_LEFT": True,
            "SCREEN_RIGHT": True,
        }.items()
    )


def engine_catalog(payload: dict[str, Any] | None = None) -> tuple[dict[str, Any], dict[str, Any]]:
    values = parameter_map(payload)
    baseline = read_json(BASELINE_CATALOG)
    catalog = copy.deepcopy(baseline)

    platform_initial_length = number(values, "PLATFORM_INITIAL_LENGTH", BASELINE["platform_initial_length"])
    platform_length = platform_initial_length + (BASELINE["platform_length"] - BASELINE["platform_initial_length"])
    platform_width_truss = number(values, "PLATFORM_WIDTH_TRUSS_LENGHT", BASELINE["platform_width_truss"])
    screen_width = platform_width_truss + 1000.0
    screen_height = number(values, "SCREEN_HEIGHT", BASELINE["screen_height"])
    screen_extension_height = number(values, "SCREEN_EXTENSION_HEIGHT", BASELINE["screen_extension_height"])
    pitch_angle = number(values, "PITCH_ANGLE", BASELINE["pitch_angle"])
    doors = door_layout(values, platform_initial_length, platform_width_truss)

    x_scale = max(0.15, platform_length / BASELINE["platform_length"])
    y_scale = max(0.15, screen_width / BASELINE["screen_width"])
    screen_z_scale = max(0.15, (screen_height + screen_extension_height) / (BASELINE["screen_height"] + BASELINE["screen_extension_height"]))
    pitch_delta = pitch_angle - BASELINE["pitch_angle"]

    scaled: list[dict[str, Any]] = []
    left_screen_templates: list[dict[str, Any]] = []
    door_templates: list[dict[str, Any]] = []
    global_scale = global_scale_matrix(x_scale, y_scale, 1.0)
    screen_scale = global_scale_matrix(x_scale, y_scale, screen_z_scale)
    roof_pitch = rotation_x(pitch_delta)

    for occurrence in catalog["instances"]:
        source_path = occurrence.get("sourcePath", "")
        is_door_template = occurrence.get("folder") == "03 Screen" and ("\\13 DOOR\\" in source_path or "/13 DOOR/" in source_path)
        if is_door_template:
            door_templates.append(copy.deepcopy(occurrence))
            continue
        if occurrence.get("folder") == "03 Screen" and not screen_side_enabled(source_path, values):
            continue

        matrix = occurrence["matrixRowMajor"]
        if occurrence.get("folder") == "03 Screen":
            matrix = matmul4(screen_scale, matrix)
        else:
            matrix = matmul4(global_scale, matrix)

        if occurrence.get("folder") in {"04 Building", "05 Roof_Mounting"} and abs(pitch_delta) > 0.0001:
            matrix = matmul4(roof_pitch, matrix)

        new_occurrence = dict(occurrence)
        new_occurrence["matrixRowMajor"] = matrix
        scaled.append(new_occurrence)
        if occurrence.get("folder") == "03 Screen" and screen_side(source_path) == "left":
            left_screen_templates.append(new_occurrence)

    if boolean(values, "SCREEN_RIGHT", True):
        for index, occurrence in enumerate(left_screen_templates, start=1):
            mirrored = copy.deepcopy(occurrence)
            mirrored["id"] = f"engine_right_{index:04d}"
            mirrored["name"] = f"RIGHT_{occurrence.get('name', mirrored['id'])}"
            mirrored["sourcePath"] = occurrence.get("sourcePath", "").replace("\\03 LEFT\\", "\\04 RIGHT\\")
            mirrored["matrixRowMajor"] = mirror_matrix_x(mirrored["matrixRowMajor"], platform_length)
            scaled.append(mirrored)

    door_occurrences = clone_door_occurrences(door_templates, doors, screen_scale, platform_length, screen_width)
    scaled.extend(door_occurrences)

    catalog["name"] = "ROOF_MOUNT_PLATFORM_ENGINE"
    catalog["basis"] = (
        "Standalone parametric engine. No Inventor process is used; the catalog is compiled from "
        "parameter JSON, an Inventor-derived baseline occurrence graph, and local geometric rules."
    )
    catalog["engine"] = {
        "name": "StandalonePlatformEngine",
        "version": "0.1.0",
        "generatedAt": datetime.now(timezone.utc).isoformat(),
        "inventorUsed": False,
        "rulesApplied": [
            "Longitudinal scale from PLATFORM_INITIAL_LENGTH",
            "Lateral scale from PLATFORM_WIDTH_TRUSS_LENGHT",
            "Screen side suppression from SCREEN_FRONT/BACK/LEFT/RIGHT",
            "Right screen mirror generation from left-side templates when SCREEN_RIGHT is true",
            "Screen vertical scale from SCREEN_HEIGHT + SCREEN_EXTENSION_HEIGHT",
            "Roof/mounting pitch delta from PITCH_ANGLE",
            "Door bay/hand layout parsed from *_DOORS_DATA for docked screen door editing",
            "Door frame/hardware occurrences generated from the Inventor-derived door template at the selected side and bay",
            "Door slat infill is retained when no door is configured so removed doors read visually as screen",
            "Counts and world bounds recomputed from transformed part bounds",
        ],
        "limitations": [
            "This first-pass engine transforms the exported baseline meshes; it does not yet regenerate every IPT feature from sketches.",
            "Door placement uses the recovered baseline door assembly as a reusable template instead of regenerating every hinge/slat feature parametrically.",
            "Hidden internal iLogic rule bodies were not fully recoverable from the IAM streams, so formulas are reimplemented incrementally.",
        ],
    }
    catalog["parameters"] = {
        "PLATFORM_INITIAL_LENGTH": platform_initial_length,
        "PLATFORM_LENGHT": platform_length,
        "PLATFORM_WIDTH_TRUSS_LENGHT": platform_width_truss,
        "SCREEN_WIDTH_DERIVED": screen_width,
        "SCREEN_HEIGHT": screen_height,
        "SCREEN_EXTENSION_HEIGHT": screen_extension_height,
        "PITCH_ANGLE": pitch_angle,
    }
    catalog["doorLayout"] = doors
    catalog["instances"] = scaled
    recompute_metadata(catalog)

    report = {
        "ok": True,
        "generatedAt": catalog["engine"]["generatedAt"],
        "inventorUsed": False,
        "catalog": str(ENGINE_CATALOG.relative_to(ROOT)).replace("\\", "/"),
        "parameters": catalog["parameters"],
        "doorLayout": doors,
        "partCount": catalog["partCount"],
        "instanceCount": catalog["instanceCount"],
        "bounds": catalog["bounds"],
        "folderInstanceCounts": catalog["folderInstanceCounts"],
        "generatedDoorOccurrences": len(door_occurrences),
        "rulesApplied": catalog["engine"]["rulesApplied"],
        "limitations": catalog["engine"]["limitations"],
    }
    return catalog, report


def main() -> int:
    payload_path = Path(sys.argv[1]) if len(sys.argv) > 1 else SUBMITTED_FILE
    payload = read_json(payload_path) if payload_path.exists() else None
    catalog, report = engine_catalog(payload)
    write_json(ENGINE_CATALOG, catalog)
    write_json(ENGINE_REPORT, report)
    print(json.dumps(report, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
