"""
NX Parameter Updater

Updates design variable values in NX .prt files.

This module can read expressions in two ways:
1. Parse .exp files (NX native export format) - RECOMMENDED, captures ALL expressions
2. Parse binary .prt files directly - fallback method, may miss some expressions

For updating values:
1. Binary .prt file modification (current implementation)
2. Future: Use NXOpen API if NX is running

The .exp format is preferred for reading because it captures:
- All expression types (formulas, references, constants)
- Unitless expressions
- Complete accuracy
"""

from pathlib import Path
from typing import Dict, List, Optional
import re
import shutil
import subprocess
from datetime import datetime
import sys

# Import centralized configuration
sys.path.insert(0, str(Path(__file__).parent.parent))
from config import NX_RUN_JOURNAL


class NXParameterUpdater:
    """
    Updates parametric expression values in NX .prt files.

    NX Expression Formats:

    Binary .prt format:
        #(Number [mm]) tip_thickness: 20.0;
        *(Number [degrees]) support_angle: 30.0;

    .exp export format (RECOMMENDED for reading):
        [MilliMeter]beam_length=5000
        [Kilogram]mass=973.968443678471
        hole_count=10
        Pattern_p7=hole_count
    """

    def __init__(self, prt_file_path: Path, backup: bool = True, nx_run_journal_path: Optional[Path] = None):
        """
        Initialize updater for a specific .prt file.

        Args:
            prt_file_path: Path to NX .prt file
            backup: If True, create backup before modifying
            nx_run_journal_path: Path to NX run_journal.exe (for .exp export)
                               If None, uses default NX 2412 path
        """
        self.prt_path = Path(prt_file_path)

        if not self.prt_path.exists():
            raise FileNotFoundError(f".prt file not found: {prt_file_path}")

        self.backup_enabled = backup
        self.content = None
        self.text_content = None

        # Default NX run_journal.exe path
        if nx_run_journal_path is None:
            self.nx_run_journal_path = NX_RUN_JOURNAL
        else:
            self.nx_run_journal_path = Path(nx_run_journal_path)

        self._load_file()

    def _load_file(self):
        """Load .prt file as binary."""
        with open(self.prt_path, 'rb') as f:
            self.content = bytearray(f.read())

        # Decode as latin-1 for text operations (preserves all bytes)
        self.text_content = self.content.decode('latin-1', errors='ignore')

    def _create_backup(self):
        """Create timestamped backup of original file."""
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        backup_path = self.prt_path.with_suffix(f'.prt.bak_{timestamp}')
        shutil.copy2(self.prt_path, backup_path)
        print(f"Backup created: {backup_path}")
        return backup_path

    def find_expressions(self) -> List[Dict[str, any]]:
        """
        Find all expressions in the .prt file.

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

        # Pattern for NX expressions (with optional units):
        # #(Number [mm]) tip_thickness: 20.0;       - with units
        # *(Number [mm]) p3: 10.0;                  - with units
        # ((Number [degrees]) support_angle: 30.0;  - with units
        # (Number) hole_count: 5.0;                 - without units (unitless)
        pattern = r'[#*\(]*\((\w+)(?:\s*\[([^\]]*)\])?\)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)'

        for match in re.finditer(pattern, self.text_content):
            expr_type, units, name, value = match.groups()
            expressions.append({
                'name': name,
                'value': float(value),
                'units': units if units else '',  # Empty string if no units
                'type': expr_type
            })

        return expressions

    def export_expressions_to_exp(self, output_path: Optional[Path] = None) -> Path:
        """
        Export expressions to .exp file using NX journal.

        This is the RECOMMENDED method for reading expressions because it:
        - Captures ALL expressions (formulas, references, constants)
        - Includes unitless expressions
        - Uses NX's native export, ensuring 100% accuracy

        Args:
            output_path: Path for .exp file (without .exp extension)
                        If None, uses temp file in same directory as .prt

        Returns:
            Path to the .exp file created
        """
        if output_path is None:
            # Create temp file in same directory
            output_path = self.prt_path.with_suffix('')  # Remove .prt
            output_path = Path(str(output_path) + "_expressions")

        # Get paths
        journal_script = Path(__file__).parent / "export_expressions.py"

        if not journal_script.exists():
            raise FileNotFoundError(f"Export journal script not found: {journal_script}")

        if not self.nx_run_journal_path.exists():
            raise FileNotFoundError(f"NX run_journal.exe not found: {self.nx_run_journal_path}")

        # Run NX journal to export expressions
        print(f"[NX] Exporting expressions from {self.prt_path.name} to .exp format...")

        # NX run_journal.exe syntax: run_journal.exe <journal-file> -args <arg1> <arg2> ...
        # Build command string with proper quoting
        cmd_str = f'"{self.nx_run_journal_path}" "{journal_script}" -args "{self.prt_path}" "{output_path}"'

        result = subprocess.run(cmd_str, capture_output=True, text=True, shell=True)

        exp_file = Path(str(output_path) + ".exp")

        # NOTE: NX run_journal.exe treats sys.exit(0) as a "syntax error" even though
        # it's a successful exit. We check if the file was created instead of return code.
        if not exp_file.exists():
            print(f"[ERROR] NX journal failed to create .exp file:")
            print(result.stdout)
            print(result.stderr)
            raise FileNotFoundError(f"Expected .exp file not created: {exp_file}")

        print(f"[OK] Expressions exported to: {exp_file}")
        return exp_file

    def parse_exp_file(self, exp_file_path: Path) -> Dict[str, Dict[str, any]]:
        """
        Parse a .exp file and return all expressions.

        .exp format examples:
            [MilliMeter]beam_length=5000
            [Kilogram]p173=973.968443678471
            hole_count=10
            Pattern_p7=hole_count

        Args:
            exp_file_path: Path to .exp file

        Returns:
            Dict mapping expression name to info dict with 'value', 'units', 'formula'
        """
        expressions = {}

        with open(exp_file_path, 'r', encoding='utf-8') as f:
            for line in f:
                line = line.strip()

                # Skip empty lines and comments
                if not line or line.startswith('//'):
                    continue

                # Pattern: [Unit]name=value or name=value
                # [MilliMeter]beam_length=5000
                # hole_count=10
                # Pattern_p7=hole_count (formula reference)
                match = re.match(r'(?:\[([^\]]+)\])?([a-zA-Z_][a-zA-Z0-9_]*)=(.*)', line)

                if match:
                    units, name, value_str = match.groups()

                    # Try to parse as number
                    try:
                        value = float(value_str)
                        formula = None
                    except ValueError:
                        # It's a formula/reference (e.g., "hole_count")
                        value = None
                        formula = value_str

                    expressions[name] = {
                        'value': value,
                        'units': units if units else '',
                        'formula': formula,
                        'type': 'Number'  # All .exp expressions are Number type
                    }

        return expressions

    def get_all_expressions(self, use_exp_export: bool = True) -> Dict[str, Dict[str, any]]:
        """
        Get all expressions as a dictionary.

        Args:
            use_exp_export: If True, uses NX .exp export (RECOMMENDED)
                           If False, uses binary .prt parsing (may miss expressions)

        Returns:
            Dict mapping expression name to info dict with 'value', 'units', 'type', 'formula'
        """
        if use_exp_export:
            # Use NX native .exp export (captures ALL expressions)
            try:
                exp_file = self.export_expressions_to_exp()
                expressions = self.parse_exp_file(exp_file)

                # Clean up temp file
                exp_file.unlink()

                return expressions
            except Exception as e:
                print(f"[WARNING] .exp export failed: {e}")
                print("[WARNING] Falling back to binary .prt parsing...")
                # Fall through to binary parsing

        # Fallback: Binary .prt parsing
        expressions_list = self.find_expressions()
        return {
            expr['name']: {
                'value': expr['value'],
                'units': expr['units'],
                'type': expr['type'],
                'formula': None  # Binary .prt files don't have formulas accessible
            }
            for expr in expressions_list
        }

    def update_expression(self, name: str, new_value: float) -> bool:
        """
        Update a single expression value.

        Args:
            name: Expression name
            new_value: New value

        Returns:
            True if updated, False if not found
        """
        # Find the expression pattern
        # Match: (Type [units]) name: old_value; OR (Type) name: old_value; (unitless)
        # We need to be careful to match the exact name and preserve formatting

        # Pattern that captures the full expression line
        # Units are optional (unitless expressions like hole_count don't have [units])
        pattern = rf'([#*\(]*\(\w+(?:\s*\[[^\]]*\])?\)\s*)({re.escape(name)})\s*:\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)'

        matches = list(re.finditer(pattern, self.text_content))

        if not matches:
            print(f"Warning: Expression '{name}' not found in .prt file")
            return False

        if len(matches) > 1:
            print(f"Warning: Multiple matches for '{name}', updating first occurrence")

        # Get the first match
        match = matches[0]
        prefix, expr_name, old_value = match.groups()

        # Format new value (preserve decimal places if possible)
        # Check if old value had decimal point
        if '.' in old_value or 'e' in old_value.lower():
            # Use same precision as old value
            decimal_places = len(old_value.split('.')[-1]) if '.' in old_value else 2
            new_value_str = f"{new_value:.{decimal_places}f}"
        else:
            # Integer format
            new_value_str = f"{int(new_value)}"

        # Build replacement string
        replacement = f"{prefix}{expr_name}: {new_value_str}"

        # Replace in text content
        old_match = match.group(0)
        self.text_content = self.text_content.replace(old_match, replacement, 1)

        # Also update in binary content
        old_bytes = old_match.encode('latin-1')
        new_bytes = replacement.encode('latin-1')

        # Find and replace in binary content
        start_pos = self.content.find(old_bytes)
        if start_pos != -1:
            # Replace bytes
            self.content[start_pos:start_pos+len(old_bytes)] = new_bytes

            print(f"Updated: {name} = {old_value} -> {new_value_str}")
            return True
        else:
            print(f"Warning: Could not update binary content for '{name}'")
            return False

    def update_expressions(self, updates: Dict[str, float], use_nx_import: bool = True):
        """
        Update multiple expressions at once.

        Args:
            updates: Dict mapping expression name to new value
                    {'tip_thickness': 22.5, 'support_angle': 35.0}
            use_nx_import: If True, uses NX journal to import .exp file (RECOMMENDED for all expressions)
                          If False, uses binary .prt editing (may miss some expressions)
        """
        if use_nx_import:
            # Use NX journal to import expressions
            return self.update_expressions_via_import(updates)

        # Fallback: Binary .prt editing
        print(f"\nUpdating {len(updates)} expressions in {self.prt_path.name}:")

        updated_count = 0
        for name, value in updates.items():
            if self.update_expression(name, value):
                updated_count += 1

        print(f"Successfully updated {updated_count}/{len(updates)} expressions")

    def update_expressions_via_import(self, updates: Dict[str, float]):
        """
        Update expressions by creating a .exp file and importing it via NX journal.

        This method works for ALL expressions including those not stored in text format
        in the binary .prt file (like hole_count).

        Args:
            updates: Dict mapping expression name to new value
        """
        print(f"\nUpdating {len(updates)} expressions via NX .exp import:")

        # Get all expressions to determine units
        all_expressions = self.get_all_expressions(use_exp_export=True)

        # Create .exp file with ONLY the study variables
        exp_file = self.prt_path.parent / f"{self.prt_path.stem}_study_variables.exp"

        with open(exp_file, 'w', encoding='utf-8') as f:
            for name, value in updates.items():
                if name in all_expressions:
                    units = all_expressions[name].get('units', '')
                    if units:
                        # Expression with units: [MilliMeter]beam_length=5000
                        f.write(f"[{units}]{name}={value}\n")
                    else:
                        # Unitless expression: hole_count=10
                        f.write(f"{name}={value}\n")
                    print(f"  {name}: {value} {units if units else ''}")
                else:
                    print(f"  Warning: {name} not found in part expressions, skipping")

        print(f"\n[EXP] Created: {exp_file}")

        # Run NX journal to import expressions
        journal_script = Path(__file__).parent / "import_expressions.py"

        if not journal_script.exists():
            raise FileNotFoundError(f"Import journal script not found: {journal_script}")

        if not self.nx_run_journal_path.exists():
            raise FileNotFoundError(f"NX run_journal.exe not found: {self.nx_run_journal_path}")

        print(f"[NX] Importing expressions into {self.prt_path.name}...")

        # Build command
        cmd_str = f'"{self.nx_run_journal_path}" "{journal_script}" -args "{self.prt_path}" "{exp_file}"'

        result = subprocess.run(cmd_str, capture_output=True, text=True, shell=True)

        # Clean up .exp file
        exp_file.unlink()

        # Check if import succeeded
        if result.returncode != 0 and "successfully" not in result.stdout.lower():
            print(f"[ERROR] NX journal failed:")
            print(result.stdout)
            print(result.stderr)
            raise RuntimeError(f"Expression import failed")

        print(f"[OK] All {len(updates)} expressions updated successfully!")

    def save(self, output_path: Path = None):
        """
        Save modified .prt file.

        Args:
            output_path: Optional different path to save to.
                        If None, overwrites original (with backup if enabled)
        """
        if output_path is None:
            output_path = self.prt_path
            if self.backup_enabled:
                self._create_backup()

        # Write updated binary content
        with open(output_path, 'wb') as f:
            f.write(self.content)

        print(f"Saved to: {output_path}")

    def verify_update(self, name: str, expected_value: float, tolerance: float = 1e-6) -> bool:
        """
        Verify that an expression was updated correctly.

        Args:
            name: Expression name
            expected_value: Expected value
            tolerance: Acceptable difference

        Returns:
            True if value matches (within tolerance)
        """
        expressions = self.find_expressions()
        expr = next((e for e in expressions if e['name'] == name), None)

        if expr is None:
            print(f"Expression '{name}' not found")
            return False

        actual_value = expr['value']
        difference = abs(actual_value - expected_value)

        if difference <= tolerance:
            print(f"OK Verified: {name} = {actual_value} (expected {expected_value})")
            return True
        else:
            print(f"FAIL Verification failed: {name} = {actual_value}, expected {expected_value} (diff: {difference})")
            return False


# Convenience function for optimization loop
def update_nx_model(prt_file_path: Path, design_variables: Dict[str, float], backup: bool = False):
    """
    Convenience function to update NX model parameters.

    Args:
        prt_file_path: Path to .prt file
        design_variables: Dict of parameter name -> value
        backup: Whether to create backup

    Example:
        >>> update_nx_model(
        ...     Path("Bracket.prt"),
        ...     {'tip_thickness': 22.5, 'support_angle': 35.0}
        ... )
    """
    updater = NXParameterUpdater(prt_file_path, backup=backup)
    updater.update_expressions(design_variables)
    updater.save()


# Example usage
if __name__ == "__main__":
    import sys

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

    prt_path = Path(sys.argv[1])

    # Test: Find and print all expressions
    print("="*60)
    print("NX PARAMETER UPDATER TEST")
    print("="*60)

    updater = NXParameterUpdater(prt_path, backup=True)

    print("\nCurrent expressions in file:")
    expressions = updater.find_expressions()
    for expr in expressions:
        print(f"  {expr['name']}: {expr['value']} {expr['units']}")

    # Test update (if expressions found)
    if expressions:
        print("\n" + "="*60)
        print("TEST UPDATE")
        print("="*60)

        # Update first expression
        first_expr = expressions[0]
        test_name = first_expr['name']
        test_new_value = first_expr['value'] * 1.1  # Increase by 10%

        print(f"\nUpdating {test_name} from {first_expr['value']} to {test_new_value}")

        updater.update_expression(test_name, test_new_value)

        # Save to test file
        test_output = prt_path.with_suffix('.prt.test')
        updater.save(test_output)

        # Verify by re-reading
        print("\n" + "="*60)
        print("VERIFICATION")
        print("="*60)
        verifier = NXParameterUpdater(test_output, backup=False)
        verifier.verify_update(test_name, test_new_value)

        print(f"\nTest complete. Modified file: {test_output}")
    else:
        print("\nNo expressions found in file. Nothing to test.")
