"""
Fracture analysis for karst systems.
Analyzes fracture patterns, density, and orientation for passage development.

Author: Samir Baladi
"""

import numpy as np
from typing import Dict, List, Tuple, Optional, Union
from dataclasses import dataclass
from scipy import stats
import logging

logger = logging.getLogger(__name__)

@dataclass
class Fracture:
    """Represents a single fracture measurement."""
    orientation: float  # Strike orientation (degrees)
    dip: float  # Dip angle (degrees)
    length: float  # Fracture length (m)
    aperture: float  # Fracture aperture (mm)
    spacing: float  # Spacing to next fracture (m)
    location: str  # Measurement location
    timestamp: float  # Measurement time


class FractureAnalysis:
    """
    Analyzes fracture patterns and their influence on cave passage development.
    """
    
    def __init__(self):
        """Initialize fracture analysis."""
        self.fractures = []
        self.sets = {}
        logger.info("Initialized FractureAnalysis")

    def add_fracture(self,
                    orientation: float,
                    dip: float,
                    length: float,
                    aperture: float,
                    spacing: float,
                    location: str,
                    timestamp: float) -> None:
        """
        Add a fracture measurement.
        
        Args:
            orientation: Strike orientation (0-360°)
            dip: Dip angle (0-90°)
            length: Fracture length (m)
            aperture: Fracture aperture (mm)
            spacing: Spacing to next fracture (m)
            location: Measurement location
            timestamp: Measurement time
        """
        fracture = Fracture(
            orientation=orientation % 360,
            dip=dip,
            length=length,
            aperture=aperture,
            spacing=spacing,
            location=location,
            timestamp=timestamp
        )
        
        self.fractures.append(fracture)
        logger.debug(f"Added fracture at {location}: {orientation}° strike, {dip}° dip")

    def analyze_orientation(self) -> Dict[str, Union[np.ndarray, float]]:
        """
        Analyze fracture orientation distribution.
        
        Returns:
            Dictionary with orientation statistics
        """
        if not self.fractures:
            return {'status': 'No fracture data'}
        
        orientations = np.array([f.orientation for f in self.fractures])
        
        # Calculate circular statistics
        rad_orient = np.radians(orientations)
        
        # Mean direction (circular mean)
        sin_sum = np.sum(np.sin(rad_orient))
        cos_sum = np.sum(np.cos(rad_orient))
        mean_rad = np.arctan2(sin_sum, cos_sum)
        mean_orientation = np.degrees(mean_rad) % 360
        
        # Circular variance
        R = np.sqrt(sin_sum**2 + cos_sum**2) / len(orientations)
        circular_variance = 1 - R
        
        # Find dominant sets (simplified clustering)
        # Bin orientations
        bins = np.arange(0, 361, 30)
        hist, edges = np.histogram(orientations, bins=bins)
        
        dominant_sets = []
        for i in range(len(hist)):
            if hist[i] > len(orientations) / 6:  # More than expected random
                center = (edges[i] + edges[i+1]) / 2
                dominant_sets.append({
                    'center': round(center),
                    'count': hist[i],
                    'percentage': round(hist[i] / len(orientations) * 100, 1)
                })
        
        return {
            'mean_orientation': round(mean_orientation, 1),
            'circular_variance': round(circular_variance, 3),
            'n_fractures': len(orientations),
            'dominant_sets': dominant_sets,
            'orientations': orientations.tolist()
        }

    def analyze_dip(self) -> Dict[str, float]:
        """
        Analyze dip angle distribution.
        
        Returns:
            Dictionary with dip statistics
        """
        if not self.fractures:
            return {'status': 'No fracture data'}
        
        dips = np.array([f.dip for f in self.fractures])
        
        return {
            'mean_dip': round(np.mean(dips), 1),
            'std_dip': round(np.std(dips), 1),
            'min_dip': round(np.min(dips), 1),
            'max_dip': round(np.max(dips), 1),
            'median_dip': round(np.median(dips), 1),
            'n_vertical': sum(1 for d in dips if d > 80),
            'n_horizontal': sum(1 for d in dips if d < 10)
        }

    def calculate_fracture_density(self,
                                  area: float,
                                  location: Optional[str] = None) -> float:
        """
        Calculate fracture density (fractures per meter).
        
        Args:
            area: Survey area (m²)
            location: Specific location to analyze (optional)
            
        Returns:
            Fracture density (fractures/m²)
        """
        if location:
            fractures = [f for f in self.fractures if f.location == location]
        else:
            fractures = self.fractures
        
        if not fractures or area <= 0:
            return 0
        
        return len(fractures) / area

    def analyze_aperture(self) -> Dict[str, float]:
        """
        Analyze fracture aperture distribution.
        
        Returns:
            Dictionary with aperture statistics
        """
        if not self.fractures:
            return {'status': 'No fracture data'}
        
        apertures = np.array([f.aperture for f in self.fractures])
        
        return {
            'mean_aperture': round(np.mean(apertures), 2),
            'std_aperture': round(np.std(apertures), 2),
            'min_aperture': round(np.min(apertures), 2),
            'max_aperture': round(np.max(apertures), 2),
            'median_aperture': round(np.median(apertures), 2),
            'total_openings': sum(apertures)
        }

    def predict_passage_orientation(self) -> Dict[str, Union[float, str]]:
        """
        Predict likely passage orientation based on fracture patterns.
        
        Returns:
            Dictionary with predicted orientation
        """
        orientation_analysis = self.analyze_orientation()
        
        if 'status' in orientation_analysis:
            return {'status': 'Insufficient fracture data'}
        
        # Passage tends to follow dominant fracture sets
        if orientation_analysis['dominant_sets']:
            # Primary orientation along dominant set
            primary = orientation_analysis['dominant_sets'][0]['center']
            
            # Secondary orientation perpendicular
            secondary = (primary + 90) % 360
            
            return {
                'primary_orientation': primary,
                'secondary_orientation': secondary,
                'confidence': round(len(orientation_analysis['dominant_sets'][0]['count']) / 
                                  orientation_analysis['n_fractures'] * 100, 1),
                'description': f"Passages likely follow {primary}° trend with cross-cuts at {secondary}°"
            }
        else:
            # No dominant set, use mean
            return {
                'primary_orientation': round(orientation_analysis['mean_orientation']),
                'secondary_orientation': round((orientation_analysis['mean_orientation'] + 90) % 360),
                'confidence': 50,
                'description': "No dominant fracture set - passage orientation variable"
            }

    def calculate_flow_enhancement(self,
                                  orientation: float,
                                  fracture_set: Optional[Dict] = None) -> float:
        """
        Calculate flow enhancement factor for given orientation.
        
        Args:
            orientation: Passage orientation (degrees)
            fracture_set: Specific fracture set to use (optional)
            
        Returns:
            Flow enhancement factor (>1 if aligned with fractures)
        """
        if not self.fractures:
            return 1.0
        
        if fracture_set:
            center = fracture_set['center']
            # Enhancement decreases with angular difference
            diff = min(abs(orientation - center), 360 - abs(orientation - center))
            enhancement = 1 + np.exp(-diff/45)  # Max 2 when aligned
        else:
            # Average enhancement across all fractures
            orientations = np.array([f.orientation for f in self.fractures])
            diffs = np.minimum(np.abs(orientations - orientation), 
                              360 - np.abs(orientations - orientation))
            enhancement = 1 + np.mean(np.exp(-diffs/45))
        
        return round(enhancement, 2)

    def identify_fracture_zones(self,
                               min_density: float = 0.5) -> List[Dict[str, Union[str, float]]]:
        """
        Identify high-density fracture zones.
        
        Args:
            min_density: Minimum density for zone identification
            
        Returns:
            List of fracture zones
        """
        if not self.fractures:
            return []
        
        # Group by location
        locations = {}
        for f in self.fractures:
            if f.location not in locations:
                locations[f.location] = []
            locations[f.location].append(f)
        
        zones = []
        for loc, fracs in locations.items():
            if len(fracs) >= 3:  # Minimum for zone
                avg_aperture = np.mean([f.aperture for f in fracs])
                avg_spacing = np.mean([f.spacing for f in fracs])
                density = len(fracs) / 100  # Approximate per 100m
                
                if density >= min_density:
                    zones.append({
                        'location': loc,
                        'n_fractures': len(fracs),
                        'density': round(density, 2),
                        'avg_aperture': round(avg_aperture, 2),
                        'avg_spacing': round(avg_spacing, 2),
                        'risk_level': 'HIGH' if avg_aperture > 10 else 'MODERATE' if avg_aperture > 5 else 'LOW'
                    })
        
        return sorted(zones, key=lambda x: x['density'], reverse=True)

    def analyze_stability_impact(self,
                                fracture_density: float,
                                aperture: float) -> Dict[str, Union[str, float]]:
        """
        Analyze impact of fractures on passage stability.
        
        Args:
            fracture_density: Fracture density (fractures/m)
            aperture: Average aperture (mm)
            
        Returns:
            Dictionary with stability impact assessment
        """
        # Stability score decreases with density and aperture
        base_score = 100
        density_penalty = min(50, fracture_density * 10)
        aperture_penalty = min(30, aperture / 2)
        
        stability_score = max(0, base_score - density_penalty - aperture_penalty)
        
        if stability_score >= 80:
            level = "STABLE"
            color = "🟢"
            description = "Minor fracturing, stable"
        elif stability_score >= 60:
            level = "CAUTION"
            color = "🟡"
            description = "Moderate fracturing, monitor"
        elif stability_score >= 40:
            level = "UNSTABLE"
            color = "🟠"
            description = "Significant fracturing, caution"
        elif stability_score >= 20:
            level = "HIGH RISK"
            color = "🔴"
            description = "Heavy fracturing, avoid"
        else:
            level = "CRITICAL"
            color = "⚫"
            description = "Intensely fractured, imminent collapse risk"
        
        return {
            'stability_score': round(stability_score, 1),
            'level': level,
            'color': color,
            'description': description,
            'density_penalty': round(density_penalty, 1),
            'aperture_penalty': round(aperture_penalty, 1)
        }

    def summary(self) -> Dict[str, Union[float, str, Dict]]:
        """Get summary of fracture analysis."""
        if not self.fractures:
            return {'status': 'No fracture data'}
        
        orientation = self.analyze_orientation()
        dip = self.analyze_dip()
        aperture = self.analyze_aperture()
        
        return {
            'n_fractures': len(self.fractures),
            'mean_orientation': orientation.get('mean_orientation', 0),
            'mean_dip': dip.get('mean_dip', 0),
            'mean_aperture': aperture.get('mean_aperture', 0),
            'dominant_sets': orientation.get('dominant_sets', []),
            'fracture_zones': self.identify_fracture_zones(),
            'predicted_passage': self.predict_passage_orientation()
        }


