123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
"""Export commands — STEP, STL, 3MF, F3D, FBX.

All 3D export commands require an active Fusion Design (3D PCB view or a CAD
design document). Switch to 3D PCB view with fusion_show_3d_board before
exporting from an electronics project.
"""

import adsk.core
import adsk.fusion
import os
import time
from pathlib import Path


# Shared hint constants — reused across every export handler.
_HINT_NO_OUTPUT_PATH = (
    "Provide an outputPath arg like {\"outputPath\": \"C:\\\\path\\\\out.step\"}. "
    "The path must include the file extension matching the export format."
)
_HINT_FORMAT_UNAVAILABLE = (
    "This format isn't available in this Fusion build. "
    "Use fusion_export_step or fusion_export_stl as a fallback."
)
_HINT_EXPORT_FAILED = (
    "If this persists, call fusion_screenshot_fusion to check for a blocking dialog, "
    "then fusion_send_key with 'escape' or 'enter' to dismiss it. "
    "Wrong-workspace errors: call fusion_show_3d_board first."
)
_HINT_FILE_LOCKED = (
    "Close the file in any viewer (Blender, Windows Explorer preview, etc.), "
    "or call fusion_close_document first if it's open in Fusion, then retry."
)


def _get_active_design(app: adsk.core.Application):
    """Get the active Fusion design, or None."""
    product = app.activeProduct
    if product and product.productType == "DesignProductType":
        return adsk.fusion.Design.cast(product)
    return None


def _no_design_error():
    """Standard error when no active design is available."""
    return {
        "success": False,
        "error": "No active design open in Fusion 360.",
        "_hint": "If you're in PCB Editor (2D board), call fusion_show_3d_board to switch to 3D view. Otherwise call fusion_open_cloud_file to open a Design document, then retry.",
        "data": {
            "hint": "3D exports require a Design product. If you're in PCB Editor (2D board), "
                    "switch to 3D view first with fusion_show_3d_board.",
            "recoverySteps": [
                "Run fusion_show_3d_board to switch to 3D PCB view",
                "Or open a .f3d design document",
                "Then retry the export command",
            ],
        },
    }


def _export_success(fmt: str, output_path: str, extra_msg: str = "") -> dict:
    """Standard success response for 3D exports."""
    size = os.path.getsize(output_path) if os.path.exists(output_path) else 0
    return {
        "success": True,
        "output": f"Exported {fmt} to {output_path}",
        "message": f"{fmt} file exported ({size // 1024} KB). "
                   f"{extra_msg}"
                   "Use pull_file to transfer to Docker.",
        "data": {
            "outputPath": output_path,
            "format": fmt.lower(),
            "fileSizeKB": size // 1024,
            "nextSteps": [
                "Use pull_file to transfer the file back to Docker",
                "Export other formats: fusion_export_step, fusion_export_stl, fusion_export_3mf, fusion_export_f3d, fusion_export_fbx",
            ],
        },
    }


