"""
Temperature Extractor for Thermal Analysis Results
===================================================

Phase 3 Task 3.1 - NX Open Automation Roadmap

Extracts nodal temperature results from Nastran OP2 files for thermal optimization.

Usage:
    from optimization_engine.extractors.extract_temperature import extract_temperature

    result = extract_temperature("path/to/thermal.op2", subcase=1)
    print(f"Max temperature: {result['max_temperature']} K")

Supports:
    - SOL 153 (Steady-State Heat Transfer)
    - SOL 159 (Transient Heat Transfer)
    - Thermal subcases in coupled analyses
"""

import numpy as np
from pathlib import Path
from typing import Dict, Any, Optional, List, Union
import logging

logger = logging.getLogger(__name__)


def extract_temperature(
    op2_file: Union[str, Path],
    subcase: int = 1,
    nodes: Optional[List[int]] = None,
    return_field: bool = False
) -> Dict[str, Any]:
    """
    Extract nodal temperatures from thermal analysis OP2 file.

    Args:
        op2_file: Path to the OP2 results file
        subcase: Subcase number to extract (default: 1)
        nodes: Optional list of specific node IDs to extract.
               If None, extracts all nodes.
        return_field: If True, include full temperature field in result

    Returns:
        dict: {
            'success': bool,
            'max_temperature': float (K or °C depending on model units),
            'min_temperature': float,
            'avg_temperature': float,
            'max_node_id': int (node with max temperature),
            'min_node_id': int (node with min temperature),
            'node_count': int,
            'temperatures': dict (node_id: temp) - only if return_field=True,
            'unit': str ('K' or 'C'),
            'subcase': int,
            'error': str or None
        }

    Example:
        >>> result = extract_temperature("thermal_analysis.op2", subcase=1)
        >>> if result['success']:
        ...     print(f"Max temp: {result['max_temperature']:.1f} K at node {result['max_node_id']}")
        ...     print(f"Temperature range: {result['min_temperature']:.1f} - {result['max_temperature']:.1f} K")
    """
    op2_path = Path(op2_file)

    if not op2_path.exists():
        return {
            'success': False,
            'error': f"OP2 file not found: {op2_path}",
            'max_temperature': None,
            'min_temperature': None,
            'avg_temperature': None,
            'max_node_id': None,
            'min_node_id': None,
            'node_count': 0,
            'subcase': subcase
        }

    try:
        from pyNastran.op2.op2 import read_op2

        # Read OP2 with minimal output
        op2 = read_op2(str(op2_path), load_geometry=False, debug=False, log=None)

        # Check for temperature results
        # pyNastran stores temperatures in different attributes depending on analysis type
        temperatures = None

        # Method 1: Check temperatures attribute (SOL 153/159)
        if hasattr(op2, 'temperatures') and op2.temperatures:
            if subcase in op2.temperatures:
                temp_data = op2.temperatures[subcase]
                temperatures = _extract_from_table(temp_data, nodes)
                logger.debug(f"Found temperatures in op2.temperatures[{subcase}]")

        # Method 2: Check thermal load results (alternative storage)
        if temperatures is None and hasattr(op2, 'thermal_load_vectors'):
            if subcase in op2.thermal_load_vectors:
                temp_data = op2.thermal_load_vectors[subcase]
                temperatures = _extract_from_table(temp_data, nodes)
                logger.debug(f"Found temperatures in op2.thermal_load_vectors[{subcase}]")

        # Method 3: Check displacement as temperature (some solvers store temp in disp)
        # In thermal analysis, "displacement" can actually be temperature
        if temperatures is None and hasattr(op2, 'displacements') and op2.displacements:
            if subcase in op2.displacements:
                disp_data = op2.displacements[subcase]
                # Check if this is actually temperature data
                # Temperature data typically has only 1 DOF (scalar field)
                if hasattr(disp_data, 'data'):
                    data = disp_data.data
                    if len(data.shape) >= 2:
                        # For thermal, we expect scalar temperature at each node
                        # Column 0 of translational data contains temperature
                        temps_array = data[0, :, 0] if len(data.shape) == 3 else data[:, 0]
                        node_ids = disp_data.node_gridtype[:, 0]
                        temperatures = {int(nid): float(temps_array[i])
                                       for i, nid in enumerate(node_ids)
                                       if nodes is None or nid in nodes}
                        logger.debug(f"Found temperatures in op2.displacements[{subcase}] (thermal mode)")

        if temperatures is None or len(temperatures) == 0:
            # List available subcases for debugging
            available_subcases = []
            if hasattr(op2, 'temperatures') and op2.temperatures:
                available_subcases.extend(list(op2.temperatures.keys()))

            return {
                'success': False,
                'error': f"No temperature results found for subcase {subcase}. "
                        f"Available subcases: {available_subcases}",
                'max_temperature': None,
                'min_temperature': None,
                'avg_temperature': None,
                'max_node_id': None,
                'min_node_id': None,
                'node_count': 0,
                'subcase': subcase
            }

        # Compute statistics
        temp_values = np.array(list(temperatures.values()))
        temp_nodes = np.array(list(temperatures.keys()))

        max_idx = np.argmax(temp_values)
        min_idx = np.argmin(temp_values)

        result = {
            'success': True,
            'error': None,
            'max_temperature': float(temp_values[max_idx]),
            'min_temperature': float(temp_values[min_idx]),
            'avg_temperature': float(np.mean(temp_values)),
            'std_temperature': float(np.std(temp_values)),
            'max_node_id': int(temp_nodes[max_idx]),
            'min_node_id': int(temp_nodes[min_idx]),
            'node_count': len(temperatures),
            'unit': 'K',  # Nastran typically uses Kelvin
            'subcase': subcase
        }

        if return_field:
            result['temperatures'] = temperatures

        return result

    except ImportError:
        return {
            'success': False,
            'error': "pyNastran not installed. Install with: pip install pyNastran",
            'max_temperature': None,
            'min_temperature': None,
            'avg_temperature': None,
            'max_node_id': None,
            'min_node_id': None,
            'node_count': 0,
            'subcase': subcase
        }
    except Exception as e:
        logger.exception(f"Error extracting temperature from {op2_path}")
        return {
            'success': False,
            'error': str(e),
            'max_temperature': None,
            'min_temperature': None,
            'avg_temperature': None,
            'max_node_id': None,
            'min_node_id': None,
            'node_count': 0,
            'subcase': subcase
        }