# Example usage
if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    
    print("="*60)
    print("CAVORA - Fracture Analysis")
    print("="*60)
    
    fa = FractureAnalysis()
    
    # Add fractures
    fa.add_fracture(45, 75, 5.2, 3.5, 2.1, "Main Passage", 2024.5)
    fa.add_fracture(50, 80, 4.8, 4.2, 1.8, "Main Passage", 2024.5)
    fa.add_fracture(135, 25, 3.1, 2.1, 3.2, "Side Passage", 2024.5)
    
    # Analyze orientation
    orientation = fa.analyze_orientation()
    print(f"\nOrientation Analysis:")
    print(f"  - Mean Orientation: {orientation['mean_orientation']}°")
    print(f"  - Dominant Sets: {orientation['dominant_sets']}")
    
    # Predict passage orientation
    prediction = fa.predict_passage_orientation()
    print(f"\nPredicted Passage Orientation:")
    print(f"  - Primary: {prediction['primary_orientation']}°")
    print(f"  - Secondary: {prediction['secondary_orientation']}°")
    print(f"  - {prediction['description']}")
    
    # Analyze stability impact
    stability = fa.analyze_stability_impact(fracture_density=2.5, aperture=3.5)
    print(f"\nStability Impact:")
    print(f"  - {stability['color']} {stability['level']}")
    print(f"  - Score: {stability['stability_score']}/100")
    print(f"  - {stability['description']}")
    
    print("\n" + "="*60)
