"""
MCP Tool: FEA Model Discovery

Parses Siemens NX .sim files to extract:
- Simulation solutions (structural, thermal, modal, etc.)
- Parametric expressions (design variables)
- FEM information (mesh, elements, materials)
- Linked part files

This tool enables LLM-driven optimization configuration by providing
structured information about what can be optimized in a given FEA model.
"""

import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Dict, Any, List, Optional
import json
import re


class SimFileParser:
    """
    Parser for Siemens NX .sim (simulation) files.

    IMPORTANT: Real NX .sim files are BINARY (not XML) in NX 12+.
    The parser uses two approaches:
    1. XML parsing for test/legacy files
    2. Binary string extraction for real NX files

    .sim files contain references to:
    - Parent .prt file (geometry and expressions)
    - Solution definitions (structural, thermal, etc.)
    - FEM (mesh, materials, loads, constraints)
    - Solver settings
    """

    def __init__(self, sim_path: Path):
        """
        Initialize parser with path to .sim file.

        Args:
            sim_path: Absolute path to .sim file

        Raises:
            FileNotFoundError: If sim file doesn't exist
            ValueError: If file is not a valid .sim file
        """
        self.sim_path = Path(sim_path)

        if not self.sim_path.exists():
            raise FileNotFoundError(f"Sim file not found: {sim_path}")

        if self.sim_path.suffix.lower() != '.sim':
            raise ValueError(f"Not a .sim file: {sim_path}")

        self.tree = None
        self.root = None
        self.is_binary = False
        self.sim_strings = []  # Extracted strings from binary file
        self._parse_file()

    def _parse_file(self):
        """
        Parse the .sim file - handles both XML (test files) and binary (real NX files).
        """
        # First, try XML parsing
        try:
            self.tree = ET.parse(self.sim_path)
            self.root = self.tree.getroot()
            self.is_binary = False
            return
        except ET.ParseError:
            # Not XML, must be binary - this is normal for real NX files
            pass

        # Binary file - extract readable strings
        try:
            with open(self.sim_path, 'rb') as f:
                content = f.read()

            # Extract strings (sequences of printable ASCII characters)
            # Minimum length of 4 to avoid noise
            text_content = content.decode('latin-1', errors='ignore')
            self.sim_strings = re.findall(r'[\x20-\x7E]{4,}', text_content)
            self.is_binary = True

        except Exception as e:
            raise ValueError(f"Failed to parse .sim file (tried both XML and binary): {e}")

    def extract_solutions(self) -> List[Dict[str, Any]]:
        """
        Extract solution definitions from .sim file.

        Returns:
            List of solution dictionaries with type, name, solver info
        """
        solutions = []

        if not self.is_binary and self.root is not None:
            # XML parsing
            for solution_tag in ['Solution', 'AnalysisSolution', 'SimSolution']:
                for elem in self.root.iter(solution_tag):
                    solution_info = {
                        'name': elem.get('name', 'Unknown'),
                        'type': elem.get('type', 'Unknown'),
                        'solver': elem.get('solver', 'NX Nastran'),
                        'description': elem.get('description', ''),
                    }
                    solutions.append(solution_info)
        else:
            # Binary parsing - look for solution type indicators
            solution_types = {
                'SOL 101': 'Linear Statics',
                'SOL 103': 'Normal Modes',
                'SOL 106': 'Nonlinear Statics',
                'SOL 108': 'Direct Frequency Response',
                'SOL 109': 'Direct Transient Response',
                'SOL 111': 'Modal Frequency Response',
                'SOL 112': 'Modal Transient Response',
                'SOL 200': 'Design Optimization',
            }

            found_solutions = set()
            for s in self.sim_strings:
                for sol_id, sol_type in solution_types.items():
                    if sol_id in s:
                        found_solutions.add(sol_type)

            # Also check for solution names in strings
            for s in self.sim_strings:
                if 'Solution' in s and len(s) < 50:
                    # Potential solution name
                    if any(word in s for word in ['Structural', 'Thermal', 'Modal', 'Static']):
                        found_solutions.add(s.strip())

            for sol_name in found_solutions:
                solutions.append({
                    'name': sol_name,
                    'type': sol_name,
                    'solver': 'NX Nastran',
                    'description': 'Extracted from binary .sim file'
                })

        # Default if nothing found
        if not solutions:
            solutions.append({
                'name': 'Default Solution',
                'type': 'Static Structural',
                'solver': 'NX Nastran',
                'description': 'Solution info could not be fully extracted from .sim file'
            })

        return solutions

    def extract_expressions(self) -> List[Dict[str, Any]]:
        """
        Extract expression references from .sim file.

        Note: Actual expression values are stored in the .prt file.
        This method extracts references and attempts to read from .prt if available.

        Returns:
            List of expression dictionaries with name, value, units
        """
        expressions = []

        # XML parsing - look for expression elements
        if not self.is_binary and self.root is not None:
            for expr_elem in self.root.iter('Expression'):
                expr_info = {
                    'name': expr_elem.get('name', ''),
                    'value': expr_elem.get('value', None),
                    'units': expr_elem.get('units', ''),
                    'formula': expr_elem.text if expr_elem.text else None
                }
                if expr_info['name']:
                    expressions.append(expr_info)

        # Try to read from associated .prt file (works for both XML and binary .sim)
        # Try multiple naming patterns:
        # 1. Same name as .sim: Bracket_sim1.prt
        # 2. Base name: Bracket.prt
        # 3. With _i suffix: Bracket_fem1_i.prt
        prt_paths = [
            self.sim_path.with_suffix('.prt'),  # Bracket_sim1.prt
            self.sim_path.parent / f"{self.sim_path.stem.split('_')[0]}.prt",  # Bracket.prt
            self.sim_path.parent / f"{self.sim_path.stem}_i.prt",  # Bracket_sim1_i.prt
        ]

        for prt_path in prt_paths:
            if prt_path.exists():
                prt_expressions = self._extract_prt_expressions(prt_path)
                # Merge with existing, prioritizing .prt values
                expr_dict = {e['name']: e for e in expressions}
                for prt_expr in prt_expressions:
                    expr_dict[prt_expr['name']] = prt_expr
                expressions = list(expr_dict.values())
                break  # Use first .prt file found

        return expressions

    def _extract_prt_expressions(self, prt_path: Path) -> List[Dict[str, Any]]:
        """
        Extract expressions from associated .prt file.

        .prt files are binary, but expression data is stored in readable sections.
        NX expression format: #(Type [units]) name: value;

        Args:
            prt_path: Path to .prt file

        Returns:
            List of expression dictionaries
        """
        expressions = []

        try:
            # Read as binary and search for text patterns
            with open(prt_path, 'rb') as f:
                content = f.read()

            # Try to decode as latin-1 (preserves all byte values)
            text_content = content.decode('latin-1', errors='ignore')

            # Pattern 1: NX native format with variations:
            # #(Number [mm]) tip_thickness: 20;
            # (Number [mm]) p3: 10;
            # *(Number [mm]) support_blend_radius: 10;
            # ((Number [degrees]) support_angle: 30;
            # Prefix can be: #(, *(, (, ((
            nx_pattern = r'[#*\(]*\((\w+)\s*\[([^\]]*)\]\)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)'

            # Use set to avoid duplicates
            expr_names_seen = set()

            for match in re.finditer(nx_pattern, text_content):
                expr_type, units, name, value = match.groups()
                if name not in expr_names_seen:
                    expr_names_seen.add(name)
                    expressions.append({
                        'name': name,
                        'value': float(value),
                        'units': units,
                        'type': expr_type,
                        'source': 'prt_file_nx_format'
                    })

            # Pattern 2: Find expression names from Root: references
            # Format: Root:expression_name:
            root_pattern = r'Root:([a-zA-Z_][a-zA-Z0-9_]{2,}):'
            potential_expr_names = set()

            for match in re.finditer(root_pattern, text_content):
                name = match.group(1)
                # Filter out common NX internal names
                if name not in ['index', '%%Name', '%%ug_objects_for_', 'WorldModifier']:
                    if not name.startswith('%%'):
                        potential_expr_names.add(name)

            # For names found in Root: but not in value patterns,
            # mark as "found but value unknown"
            for name in potential_expr_names:
                if name not in expr_names_seen:
                    expressions.append({
                        'name': name,
                        'value': None,
                        'units': '',
                        'type': 'Unknown',
                        'source': 'prt_file_reference_only'
                    })

            # Pattern 3: Fallback - simple name=value pattern
            # Only use if no NX-format expressions found
            if not expressions:
                simple_pattern = r'([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)'

                for match in re.finditer(simple_pattern, text_content):
                    name, value = match.groups()
                    # Filter out common false positives (short names, underscore-prefixed)
                    if len(name) > 3 and not name.startswith('_'):
                        # Additional filter: avoid Nastran keywords
                        if name.upper() not in ['PRINT', 'PUNCH', 'PLOT', 'BOTH', 'GRID', 'GAUSS']:
                            expressions.append({
                                'name': name,
                                'value': float(value),
                                'units': '',
                                'source': 'prt_file_simple_pattern'
                            })

        except Exception as e:
            # .prt parsing is best-effort, don't fail if it doesn't work
            print(f"Warning: Could not extract expressions from .prt file: {e}")

        return expressions

    def extract_fem_info(self) -> Dict[str, Any]:
        """
        Extract FEM (finite element model) information.

        Returns:
            Dictionary with mesh, material, and element info
        """
        fem_info = {
            'mesh': {},
            'materials': [],
            'element_types': [],
            'loads': [],
            'constraints': []
        }

        if not self.is_binary and self.root is not None:
            # XML parsing
            for mesh_elem in self.root.iter('Mesh'):
                fem_info['mesh'] = {
                    'name': mesh_elem.get('name', 'Default Mesh'),
                    'element_size': mesh_elem.get('element_size', 'Unknown'),
                    'node_count': mesh_elem.get('node_count', 'Unknown'),
                    'element_count': mesh_elem.get('element_count', 'Unknown')
                }

            for mat_elem in self.root.iter('Material'):
                material = {
                    'name': mat_elem.get('name', 'Unknown'),
                    'type': mat_elem.get('type', 'Isotropic'),
                    'properties': {}
                }
                for prop in ['youngs_modulus', 'poissons_ratio', 'density', 'yield_strength']:
                    if mat_elem.get(prop):
                        material['properties'][prop] = mat_elem.get(prop)
                fem_info['materials'].append(material)

            for elem_type in self.root.iter('ElementType'):
                fem_info['element_types'].append(elem_type.get('type', 'Unknown'))

            for load_elem in self.root.iter('Load'):
                load = {
                    'name': load_elem.get('name', 'Unknown'),
                    'type': load_elem.get('type', 'Force'),
                    'magnitude': load_elem.get('magnitude', 'Unknown')
                }
                fem_info['loads'].append(load)

            for constraint_elem in self.root.iter('Constraint'):
                constraint = {
                    'name': constraint_elem.get('name', 'Unknown'),
                    'type': constraint_elem.get('type', 'Fixed'),
                }
                fem_info['constraints'].append(constraint)

        else:
            # Binary parsing - extract from .fem file if available
            fem_path = self.sim_path.with_name(self.sim_path.stem.replace('_sim', '_fem') + '.fem')
            if not fem_path.exists():
                # Try alternative naming patterns
                fem_path = self.sim_path.parent / f"{self.sim_path.stem.split('_')[0]}_fem1.fem"

            if fem_path.exists():
                fem_info = self._extract_fem_from_fem_file(fem_path)
            else:
                # Extract what we can from .sim strings
                fem_info['note'] = 'Limited FEM info available from binary .sim file'

        return fem_info

    def _extract_fem_from_fem_file(self, fem_path: Path) -> Dict[str, Any]:
        """
        Extract FEM information from .fem file.

        Args:
            fem_path: Path to .fem file

        Returns:
            Dictionary with FEM information
        """
        fem_info = {
            'mesh': {},
            'materials': [],
            'element_types': set(),
            'loads': [],
            'constraints': []
        }

        try:
            with open(fem_path, 'rb') as f:
                content = f.read()
            text_content = content.decode('latin-1', errors='ignore')

            # Look for mesh metadata
            mesh_match = re.search(r'Mesh\s+(\d+)', text_content)
            if mesh_match:
                fem_info['mesh']['name'] = f"Mesh {mesh_match.group(1)}"

            # Look for material names
            for material_match in re.finditer(r'MAT\d+\s+([A-Za-z0-9_\-\s]+)', text_content):
                mat_name = material_match.group(1).strip()
                if mat_name and len(mat_name) > 2:
                    fem_info['materials'].append({
                        'name': mat_name,
                        'type': 'Unknown',
                        'properties': {}
                    })

            # Look for element types (Nastran format: CQUAD4, CTRIA3, CTETRA, etc.)
            element_pattern = r'\b(C[A-Z]{3,6}\d?)\b'
            for elem_match in re.finditer(element_pattern, text_content):
                elem_type = elem_match.group(1)
                if elem_type.startswith('C') and len(elem_type) <= 8:
                    fem_info['element_types'].add(elem_type)

            fem_info['element_types'] = list(fem_info['element_types'])

        except Exception as e:
            fem_info['note'] = f'Could not fully parse .fem file: {e}'

        return fem_info

    def get_linked_files(self) -> Dict[str, str]:
        """
        Get paths to linked files (.prt, result files, etc.)

        Returns:
            Dictionary mapping file type to path
        """
        linked_files = {}

        # .prt file (geometry and expressions)
        prt_path = self.sim_path.with_suffix('.prt')
        if prt_path.exists():
            linked_files['part_file'] = str(prt_path)

        # Common result file locations
        result_dir = self.sim_path.parent
        sim_name = self.sim_path.stem

        # Nastran result files
        for ext in ['.op2', '.f06', '.f04', '.bdf']:
            result_file = result_dir / f"{sim_name}{ext}"
            if result_file.exists():
                linked_files[f'result{ext}'] = str(result_file)

        return linked_files