def _extract_from_table(temp_data, nodes: Optional[List[int]] = None) -> Dict[int, float]:
    """Extract temperature values from a pyNastran result table."""
    temperatures = {}

    if hasattr(temp_data, 'data') and hasattr(temp_data, 'node_gridtype'):
        data = temp_data.data
        node_ids = temp_data.node_gridtype[:, 0]

        # Data shape is typically (ntimes, nnodes, ncomponents)
        # For temperature, we want the first component
        if len(data.shape) == 3:
            temp_values = data[0, :, 0]  # First time step, all nodes, first component
        elif len(data.shape) == 2:
            temp_values = data[:, 0]
        else:
            temp_values = data

        for i, nid in enumerate(node_ids):
            if nodes is None or nid in nodes:
                temperatures[int(nid)] = float(temp_values[i])

    return temperatures


def extract_temperature_gradient(
    op2_file: Union[str, Path],
    subcase: int = 1,
    method: str = 'nodal_difference'
) -> Dict[str, Any]:
    """
    Extract temperature gradients from thermal analysis.

    Computes temperature gradients based on nodal temperature differences.
    This is useful for identifying thermal stress hot spots.

    Args:
        op2_file: Path to the OP2 results file
        subcase: Subcase number
        method: Gradient calculation method:
                - 'nodal_difference': Max temperature difference between adjacent nodes
                - 'element_based': Gradient within elements (requires mesh connectivity)

    Returns:
        dict: {
            'success': bool,
            'max_gradient': float (K/mm or temperature units/length),
            'avg_gradient': float,
            'temperature_range': float (max - min temperature),
            'gradient_location': tuple (node_id_hot, node_id_cold),
            'error': str or None
        }

    Note:
        For accurate gradients, element-based calculation requires mesh connectivity
        which may not be available in all OP2 files. The nodal_difference method
        provides an approximation based on temperature range.
    """
    # First extract temperatures
    temp_result = extract_temperature(op2_file, subcase=subcase, return_field=True)

    if not temp_result['success']:
        return {
            'success': False,
            'error': temp_result['error'],
            'max_gradient': None,
            'avg_gradient': None,
            'temperature_range': None,
            'gradient_location': None
        }

    temperatures = temp_result.get('temperatures', {})

    if len(temperatures) < 2:
        return {
            'success': False,
            'error': "Need at least 2 nodes to compute gradient",
            'max_gradient': None,
            'avg_gradient': None,
            'temperature_range': None,
            'gradient_location': None
        }

    # Compute temperature range (proxy for max gradient without mesh)
    temp_range = temp_result['max_temperature'] - temp_result['min_temperature']

    # For nodal_difference method, we report the temperature range
    # True gradient computation would require mesh connectivity
    return {
        'success': True,
        'error': None,
        'max_gradient': temp_range,  # Simplified - actual gradient needs mesh
        'avg_gradient': temp_range / 2,  # Rough estimate
        'temperature_range': temp_range,
        'gradient_location': (temp_result['max_node_id'], temp_result['min_node_id']),
        'max_temperature': temp_result['max_temperature'],
        'min_temperature': temp_result['min_temperature'],
        'note': "Gradient approximated from temperature range. "
                "Accurate gradient requires mesh connectivity."
    }


