#!/usr/bin/env python
"""
Extract Mirror Optical Specifications from FEA Mesh Geometry

This tool analyzes mirror mesh geometry to estimate optical specifications
including focal length, aperture diameter, f-number, and radius of curvature.

Usage:
    # From study directory containing OP2 files
    python -m optimization_engine.tools.extract_mirror_optical_specs .

    # From specific OP2 file
    python -m optimization_engine.tools.extract_mirror_optical_specs path/to/results.op2

    # Save to study README
    python -m optimization_engine.tools.extract_mirror_optical_specs . --update-readme

Output:
    - Console: Optical specifications summary
    - Optional: Updates parent README.md with validated specs

Author: Atomizer Framework
"""

from pathlib import Path
import argparse
import sys

# Add project root to path (tools/ is at project root, so parent is Atomizer/)
sys.path.insert(0, str(Path(__file__).parent.parent))

import numpy as np


def find_op2_file(path: Path) -> Path:
    """Find an OP2 file from path (file or directory)."""
    path = Path(path)

    if path.is_file() and path.suffix.lower() == '.op2':
        return path

    if path.is_dir():
        # Look in common locations
        search_patterns = [
            '**/2_iterations/**/*.op2',
            '**/*.op2',
            '2_iterations/**/*.op2',
            '1_setup/model/*.op2',
        ]

        for pattern in search_patterns:
            op2_files = list(path.glob(pattern))
            if op2_files:
                # Return most recent
                return max(op2_files, key=lambda p: p.stat().st_mtime)

    raise FileNotFoundError(f"No OP2 file found in {path}")


def extract_optical_specs(op2_path: Path, verbose: bool = True) -> dict:
    """
    Extract optical specifications from mirror mesh geometry.

    Args:
        op2_path: Path to OP2 file
        verbose: Print detailed output

    Returns:
        dict with optical specifications
    """
    from optimization_engine.extractors.extract_zernike_opd import (
        ZernikeOPDExtractor,
        estimate_focal_length_from_geometry
    )

    if verbose:
        print(f"Analyzing: {op2_path}")
        print("=" * 60)

    extractor = ZernikeOPDExtractor(op2_path)

    # Get geometry
    geo = extractor.node_geometry
    all_pos = np.array(list(geo.values()))
    x, y, z = all_pos[:, 0], all_pos[:, 1], all_pos[:, 2]

    # Compute radius/diameter
    r = np.sqrt(x**2 + y**2)

    # Estimate focal length
    focal = estimate_focal_length_from_geometry(x, y, z, concave=True)

    # Derived quantities
    diameter = 2 * r.max()
    f_number = focal / diameter
    RoC = 2 * focal  # Radius of curvature
    sag = r.max()**2 / (4 * focal)  # Surface sag at edge
    central_obs = r.min() if r.min() > 1.0 else 0.0  # Central obscuration

    # Parabola fit quality check
    r_sq = x**2 + y**2
    A = np.column_stack([r_sq, np.ones_like(r_sq)])
    coeffs, _, _, _ = np.linalg.lstsq(A, z, rcond=None)
    a, b = coeffs
    z_fit = a * r_sq + b
    rms_error = np.sqrt(np.mean((z - z_fit)**2))

    # Determine fit quality
    if rms_error < 0.1:
        fit_quality = "Excellent"
        fit_note = "Focal length estimate is reliable"
    elif rms_error < 1.0:
        fit_quality = "Good"
        fit_note = "Focal length estimate is reasonably accurate"
    else:
        fit_quality = "Poor"
        fit_note = "Consider using explicit focal length from optical design"

    specs = {
        'aperture_diameter_mm': diameter,
        'aperture_radius_mm': r.max(),
        'focal_length_mm': focal,
        'f_number': f_number,
        'radius_of_curvature_mm': RoC,
        'surface_sag_mm': sag,
        'central_obscuration_mm': central_obs,
        'node_count': len(geo),
        'x_range_mm': (x.min(), x.max()),
        'y_range_mm': (y.min(), y.max()),
        'z_range_mm': (z.min(), z.max()),
        'parabola_fit_rms_mm': rms_error,
        'fit_quality': fit_quality,
        'fit_note': fit_note,
        'source_file': str(op2_path),
    }

    if verbose:
        print()
        print("MIRROR OPTICAL SPECIFICATIONS (from mesh geometry)")
        print("=" * 60)
        print()
        print(f"Aperture Diameter:     {diameter:.1f} mm ({diameter/1000:.3f} m)")
        print(f"Aperture Radius:       {r.max():.1f} mm")
        if central_obs > 0:
            print(f"Central Obscuration:   {central_obs:.1f} mm")
        print()
        print(f"Estimated Focal Length: {focal:.1f} mm ({focal/1000:.3f} m)")
        print(f"Radius of Curvature:    {RoC:.1f} mm ({RoC/1000:.3f} m)")
        print(f"f-number (f/D):         f/{f_number:.2f}")
        print()
        print(f"Surface Sag at Edge:    {sag:.2f} mm")
        print()
        print("--- Mesh Statistics ---")
        print(f"Node count:    {len(geo)}")
        print(f"X range:       {x.min():.1f} to {x.max():.1f} mm")
        print(f"Y range:       {y.min():.1f} to {y.max():.1f} mm")
        print(f"Z range:       {z.min():.2f} to {z.max():.2f} mm")
        print()
        print("--- Parabola Fit Quality ---")
        print(f"RMS fit residual: {rms_error:.4f} mm ({rms_error*1000:.2f} µm)")
        print(f"Quality: {fit_quality} - {fit_note}")
        print()
        print("=" * 60)

    return specs


