"""Structured, kit-aware mechanical assembly BOM (mirrors Fusion's Manage -> BOM).

The electronics `export_bom` only works on an open PCB board. This is its mechanical
counterpart: it walks the active Design assembly and returns a purchasing BOM that
respects the component hierarchy. It recurses through organizational subassemblies but
counts a physical part or a purchased kit/unit ONCE, so hardware modeled INSIDE a kit
(for example a "... with fasteners" bracket, or a bearing/pulley unit) is not
double-counted the way a flat allOccurrences walk would count it.
"""

import csv

import adsk.core
import adsk.fusion


def _material_name(comp):
    try:
        if comp.material:
            return comp.material.name
    except Exception:
        pass
    try:
        if comp.bRepBodies.count > 0 and comp.bRepBodies.item(0).material:
            return comp.bRepBodies.item(0).material.name
    except Exception:
        pass
    return ""


def _attr(comp, name):
    try:
        v = getattr(comp, name, "") or ""
        return v.strip()
    except Exception:
        return ""


def handle_assembly_bom(app: adsk.core.Application, args: dict) -> dict:
    """Return a structured, kit-aware parts list for the active mechanical design.

    args:
      treatAsUnit  optional list of name substrings whose subassemblies are counted once
                   and NOT exploded (default: ["with fasteners"]).
      exclude      optional list of top-level component-name prefixes to skip entirely.
      includePhysicalProperties  optional bool -- add volume_cm3 / mass_kg per line.
      outputPath   optional str -- also write a CSV to this local path.
    """
    design = adsk.fusion.Design.cast(app.activeProduct)
    if not design:
        return {"success": False, "error": "No active Fusion Design.",
                "_hint": "Open a mechanical design in the Design workspace, then retry."}

    args = args or {}
    kit = [k.lower() for k in (args.get("treatAsUnit") or ["with fasteners", "with fastener"])]
    exclude = tuple(args.get("exclude") or [])
    include_pp = bool(args.get("includePhysicalProperties", False))

    def is_leaf(comp):
        n = comp.name.lower()
        if any(k in n for k in kit):
            return True
        return comp.bRepBodies.count > 0  # a physical part is a leaf

    parts = {}

    def walk(occs):
        for i in range(occs.count):
            occ = occs.item(i)
            comp = occ.component
            name = comp.name
            if any(name.startswith(e) for e in exclude):
                continue
            children = occ.childOccurrences
            has_children = children is not None and children.count > 0
            if is_leaf(comp):
                row = parts.get(name)
                if row is None:
                    row = {
                        "componentName": name,
                        "partNumber": _attr(comp, "partNumber"),
                        "description": _attr(comp, "description"),
                        "material": _material_name(comp),
                        "quantity": 1,
                        "bodies": comp.bRepBodies.count,
                    }
                    if include_pp:
                        try:
                            pp = comp.getPhysicalProperties(
                                adsk.fusion.CalculationAccuracy.LowCalculationAccuracy)
                            row["volume_cm3"] = round(pp.volume, 4)
                            row["mass_kg"] = round(pp.mass, 6)
                        except Exception:
                            row["volume_cm3"] = None
                            row["mass_kg"] = None
                    parts[name] = row
                else:
                    row["quantity"] += 1
            elif has_children:
                walk(children)

    walk(design.rootComponent.occurrences)
    rows = list(parts.values())

    doc_name = design.rootComponent.name
    try:
        if design.parentDocument:
            doc_name = design.parentDocument.name
    except Exception:
        pass

    out = {
        "success": True,
        "design": doc_name,
        "partCount": len(rows),
        "totalInstances": sum(r["quantity"] for r in rows),
        "treatAsUnit": kit,
        "parts": rows,
    }

    outpath = args.get("outputPath")
    if outpath:
        try:
            cols = ["Part Number", "Part Name", "Description", "Material", "Quantity"]
            if include_pp:
                cols += ["Volume cm3", "Mass kg"]
            with open(outpath, "w", newline="", encoding="utf-8") as f:
                w = csv.writer(f)
                w.writerow(cols)
                for r in rows:
                    line = [r["partNumber"], r["componentName"], r["description"],
                            r["material"], r["quantity"]]
                    if include_pp:
                        line += [r.get("volume_cm3"), r.get("mass_kg")]
                    w.writerow(line)
            out["outputPath"] = outpath
        except Exception as e:
            out["csvError"] = str(e)

    return out