"""
NX Geometry Query Hook
======================

Provides Python functions to query geometry properties from NX parts.

API Reference (verified via Siemens MCP docs):
- Part.MeasureManager() -> Returns measure manager for this part
- MeasureManager.NewMassProperties() -> Create mass properties measurement
- Part.Bodies() -> BodyCollection (solid bodies in the part)
- Body.GetPhysicalMaterial() -> Get material assigned to body

Usage:
    from optimization_engine.hooks.nx_cad import geometry_query

    # Get mass properties
    result = geometry_query.get_mass_properties("C:/path/to/part.prt")

    # Get body info
    result = geometry_query.get_bodies("C:/path/to/part.prt")

    # Get volume
    result = geometry_query.get_volume("C:/path/to/part.prt")
"""

import os
import json
import subprocess
import tempfile
from pathlib import Path
from typing import Optional, Dict, Any, List, Tuple

# Import NX path from centralized config
try:
    from config import NX_BIN_DIR
    NX_BIN_PATH = str(NX_BIN_DIR)
except ImportError:
    NX_BIN_PATH = os.environ.get(
        "NX_BIN_PATH",
        r"C:\Program Files\Siemens\DesigncenterNX2512\NXBIN"
    )

# Journal template for geometry query operations
GEOMETRY_QUERY_JOURNAL = '''
# NX Open Python Journal - Geometry Query Operations
# Auto-generated by Atomizer hooks
#
# Based on Siemens NX Open Python API:
# - MeasureManager.NewMassProperties()
# - BodyCollection
# - Body.GetPhysicalMaterial()

import NXOpen
import NXOpen.UF
import json
import sys
import os
import math

def main():
    """Execute geometry query operation based on command arguments."""
    # Get the NX session
    session = NXOpen.Session.GetSession()

    # Parse arguments: operation, part_path, output_json, [extra_args...]
    args = sys.argv[1:] if len(sys.argv) > 1 else []

    if len(args) < 3:
        raise ValueError("Usage: script.py <operation> <part_path> <output_json> [args...]")

    operation = args[0]
    part_path = args[1]
    output_json = args[2]
    extra_args = args[3:] if len(args) > 3 else []

    result = {"success": False, "error": None, "data": {}}

    try:
        # Ensure part is open
        part = ensure_part_open(session, part_path)

        if part is None:
            result["error"] = f"Failed to open part: {part_path}"
        elif operation == "mass_properties":
            result = get_mass_properties(part)
        elif operation == "bodies":
            result = get_bodies(part)
        elif operation == "volume":
            result = get_volume(part)
        elif operation == "surface_area":
            result = get_surface_area(part)
        elif operation == "material":
            result = get_material(part)
        else:
            result["error"] = f"Unknown operation: {operation}"

    except Exception as e:
        import traceback
        result["error"] = str(e)
        result["traceback"] = traceback.format_exc()

    # Write result to output JSON
    with open(output_json, 'w') as f:
        json.dump(result, f, indent=2)

    return result


def ensure_part_open(session, part_path):
    """Ensure the part is open and return it."""
    # Check if already open
    part_path_normalized = os.path.normpath(part_path).lower()

    for part in session.Parts:
        if os.path.normpath(part.FullPath).lower() == part_path_normalized:
            return part

    # Need to open it
    if not os.path.exists(part_path):
        return None

    try:
        # Set load options for the working directory
        working_dir = os.path.dirname(part_path)
        session.Parts.LoadOptions.ComponentLoadMethod = NXOpen.LoadOptions.LoadMethod.FromDirectory
        session.Parts.LoadOptions.SetSearchDirectories([working_dir], [True])

        # Use OpenActiveDisplay instead of OpenBase for better compatibility
        part, load_status = session.Parts.OpenActiveDisplay(
            part_path,
            NXOpen.DisplayPartOption.AllowAdditional
        )
        load_status.Dispose()
        return part
    except:
        return None


def get_solid_bodies(part):
    """Get all solid bodies from a part."""
    solid_bodies = []
    for body in part.Bodies:
        if body.IsSolidBody:
            solid_bodies.append(body)
    return solid_bodies


def get_mass_properties(part):
    """Get mass properties from a part.

    NX Open API: MeasureManager.NewMassProperties()

    Returns mass, volume, surface area, centroid, and inertia properties.
    """
    result = {"success": False, "error": None, "data": {}}

    try:
        # Get solid bodies
        solid_bodies = get_solid_bodies(part)

        if not solid_bodies:
            result["error"] = "No solid bodies found in part"
            return result

        # Get measure manager
        measure_manager = part.MeasureManager

        # Get units - use base units array like the working journal
        uc = part.UnitCollection
        mass_units = [
            uc.GetBase("Area"),
            uc.GetBase("Volume"),
            uc.GetBase("Mass"),
            uc.GetBase("Length")
        ]

        # Create mass properties measurement
        # Signature: NewMassProperties(mass_units, accuracy, objects)
        mass_props = measure_manager.NewMassProperties(mass_units, 0.99, solid_bodies)

        # Get properties
        mass = mass_props.Mass
        volume = mass_props.Volume
        area = mass_props.Area

        # Get centroid
        centroid = mass_props.Centroid
        centroid_x = centroid.X
        centroid_y = centroid.Y
        centroid_z = centroid.Z

        # Get principal moments of inertia (may not be available)
        ixx = 0.0
        iyy = 0.0
        izz = 0.0
        try:
            principal_moments = mass_props.PrincipalMomentsOfInertia
            ixx = principal_moments[0]
            iyy = principal_moments[1]
            izz = principal_moments[2]
        except:
            pass

        # Get material info from first body via attributes
        material_name = None
        density = None
        try:
            # Try body attributes (NX stores material as attribute)
            attrs = solid_bodies[0].GetUserAttributes()
            for attr in attrs:
                if 'material' in attr.Title.lower():
                    material_name = attr.StringValue
                    break
        except:
            pass

        result["success"] = True
        result["data"] = {
            "mass": mass,
            "mass_unit": "kg",
            "volume": volume,
            "volume_unit": "mm^3",
            "surface_area": area,
            "area_unit": "mm^2",
            "centroid": {
                "x": centroid_x,
                "y": centroid_y,
                "z": centroid_z,
                "unit": "mm"
            },
            "principal_moments": {
                "Ixx": ixx,
                "Iyy": iyy,
                "Izz": izz,
                "unit": "kg*mm^2"
            },
            "material": material_name,
            "density": density,
            "body_count": len(solid_bodies)
        }

    except Exception as e:
        import traceback
        result["error"] = str(e)
        result["traceback"] = traceback.format_exc()

    return result


def get_bodies(part):
    """Get information about all bodies in the part.

    NX Open API: Part.Bodies()
    """
    result = {"success": False, "error": None, "data": {}}

    try:
        bodies_info = []

        for body in part.Bodies:
            body_data = {
                "name": body.Name if hasattr(body, 'Name') else None,
                "is_solid": body.IsSolidBody,
                "is_sheet": body.IsSheetBody,
            }

            # Try to get material
            try:
                phys_mat = body.GetPhysicalMaterial()
                if phys_mat:
                    body_data["material"] = phys_mat.Name
            except:
                body_data["material"] = None

            bodies_info.append(body_data)

        result["success"] = True
        result["data"] = {
            "count": len(bodies_info),
            "solid_count": sum(1 for b in bodies_info if b["is_solid"]),
            "sheet_count": sum(1 for b in bodies_info if b["is_sheet"]),
            "bodies": bodies_info
        }

    except Exception as e:
        result["error"] = str(e)

    return result


def get_volume(part):
    """Get total volume of all solid bodies.

    NX Open API: MeasureManager.NewMassProperties()
    """
    result = {"success": False, "error": None, "data": {}}

    try:
        solid_bodies = get_solid_bodies(part)

        if not solid_bodies:
            result["error"] = "No solid bodies found in part"
            return result

        measure_manager = part.MeasureManager
        units = part.UnitCollection

        body_array = [NXOpen.IBody.Wrap(body) for body in solid_bodies]

        mass_props = measure_manager.NewMassProperties(
            units.FindObject("SquareMilliMeter"),
            units.FindObject("CubicMillimeter"),
            units.FindObject("Kilogram"),
            body_array
        )

        result["success"] = True
        result["data"] = {
            "volume": mass_props.Volume,
            "unit": "mm^3",
            "body_count": len(solid_bodies)
        }

    except Exception as e:
        result["error"] = str(e)

    return result


def get_surface_area(part):
    """Get total surface area of all solid bodies.

    NX Open API: MeasureManager.NewMassProperties()
    """
    result = {"success": False, "error": None, "data": {}}

    try:
        solid_bodies = get_solid_bodies(part)

        if not solid_bodies:
            result["error"] = "No solid bodies found in part"
            return result

        measure_manager = part.MeasureManager
        units = part.UnitCollection

        body_array = [NXOpen.IBody.Wrap(body) for body in solid_bodies]

        mass_props = measure_manager.NewMassProperties(
            units.FindObject("SquareMilliMeter"),
            units.FindObject("CubicMillimeter"),
            units.FindObject("Kilogram"),
            body_array
        )

        result["success"] = True
        result["data"] = {
            "surface_area": mass_props.Area,
            "unit": "mm^2",
            "body_count": len(solid_bodies)
        }

    except Exception as e:
        result["error"] = str(e)

    return result


def get_material(part):
    """Get material information from bodies in the part.

    NX Open API: Body.GetPhysicalMaterial()
    """
    result = {"success": False, "error": None, "data": {}}

    try:
        solid_bodies = get_solid_bodies(part)

        if not solid_bodies:
            result["error"] = "No solid bodies found in part"
            return result

        materials = {}

        for body in solid_bodies:
            try:
                phys_mat = body.GetPhysicalMaterial()
                if phys_mat:
                    mat_name = phys_mat.Name
                    if mat_name not in materials:
                        mat_data = {"name": mat_name}
                        # Try to get properties
                        try:
                            mat_data["density"] = phys_mat.GetRealPropertyValue("Density")
                        except:
                            pass
                        try:
                            mat_data["youngs_modulus"] = phys_mat.GetRealPropertyValue("YoungsModulus")
                        except:
                            pass
                        try:
                            mat_data["poissons_ratio"] = phys_mat.GetRealPropertyValue("PoissonsRatio")
                        except:
                            pass
                        materials[mat_name] = mat_data
            except:
                pass

        result["success"] = True
        result["data"] = {
            "material_count": len(materials),
            "materials": materials,
            "body_count": len(solid_bodies)
        }

    except Exception as e:
        result["error"] = str(e)

    return result


if __name__ == "__main__":
    main()
'''