def handle_export_step(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as a STEP file (.step).

    Industry-standard CAD interchange format. Best for mechanical CAD
    interoperability (SolidWorks, CATIA, Creo, etc.).

    Args:
        outputPath: Full path for the output .step file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    export_mgr = design.exportManager
    options = export_mgr.createSTEPExportOptions(output_path)
    result = export_mgr.execute(options)

    if result:
        return _export_success("STEP", output_path,
                               "Industry-standard CAD interchange — opens in any CAD tool. ")
    return {"success": False, "error": "STEP export failed", "_hint": _HINT_EXPORT_FAILED}


def handle_export_stl(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as an STL file (.stl).

    Mesh-based format for 3D printing and visualization.

    Args:
        outputPath: Full path for the output .stl file (required).
        refinement: "low", "medium", or "high" (default: "medium").
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    root = design.rootComponent
    export_mgr = design.exportManager
    options = export_mgr.createSTLExportOptions(root, output_path)

    refinement = args.get("refinement", "medium").lower()
    refinement_map = {
        "low": adsk.fusion.MeshRefinementSettings.MeshRefinementLow,
        "medium": adsk.fusion.MeshRefinementSettings.MeshRefinementMedium,
        "high": adsk.fusion.MeshRefinementSettings.MeshRefinementHigh,
    }
    options.meshRefinement = refinement_map.get(
        refinement, adsk.fusion.MeshRefinementSettings.MeshRefinementMedium
    )

    result = export_mgr.execute(options)

    if result:
        return _export_success("STL", output_path,
                               f"Mesh refinement: {refinement}. Good for 3D printing. ")
    return {"success": False, "error": "STL export failed", "_hint": _HINT_EXPORT_FAILED}


def handle_export_3mf(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as a 3MF file (.3mf).

    Modern 3D printing format — supports color, materials, and multi-body.

    Args:
        outputPath: Full path for the output .3mf file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    root = design.rootComponent
    export_mgr = design.exportManager
    options = export_mgr.createC3MFExportOptions(root, output_path)
    result = export_mgr.execute(options)

    if result:
        return _export_success("3MF", output_path,
                               "Modern 3D printing format with color/material support. ")
    return {"success": False, "error": "3MF export failed", "_hint": _HINT_EXPORT_FAILED}


def handle_export_f3d(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as a Fusion 360 archive (.f3d).

    Native Fusion format — preserves all parametric features, sketches,
    timeline history, and component references. Best for archiving or
    sharing with other Fusion 360 users.

    Args:
        outputPath: Full path for the output .f3d file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    export_mgr = design.exportManager
    options = export_mgr.createFusionArchiveExportOptions(output_path)
    result = export_mgr.execute(options)

    if result:
        return _export_success("F3D", output_path,
                               "Fusion 360 native archive — preserves all features and history. ")
    return {"success": False, "error": "F3D export failed", "_hint": _HINT_EXPORT_FAILED}


def handle_export_usdz(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as a USDZ file (.usdz).

    Universal Scene Description (USD) — Pixar's scene-graph format adopted
    by Apple, NVIDIA, and Autodesk. Preserves:
    - Full component hierarchy (board, copper layers, soldermask, packages)
    - PBR materials and colors
    - Named nodes for toggling parts on/off in viewers

    Best format for digital twins, AR (Apple Quick Look), and GLB conversion
    via Blender. The component hierarchy maps directly to toggleable nodes
    in GLB viewers.

    Pipeline: Fusion → USDZ → Blender → GLB (or view USDZ directly on iOS)

    Args:
        outputPath: Full path for the output .usdz file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    # Ensure .usdz extension
    if not output_path.lower().endswith(('.usdz', '.usd', '.usda', '.usdc')):
        output_path = str(Path(output_path).with_suffix('.usdz'))

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    # Pre-delete to avoid overwrite dialogs
    if os.path.exists(output_path):
        try:
            os.remove(output_path)
        except PermissionError:
            return {
                "success": False,
                "error": f"Cannot overwrite {output_path} — file is locked.",
                "_hint": _HINT_FILE_LOCKED,
                "data": {
                    "hint": "Close the file in any viewer and retry.",
                    "outputPath": output_path,
                },
            }

    export_mgr = design.exportManager
    try:
        options = export_mgr.createUSDExportOptions(output_path)
    except AttributeError:
        return {
            "success": False,
            "error": "USDZ export not available in this Fusion 360 version.",
            "_hint": _HINT_FORMAT_UNAVAILABLE,
            "data": {
                "hint": "createUSDExportOptions requires a recent Fusion 360 build. "
                        "Export as STEP instead and convert via Blender.",
                "recoverySteps": [
                    "Export STEP: fusion_export_step (then convert in Blender)",
                    "Export OBJ: fusion_export_fbx (falls back to OBJ)",
                ],
            },
        }

    result = export_mgr.execute(options)

    if result and os.path.exists(output_path):
        size = os.path.getsize(output_path)

        # Get component hierarchy for the response
        components = []
        try:
            root = design.rootComponent
            for i in range(root.occurrences.count):
                occ = root.occurrences.item(i)
                components.append(occ.name)
        except Exception:
            pass

        return {
            "success": True,
            "output": f"Exported USDZ to {output_path}",
            "message": f"USDZ exported ({size // 1024} KB) with full component hierarchy. "
                       f"Contains {len(components)} top-level components that map to "
                       f"toggleable nodes in GLB viewers. "
                       f"Use pull_file to transfer to Docker.",
            "data": {
                "outputPath": output_path,
                "format": "usdz",
                "fileSizeKB": size // 1024,
                "components": components,
                "hint": "USDZ preserves PBR materials and scene hierarchy. "
                        "Each component (Board, copper layers, soldermask, Packages) "
                        "becomes a separate toggleable node in the GLB.",
                "nextSteps": [
                    "Use pull_file to transfer the USDZ to Docker",
                    "Convert to GLB: blender --background --python-expr "
                    "\"import bpy; bpy.ops.wm.read_factory_settings(use_empty=True); "
                    "bpy.ops.wm.usd_import(filepath='input.usdz'); "
                    "bpy.ops.export_scene.gltf(filepath='output.glb')\"",
                    "Or view directly on iOS/macOS — USDZ is Apple's native AR format",
                    "Or import into Unity/Unreal Engine for interactive 3D",
                ],
            },
        }

    return {"success": False, "error": "USDZ export failed", "_hint": _HINT_EXPORT_FAILED}


def handle_export_fbx(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as an FBX file (.fbx).

    Autodesk FBX format — widely used for 3D interchange with Blender,
    Unity, Unreal Engine, and other DCC tools.

    NOTE: FBX export is not available in current Fusion 360 builds via the API.
    This command will fail clearly — use fusion_export_usdz or fusion_export_step
    instead. No silent fallbacks.

    Args:
        outputPath: Full path for the output .fbx file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    # Pre-delete to avoid overwrite dialogs
    if os.path.exists(output_path):
        try:
            os.remove(output_path)
        except PermissionError:
            return {
                "success": False,
                "error": f"Cannot overwrite {output_path} — file is locked.",
                "_hint": _HINT_FILE_LOCKED,
                "data": {
                    "hint": "The file may be open in another application. Close it and retry.",
                    "outputPath": output_path,
                },
            }

    # Try FBX via exportManager API
    export_mgr = design.exportManager
    try:
        options = export_mgr.createFBXExportOptions(output_path)
        result = export_mgr.execute(options)
        if result and os.path.exists(output_path):
            return _export_success("FBX", output_path,
                                   "Convert to GLB via Blender: "
                                   "blender --background --python-expr "
                                   "\"import bpy; bpy.ops.import_scene.fbx(filepath='<fbx>'); "
                                   "bpy.ops.export_scene.gltf(filepath='<glb>')\" ")
    except (AttributeError, Exception):
        pass  # createFBXExportOptions doesn't exist in this Fusion build

    # FBX not available — fail clearly, no fallbacks
    return {
        "success": False,
        "error": "FBX export not available in this Fusion 360 build. "
                 "Use fusion_export_usdz or fusion_export_step instead.",
        "_hint": _HINT_FORMAT_UNAVAILABLE,
        "data": {
            "hint": "FBX is not supported via the Fusion API in current builds. "
                    "Use fusion_export_usdz (best for Blender/GLB pipeline, preserves "
                    "PBR materials and scene hierarchy) or fusion_export_step "
                    "(highest geometric fidelity).",
            "alternatives": ["fusion_export_usdz", "fusion_export_step", "fusion_export_stl"],
        },
    }


def handle_export_dxf(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as a DXF file (.dxf).

    AutoCAD DXF (Drawing Exchange Format) — universal 2D/3D CAD interchange.
    Widely supported by all CAD tools, laser cutters, CNC routers, and
    PCB fabrication houses.

    Args:
        outputPath: Full path for the output .dxf file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    if os.path.exists(output_path):
        try:
            os.remove(output_path)
        except PermissionError:
            return {
                "success": False,
                "error": f"Cannot overwrite {output_path} — file is locked.",
                "_hint": _HINT_FILE_LOCKED,
                "data": {"outputPath": output_path},
            }

    export_mgr = design.exportManager
    try:
        options = export_mgr.createDXFExportOptions(output_path)
        result = export_mgr.execute(options)
        if result and os.path.exists(output_path):
            return _export_success("DXF", output_path,
                                   "Universal CAD interchange — opens in AutoCAD, LibreCAD, etc. ")
    except (AttributeError, Exception):
        pass

    return {
        "success": False,
        "error": "DXF export failed or not available in this Fusion 360 build.",
        "_hint": _HINT_FORMAT_UNAVAILABLE,
        "data": {
            "hint": "Try fusion_export_step for CAD interchange instead.",
            "alternatives": ["fusion_export_step", "fusion_export_dwg"],
        },
    }


def handle_export_dwg(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as a DWG file (.dwg).

    AutoCAD native format — the industry standard for 2D/3D technical
    drawings. Best for sharing with AutoCAD, BricsCAD, DraftSight users.

    Args:
        outputPath: Full path for the output .dwg file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    if os.path.exists(output_path):
        try:
            os.remove(output_path)
        except PermissionError:
            return {
                "success": False,
                "error": f"Cannot overwrite {output_path} — file is locked.",
                "_hint": _HINT_FILE_LOCKED,
                "data": {"outputPath": output_path},
            }

    export_mgr = design.exportManager
    try:
        options = export_mgr.createDWGExportOptions(output_path)
        result = export_mgr.execute(options)
        if result and os.path.exists(output_path):
            return _export_success("DWG", output_path,
                                   "AutoCAD native format — opens in AutoCAD, BricsCAD, etc. ")
    except (AttributeError, Exception):
        pass

    return {
        "success": False,
        "error": "DWG export failed or not available in this Fusion 360 build.",
        "_hint": _HINT_FORMAT_UNAVAILABLE,
        "data": {
            "hint": "Try fusion_export_dxf or fusion_export_step instead.",
            "alternatives": ["fusion_export_dxf", "fusion_export_step"],
        },
    }


def handle_export_iges(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as an IGES file (.igs/.iges).

    Legacy CAD interchange format. Still used by some older CAD systems
    and manufacturing tools. STEP is preferred for modern workflows.

    Args:
        outputPath: Full path for the output .igs file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    if os.path.exists(output_path):
        try:
            os.remove(output_path)
        except PermissionError:
            return {
                "success": False,
                "error": f"Cannot overwrite {output_path} — file is locked.",
                "_hint": _HINT_FILE_LOCKED,
                "data": {"outputPath": output_path},
            }

    export_mgr = design.exportManager
    try:
        options = export_mgr.createIGESExportOptions(output_path)
        result = export_mgr.execute(options)
        if result and os.path.exists(output_path):
            return _export_success("IGES", output_path,
                                   "Legacy CAD interchange — STEP is preferred for modern use. ")
    except (AttributeError, Exception):
        pass

    return {
        "success": False,
        "error": "IGES export failed or not available in this Fusion 360 build.",
        "_hint": _HINT_FORMAT_UNAVAILABLE,
        "data": {
            "hint": "Try fusion_export_step instead (modern replacement for IGES).",
            "alternatives": ["fusion_export_step"],
        },
    }


def handle_export_obj(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as an OBJ file (.obj).

    Wavefront OBJ — simple mesh format widely supported by 3D viewers,
    game engines, and DCC tools. No materials or hierarchy.

    Args:
        outputPath: Full path for the output .obj file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    if os.path.exists(output_path):
        try:
            os.remove(output_path)
        except PermissionError:
            return {
                "success": False,
                "error": f"Cannot overwrite {output_path} — file is locked.",
                "_hint": _HINT_FILE_LOCKED,
                "data": {"outputPath": output_path},
            }

    export_mgr = design.exportManager
    try:
        options = export_mgr.createOBJExportOptions(output_path)
        result = export_mgr.execute(options)
        if result and os.path.exists(output_path):
            return _export_success("OBJ", output_path,
                                   "Simple mesh format — opens in Blender, Unity, etc. ")
    except (AttributeError, Exception):
        pass

    return {
        "success": False,
        "error": "OBJ export failed or not available in this Fusion 360 build.",
        "_hint": _HINT_FORMAT_UNAVAILABLE,
        "data": {
            "hint": "Try fusion_export_stl or fusion_export_usdz instead.",
            "alternatives": ["fusion_export_stl", "fusion_export_usdz"],
        },
    }


def handle_export_sat(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as a SAT file (.sat).

    ACIS SAT — solid modeling kernel format. Used by SolidWorks,
    SpaceClaim, and other ACIS-based CAD tools.

    Args:
        outputPath: Full path for the output .sat file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    if os.path.exists(output_path):
        try:
            os.remove(output_path)
        except PermissionError:
            return {
                "success": False,
                "error": f"Cannot overwrite {output_path} — file is locked.",
                "_hint": _HINT_FILE_LOCKED,
                "data": {"outputPath": output_path},
            }

    export_mgr = design.exportManager
    try:
        options = export_mgr.createSATExportOptions(output_path)
        result = export_mgr.execute(options)
        if result and os.path.exists(output_path):
            return _export_success("SAT", output_path,
                                   "ACIS solid modeling format — opens in SolidWorks, SpaceClaim. ")
    except (AttributeError, Exception):
        pass

    return {
        "success": False,
        "error": "SAT export failed or not available in this Fusion 360 build.",
        "_hint": _HINT_FORMAT_UNAVAILABLE,
        "data": {
            "hint": "Try fusion_export_step instead.",
            "alternatives": ["fusion_export_step"],
        },
    }


def handle_export_skp(app: adsk.core.Application, args: dict) -> dict:
    """Export the active design as a SketchUp file (.skp).

    SketchUp format — popular for architectural visualization and
    simple 3D modeling.

    Args:
        outputPath: Full path for the output .skp file (required).
    """
    output_path = args.get("outputPath", "")
    if not output_path:
        return {"success": False, "error": "No outputPath specified", "_hint": _HINT_NO_OUTPUT_PATH}

    design = _get_active_design(app)
    if not design:
        return _no_design_error()

    Path(output_path).parent.mkdir(parents=True, exist_ok=True)

    if os.path.exists(output_path):
        try:
            os.remove(output_path)
        except PermissionError:
            return {
                "success": False,
                "error": f"Cannot overwrite {output_path} — file is locked.",
                "_hint": _HINT_FILE_LOCKED,
                "data": {"outputPath": output_path},
            }

    export_mgr = design.exportManager
    try:
        options = export_mgr.createSKPExportOptions(output_path)
        result = export_mgr.execute(options)
        if result and os.path.exists(output_path):
            return _export_success("SKP", output_path,
                                   "SketchUp format — opens in SketchUp, Trimble viewers. ")
    except (AttributeError, Exception):
        pass

    return {
        "success": False,
        "error": "SketchUp export failed or not available in this Fusion 360 build.",
        "_hint": _HINT_FORMAT_UNAVAILABLE,
        "data": {
            "hint": "Try fusion_export_step or fusion_export_stl instead.",
            "alternatives": ["fusion_export_step", "fusion_export_stl"],
        },
    }