def extract_heat_flux(
    op2_file: Union[str, Path],
    subcase: int = 1,
    element_type: str = 'all'
) -> Dict[str, Any]:
    """
    Extract element heat flux from thermal analysis OP2 file.

    Args:
        op2_file: Path to the OP2 results file
        subcase: Subcase number
        element_type: Element type to extract ('all', 'ctetra', 'chexa', etc.)

    Returns:
        dict: {
            'success': bool,
            'max_heat_flux': float (W/mm² or model units),
            'min_heat_flux': float,
            'avg_heat_flux': float,
            'max_element_id': int,
            'element_count': int,
            'unit': str,
            'error': str or None
        }
    """
    op2_path = Path(op2_file)

    if not op2_path.exists():
        return {
            'success': False,
            'error': f"OP2 file not found: {op2_path}",
            'max_heat_flux': None,
            'min_heat_flux': None,
            'avg_heat_flux': None,
            'max_element_id': None,
            'element_count': 0,
            'subcase': subcase
        }

    try:
        from pyNastran.op2.op2 import read_op2

        op2 = read_op2(str(op2_path), load_geometry=False, debug=False, log=None)

        # Check for heat flux results
        # pyNastran stores thermal flux in chbdyg_thermal_load or similar
        heat_flux_data = None

        # Check various thermal flux attributes
        flux_attrs = [
            'chbdyg_thermal_load',
            'chbdye_thermal_load',
            'chbdyp_thermal_load',
            'thermalLoad_CONV',
            'thermalLoad_CHBDY'
        ]

        for attr in flux_attrs:
            if hasattr(op2, attr):
                data = getattr(op2, attr)
                if data and subcase in data:
                    heat_flux_data = data[subcase]
                    logger.debug(f"Found heat flux in op2.{attr}[{subcase}]")
                    break

        if heat_flux_data is None:
            return {
                'success': False,
                'error': f"No heat flux results found for subcase {subcase}. "
                        "Heat flux output may not be requested in the analysis.",
                'max_heat_flux': None,
                'min_heat_flux': None,
                'avg_heat_flux': None,
                'max_element_id': None,
                'element_count': 0,
                'subcase': subcase
            }

        # Extract flux values
        if hasattr(heat_flux_data, 'data'):
            data = heat_flux_data.data
            element_ids = heat_flux_data.element if hasattr(heat_flux_data, 'element') else []

            # Flux magnitude
            if len(data.shape) == 3:
                flux_values = np.linalg.norm(data[0, :, :], axis=1)
            else:
                flux_values = np.abs(data[:, 0]) if len(data.shape) == 2 else data

            max_idx = np.argmax(flux_values)

            return {
                'success': True,
                'error': None,
                'max_heat_flux': float(np.max(flux_values)),
                'min_heat_flux': float(np.min(flux_values)),
                'avg_heat_flux': float(np.mean(flux_values)),
                'max_element_id': int(element_ids[max_idx]) if len(element_ids) > max_idx else None,
                'element_count': len(flux_values),
                'unit': 'W/mm²',
                'subcase': subcase
            }

        return {
            'success': False,
            'error': "Could not parse heat flux data format",
            'max_heat_flux': None,
            'min_heat_flux': None,
            'avg_heat_flux': None,
            'max_element_id': None,
            'element_count': 0,
            'subcase': subcase
        }

    except Exception as e:
        logger.exception(f"Error extracting heat flux from {op2_path}")
        return {
            'success': False,
            'error': str(e),
            'max_heat_flux': None,
            'min_heat_flux': None,
            'avg_heat_flux': None,
            'max_element_id': None,
            'element_count': 0,
            'subcase': subcase
        }


# Convenience function for optimization constraints
def get_max_temperature(op2_file: Union[str, Path], subcase: int = 1) -> float:
    """
    Get maximum temperature from OP2 file.

    Convenience function for use in optimization constraints.
    Returns inf if extraction fails.

    Args:
        op2_file: Path to OP2 file
        subcase: Subcase number

    Returns:
        float: Maximum temperature or inf on failure
    """
    result = extract_temperature(op2_file, subcase=subcase)
    if result['success']:
        return result['max_temperature']
    else:
        logger.warning(f"Temperature extraction failed: {result['error']}")
        return float('inf')


if __name__ == "__main__":
    import sys

    if len(sys.argv) > 1:
        op2_file = sys.argv[1]
        subcase = int(sys.argv[2]) if len(sys.argv) > 2 else 1

        print(f"Extracting temperature from: {op2_file}")
        result = extract_temperature(op2_file, subcase=subcase)

        if result['success']:
            print(f"\n=== Temperature Results (Subcase {subcase}) ===")
            print(f"Max temperature: {result['max_temperature']:.2f} {result['unit']} (node {result['max_node_id']})")
            print(f"Min temperature: {result['min_temperature']:.2f} {result['unit']} (node {result['min_node_id']})")
            print(f"Avg temperature: {result['avg_temperature']:.2f} {result['unit']}")
            print(f"Node count: {result['node_count']}")
        else:
            print(f"Error: {result['error']}")
    else:
        print("Usage: python extract_temperature.py <op2_file> [subcase]")