def generate_readme_section(specs: dict) -> str:
    """Generate markdown section for README."""
    return f"""## 2. Optical Prescription

> **Source**: Estimated from mesh geometry. Validate against optical design.

| Parameter | Value | Units | Status |
|-----------|-------|-------|--------|
| Aperture Diameter | {specs['aperture_diameter_mm']:.1f} | mm | Estimated |
| Focal Length | {specs['focal_length_mm']:.1f} | mm | Estimated |
| f-number | f/{specs['f_number']:.2f} | - | Computed |
| Radius of Curvature | {specs['radius_of_curvature_mm']:.1f} | mm | Computed (2×f) |
| Central Obscuration | {specs['central_obscuration_mm']:.1f} | mm | From mesh |
| Surface Type | Parabola | - | Assumed |

**Fit Quality**: {specs['fit_quality']} ({specs['fit_note']})

### 2.1 Usage in OPD Extractor

For rigorous WFE analysis, use explicit focal length:

```python
from optimization_engine.extractors import ZernikeOPDExtractor

extractor = ZernikeOPDExtractor(
    op2_file,
    focal_length={specs['focal_length_mm']:.1f},  # mm - validate against design
    concave=True
)
```
"""


def update_readme(study_dir: Path, specs: dict):
    """Update parent README with optical specs."""
    readme_path = study_dir / 'README.md'

    if not readme_path.exists():
        print(f"README.md not found at {readme_path}")
        return False

    content = readme_path.read_text(encoding='utf-8')

    # Find and replace optical prescription section
    new_section = generate_readme_section(specs)

    # Look for existing section
    import re
    pattern = r'## 2\. Optical Prescription.*?(?=## 3\.|$)'

    if re.search(pattern, content, re.DOTALL):
        content = re.sub(pattern, new_section + '\n---\n\n', content, flags=re.DOTALL)
        print(f"Updated optical prescription in {readme_path}")
    else:
        print(f"Could not find '## 2. Optical Prescription' section in {readme_path}")
        print("Please add manually or check section numbering.")
        return False

    readme_path.write_text(content, encoding='utf-8')
    return True


def main():
    parser = argparse.ArgumentParser(
        description='Extract mirror optical specifications from FEA mesh',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
    # Analyze current study directory
    python -m optimization_engine.tools.extract_mirror_optical_specs .

    # Analyze specific OP2 file
    python -m optimization_engine.tools.extract_mirror_optical_specs results.op2

    # Update parent README with specs
    python -m optimization_engine.tools.extract_mirror_optical_specs . --update-readme
        """
    )

    parser.add_argument('path', type=str,
                       help='Path to OP2 file or study directory')
    parser.add_argument('--update-readme', action='store_true',
                       help='Update parent README.md with optical specs')
    parser.add_argument('--quiet', '-q', action='store_true',
                       help='Suppress detailed output')
    parser.add_argument('--json', action='store_true',
                       help='Output specs as JSON')

    args = parser.parse_args()

    try:
        path = Path(args.path).resolve()
        op2_path = find_op2_file(path)

        specs = extract_optical_specs(op2_path, verbose=not args.quiet and not args.json)

        if args.json:
            import json
            print(json.dumps(specs, indent=2, default=str))

        if args.update_readme:
            # Find study root (parent of geometry type folder)
            study_dir = path if path.is_dir() else path.parent
            # Go up to geometry type level
            while study_dir.name not in ['studies', ''] and not (study_dir / 'README.md').exists():
                if (study_dir.parent / 'README.md').exists():
                    study_dir = study_dir.parent
                    break
                study_dir = study_dir.parent

            if (study_dir / 'README.md').exists():
                update_readme(study_dir, specs)
            else:
                print(f"Could not find parent README.md to update")

    except FileNotFoundError as e:
        print(f"Error: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == '__main__':
    main()