def _get_run_journal_exe() -> str:
    """Get the path to run_journal.exe."""
    return os.path.join(NX_BIN_PATH, "run_journal.exe")


def _run_journal(journal_path: str, args: list) -> Tuple[bool, str]:
    """Run an NX journal with arguments.

    Returns:
        Tuple of (success, output_or_error)
    """
    run_journal = _get_run_journal_exe()

    if not os.path.exists(run_journal):
        return False, f"run_journal.exe not found at {run_journal}"

    cmd = [run_journal, journal_path, "-args"] + args

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=120  # 2 minute timeout
        )

        if result.returncode != 0:
            return False, f"Journal execution failed: {result.stderr}"

        return True, result.stdout

    except subprocess.TimeoutExpired:
        return False, "Journal execution timed out"
    except Exception as e:
        return False, str(e)


def _execute_geometry_operation(
    operation: str,
    part_path: str,
    extra_args: list = None
) -> Dict[str, Any]:
    """Execute a geometry query operation via NX journal.

    Args:
        operation: The operation to perform
        part_path: Path to the part file
        extra_args: Additional arguments for the operation

    Returns:
        Dict with operation result
    """
    # Create temporary journal file
    with tempfile.NamedTemporaryFile(
        mode='w',
        suffix='.py',
        delete=False
    ) as journal_file:
        journal_file.write(GEOMETRY_QUERY_JOURNAL)
        journal_path = journal_file.name

    # Create temporary output file
    output_file = tempfile.NamedTemporaryFile(
        mode='w',
        suffix='.json',
        delete=False
    ).name

    try:
        # Build arguments
        args = [operation, part_path, output_file]
        if extra_args:
            args.extend(extra_args)

        # Run the journal
        success, output = _run_journal(journal_path, args)

        if not success:
            return {"success": False, "error": output, "data": {}}

        # Read the result
        if os.path.exists(output_file):
            with open(output_file, 'r') as f:
                return json.load(f)
        else:
            return {"success": False, "error": "Output file not created", "data": {}}

    finally:
        # Cleanup temporary files
        if os.path.exists(journal_path):
            os.unlink(journal_path)
        if os.path.exists(output_file):
            os.unlink(output_file)