def discover_fea_model(sim_file_path: str) -> Dict[str, Any]:
    """
    MCP Tool: Discover FEA Model

    Analyzes a Siemens NX .sim file and extracts:
    - Solutions (analysis types)
    - Expressions (potential design variables)
    - FEM information (mesh, materials, loads)
    - Linked files

    This is the primary tool for LLM-driven optimization setup.

    Args:
        sim_file_path: Absolute path to .sim file (Windows or Unix format)

    Returns:
        Structured dictionary with model information

    Example:
        >>> result = discover_fea_model("C:/Projects/Bracket/analysis.sim")
        >>> print(result['expressions'])
        [{'name': 'wall_thickness', 'value': 5.0, 'units': 'mm'}, ...]
    """
    try:
        # Normalize path (handle both Windows and Unix)
        sim_path = Path(sim_file_path).resolve()

        # Parse the .sim file
        parser = SimFileParser(sim_path)

        # Extract all components
        result = {
            'status': 'success',
            'sim_file': str(sim_path),
            'file_exists': sim_path.exists(),
            'solutions': parser.extract_solutions(),
            'expressions': parser.extract_expressions(),
            'fem_info': parser.extract_fem_info(),
            'linked_files': parser.get_linked_files(),
            'metadata': {
                'parser_version': '0.1.0',
                'nx_version': 'NX 2412',  # Can be extracted from .sim file in future
            }
        }

        # Add summary statistics
        result['summary'] = {
            'solution_count': len(result['solutions']),
            'expression_count': len(result['expressions']),
            'material_count': len(result['fem_info']['materials']),
            'load_count': len(result['fem_info']['loads']),
            'constraint_count': len(result['fem_info']['constraints']),
        }

        return result

    except FileNotFoundError as e:
        return {
            'status': 'error',
            'error_type': 'file_not_found',
            'message': str(e),
            'suggestion': 'Check that the file path is absolute and the .sim file exists'
        }

    except ValueError as e:
        return {
            'status': 'error',
            'error_type': 'invalid_file',
            'message': str(e),
            'suggestion': 'Ensure the file is a valid NX .sim file (not corrupted or encrypted)'
        }

    except Exception as e:
        return {
            'status': 'error',
            'error_type': 'unexpected_error',
            'message': str(e),
            'suggestion': 'This may be an unsupported .sim file format. Please report this issue.'
        }