# =============================================================================
# Public API
# =============================================================================

def get_mass_properties(part_path: str) -> Dict[str, Any]:
    """Get mass properties from an NX part.

    Args:
        part_path: Full path to the .prt file

    Returns:
        Dict with keys:
            - success: bool
            - error: Optional error message
            - data: Dict with mass, volume, surface_area, centroid,
                    principal_moments, material, density, body_count

    Example:
        >>> result = get_mass_properties("C:/models/bracket.prt")
        >>> if result["success"]:
        ...     print(f"Mass: {result['data']['mass']} kg")
        ...     print(f"Volume: {result['data']['volume']} mm^3")
    """
    part_path = os.path.abspath(part_path)

    if not os.path.exists(part_path):
        return {
            "success": False,
            "error": f"Part file not found: {part_path}",
            "data": {}
        }

    return _execute_geometry_operation("mass_properties", part_path)


def get_bodies(part_path: str) -> Dict[str, Any]:
    """Get information about bodies in an NX part.

    Args:
        part_path: Full path to the .prt file

    Returns:
        Dict with keys:
            - success: bool
            - error: Optional error message
            - data: Dict with count, solid_count, sheet_count, bodies list

    Example:
        >>> result = get_bodies("C:/models/bracket.prt")
        >>> if result["success"]:
        ...     print(f"Solid bodies: {result['data']['solid_count']}")
    """
    part_path = os.path.abspath(part_path)

    if not os.path.exists(part_path):
        return {
            "success": False,
            "error": f"Part file not found: {part_path}",
            "data": {}
        }

    return _execute_geometry_operation("bodies", part_path)