def format_discovery_result_for_llm(result: Dict[str, Any]) -> str:
    """
    Format discovery result for LLM consumption (Markdown).

    This is used by the MCP server to present results to the LLM
    in a clear, structured format.

    Args:
        result: Output from discover_fea_model()

    Returns:
        Markdown-formatted string
    """
    if result['status'] != 'success':
        return f"❌ **Error**: {result['message']}\n\n💡 {result['suggestion']}"

    md = []
    md.append(f"# FEA Model Analysis\n")
    md.append(f"**File**: `{result['sim_file']}`\n")

    # Solutions
    md.append(f"## Solutions ({result['summary']['solution_count']})\n")
    for sol in result['solutions']:
        md.append(f"- **{sol['name']}** ({sol['type']}) - Solver: {sol['solver']}")
        if sol['description']:
            md.append(f"  - {sol['description']}")
    md.append("")

    # Expressions (Design Variables)
    md.append(f"## Expressions ({result['summary']['expression_count']})\n")
    if result['expressions']:
        md.append("| Name | Value | Units |")
        md.append("|------|-------|-------|")
        for expr in result['expressions']:
            value = expr.get('value', 'N/A')
            units = expr.get('units', '')
            md.append(f"| `{expr['name']}` | {value} | {units} |")
    else:
        md.append("⚠️ No expressions found. Model may not be parametric.")
    md.append("")

    # FEM Information
    fem = result['fem_info']
    md.append(f"## FEM Information\n")

    if fem['mesh']:
        md.append(f"**Mesh**: {fem['mesh'].get('name', 'Unknown')}")
        md.append(f"- Nodes: {fem['mesh'].get('node_count', 'Unknown')}")
        md.append(f"- Elements: {fem['mesh'].get('element_count', 'Unknown')}")
        md.append("")

    if fem['materials']:
        md.append(f"**Materials** ({len(fem['materials'])})")
        for mat in fem['materials']:
            md.append(f"- {mat['name']} ({mat['type']})")
        md.append("")

    if fem['loads']:
        md.append(f"**Loads** ({len(fem['loads'])})")
        for load in fem['loads']:
            md.append(f"- {load['name']} ({load['type']})")
        md.append("")

    if fem['constraints']:
        md.append(f"**Constraints** ({len(fem['constraints'])})")
        for constraint in fem['constraints']:
            md.append(f"- {constraint['name']} ({constraint['type']})")
        md.append("")

    # Linked Files
    if result['linked_files']:
        md.append(f"## Linked Files\n")
        for file_type, file_path in result['linked_files'].items():
            md.append(f"- **{file_type}**: `{file_path}`")
        md.append("")

    return "\n".join(md)


# For testing/debugging
if __name__ == "__main__":
    import sys

    if len(sys.argv) < 2:
        print("Usage: python model_discovery.py <path_to_sim_file>")
        sys.exit(1)

    sim_path = sys.argv[1]
    result = discover_fea_model(sim_path)

    if result['status'] == 'success':
        print(format_discovery_result_for_llm(result))
        print("\n" + "="*60)
        print("JSON Output:")
        print(json.dumps(result, indent=2))
    else:
        print(f"Error: {result['message']}")