def get_volume(part_path: str) -> Dict[str, Any]:
    """Get total volume of solid bodies in an NX part.

    Args:
        part_path: Full path to the .prt file

    Returns:
        Dict with keys:
            - success: bool
            - error: Optional error message
            - data: Dict with volume (mm^3), unit, body_count

    Example:
        >>> result = get_volume("C:/models/bracket.prt")
        >>> if result["success"]:
        ...     print(f"Volume: {result['data']['volume']} mm^3")
    """
    part_path = os.path.abspath(part_path)

    if not os.path.exists(part_path):
        return {
            "success": False,
            "error": f"Part file not found: {part_path}",
            "data": {}
        }

    return _execute_geometry_operation("volume", part_path)


def get_surface_area(part_path: str) -> Dict[str, Any]:
    """Get total surface area of solid bodies in an NX part.

    Args:
        part_path: Full path to the .prt file

    Returns:
        Dict with keys:
            - success: bool
            - error: Optional error message
            - data: Dict with surface_area (mm^2), unit, body_count

    Example:
        >>> result = get_surface_area("C:/models/bracket.prt")
        >>> if result["success"]:
        ...     print(f"Area: {result['data']['surface_area']} mm^2")
    """
    part_path = os.path.abspath(part_path)

    if not os.path.exists(part_path):
        return {
            "success": False,
            "error": f"Part file not found: {part_path}",
            "data": {}
        }

    return _execute_geometry_operation("surface_area", part_path)


def get_material(part_path: str) -> Dict[str, Any]:
    """Get material information from bodies in an NX part.

    Args:
        part_path: Full path to the .prt file

    Returns:
        Dict with keys:
            - success: bool
            - error: Optional error message
            - data: Dict with material_count, materials dict, body_count

    Example:
        >>> result = get_material("C:/models/bracket.prt")
        >>> if result["success"]:
        ...     for name, mat in result["data"]["materials"].items():
        ...         print(f"{name}: density={mat.get('density')}")
    """
    part_path = os.path.abspath(part_path)

    if not os.path.exists(part_path):
        return {
            "success": False,
            "error": f"Part file not found: {part_path}",
            "data": {}
        }

    return _execute_geometry_operation("material", part_path)
