"""
Video and audio analysis tools for FMUS-VID.

This module provides functionality for scene detection, audio analysis,
and other analytical features.
"""

from typing import List, Dict, Any, Tuple, Optional, Union, Callable
import numpy as np
from PIL import Image
import librosa
import cv2
from scipy import signal
import logging
from pathlib import Path
from dataclasses import dataclass
import os

logger = logging.getLogger(__name__)

@dataclass
class SceneChange:
    """Represents a scene change in a video."""
    frame_number: int  # Frame number where the scene change occurs
    timestamp: float   # Timestamp in seconds
    score: float       # Confidence score (0-1)

class SceneDetector:
    """
    Detect scene changes in videos.

    This class uses computer vision techniques to identify scene boundaries
    in video content.
    """

    def __init__(self, threshold: float = 30.0, min_scene_length: int = 15):
        """
        Initialize scene detector.

        Args:
            threshold: Threshold for scene change detection (higher = fewer detections)
            min_scene_length: Minimum number of frames between scene changes
        """
        self.threshold = threshold
        self.min_scene_length = min_scene_length
        self._last_frame = None
        self._frame_count = 0
        self._last_scene = 0
        self._scenes = []

    def detect_from_video(self, video_path: Union[str, Path], fps: Optional[float] = None) -> List[SceneChange]:
        """
        Detect scenes from a video file.

        Args:
            video_path: Path to video file
            fps: Video frame rate (will be detected if not provided)

        Returns:
            List of SceneChange objects
        """
        cap = cv2.VideoCapture(str(video_path))
        if not cap.isOpened():
            raise ValueError(f"Could not open video: {video_path}")

        # Get video properties
        if fps is None:
            fps = cap.get(cv2.CAP_PROP_FPS)

        # Reset state
        self._reset()

        # Process each frame
        while True:
            ret, frame = cap.read()
            if not ret:
                break

            # Process frame
            self.process_frame(frame)

        # Clean up
        cap.release()

        # Convert frame numbers to timestamps
        for scene in self._scenes:
            scene.timestamp = scene.frame_number / fps

        return self._scenes

    def detect_from_video_object(self, video_object: Any) -> List[SceneChange]:
        """
        Detect scenes from a Video object.

        Args:
            video_object: Video object from fmusvid.core.Video

        Returns:
            List of SceneChange objects
        """
        video_path = video_object._video.get("path", "")
        if not video_path:
            raise ValueError("Video object must have a valid path")

        info = video_object.get_info()
        fps = info.get("fps", None)

        return self.detect_from_video(video_path, fps)

    def process_frame(self, frame: np.ndarray) -> bool:
        """
        Process a single frame for scene detection.

        Args:
            frame: Frame as numpy array (BGR format)

        Returns:
            True if a scene change was detected, False otherwise
        """
        # Convert to grayscale
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        # Skip first frame
        if self._last_frame is None:
            self._last_frame = gray
            self._frame_count += 1
            return False

        # Calculate difference
        score = self._calculate_difference(self._last_frame, gray)

        # Check for scene change
        is_scene_change = False
        if (score > self.threshold and
            self._frame_count - self._last_scene >= self.min_scene_length):

            # Add scene change
            self._scenes.append(SceneChange(
                frame_number=self._frame_count,
                timestamp=0.0,  # Will be calculated later
                score=score
            ))

            self._last_scene = self._frame_count
            is_scene_change = True

        # Update state
        self._last_frame = gray
        self._frame_count += 1

        return is_scene_change

    def _calculate_difference(self, frame1: np.ndarray, frame2: np.ndarray) -> float:
        """
        Calculate difference between two frames.

        Args:
            frame1: First frame (grayscale)
            frame2: Second frame (grayscale)

        Returns:
            Difference score
        """
        # Calculate absolute difference
        diff = cv2.absdiff(frame1, frame2)

        # Calculate mean difference
        mean_diff = np.mean(diff)

        return mean_diff

    def _reset(self):
        """Reset detector state."""
        self._last_frame = None
        self._frame_count = 0
        self._last_scene = 0
        self._scenes = []

class AudioAnalyzer:
    """Class for analyzing audio characteristics."""

    def __init__(self, sample_rate: int = 44100):
        """
        Initialize audio analyzer.

        Args:
            sample_rate: Audio sample rate in Hz
        """
        self.sample_rate = sample_rate

    def analyze_audio(self, audio_data: np.ndarray) -> Dict[str, Any]:
        """
        Analyze audio characteristics.

        Args:
            audio_data: Audio samples (numpy array)

        Returns:
            Dictionary with audio analysis results
        """
        # Convert to mono if stereo
        if len(audio_data.shape) > 1:
            audio_data = np.mean(audio_data, axis=1)

        # Calculate basic statistics
        rms = librosa.feature.rms(y=audio_data)[0]
        db = librosa.amplitude_to_db(rms)

        # Calculate spectral features
        spectral_centroids = librosa.feature.spectral_centroid(y=audio_data,
                                                             sr=self.sample_rate)[0]
        spectral_rolloff = librosa.feature.spectral_rolloff(y=audio_data,
                                                          sr=self.sample_rate)[0]

        # Calculate tempo
        tempo, _ = librosa.beat.beat_track(y=audio_data, sr=self.sample_rate)

        # Calculate pitch
        pitches, magnitudes = librosa.piptrack(y=audio_data, sr=self.sample_rate)
        pitch = np.mean(pitches[magnitudes > np.max(magnitudes)/2])

        return {
            "loudness": {
                "mean_db": float(np.mean(db)),
                "max_db": float(np.max(db)),
                "min_db": float(np.min(db))
            },
            "spectral": {
                "centroid_mean": float(np.mean(spectral_centroids)),
                "rolloff_mean": float(np.mean(spectral_rolloff))
            },
            "rhythm": {
                "tempo": float(tempo)
            },
            "pitch": {
                "mean": float(pitch) if not np.isnan(pitch) else None
            }
        }

    def analyze_video(self, video_object: Any) -> Dict[str, Any]:
        """
        Analyze audio from a Video object.

        Args:
            video_object: Video object from fmusvid.core.Video

        Returns:
            Dictionary with audio analysis results
        """
        import tempfile
        import subprocess

        video_path = video_object._video.get("path", "")
        if not video_path:
            raise ValueError("Video object must have a valid path")

        # Extract audio to temporary file
        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
            temp_path = temp_file.name

        try:
            # Extract audio using ffmpeg
            subprocess.run([
                "ffmpeg", "-i", video_path,
                "-q:a", "0", "-map", "a",
                "-c:a", "pcm_s16le", "-y",
                temp_path
            ], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

            # Load audio
            import librosa
            audio_data, sr = librosa.load(temp_path, sr=self.sample_rate)

            # Analyze audio
            return self.analyze_audio(audio_data)

        finally:
            # Clean up temp file
            try:
                import os
                os.remove(temp_path)
            except:
                pass

    def detect_silence(self, video_path: Union[str, Path],
                     threshold: float = 0.1, min_duration: float = 0.5) -> List[Tuple[float, float]]:
        """
        Detect silent segments in a video.

        Args:
            video_path: Path to video file
            threshold: Volume threshold for silence (0-1)
            min_duration: Minimum silence duration in seconds

        Returns:
            List of (start_time, end_time) tuples for silent segments
        """
        try:
            import librosa
            import soundfile as sf
        except ImportError:
            raise ImportError("Requires librosa and soundfile libraries: pip install librosa soundfile")

        # Extract audio using ffmpeg
        import subprocess
        import tempfile

        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
            temp_path = temp_file.name

        try:
            # Extract audio
            subprocess.run([
                "ffmpeg", "-i", str(video_path),
                "-q:a", "0", "-map", "a",
                "-c:a", "pcm_s16le", temp_path
            ], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

            # Load audio
            y, sr = librosa.load(temp_path, sr=None)

            # Calculate energy
            energy = librosa.feature.rms(y=y)[0]

            # Normalize energy to 0-1
            max_energy = np.max(energy) if np.max(energy) > 0 else 1
            energy_norm = energy / max_energy

            # Find silent segments
            silent = energy_norm < threshold

            # Convert to time segments
            frame_time = librosa.frames_to_time(np.arange(len(energy)), sr=sr)

            # Find silence boundaries
            silent_segments = []
            in_silence = False
            silence_start = 0

            for i, (is_silent, time) in enumerate(zip(silent, frame_time)):
                if is_silent and not in_silence:
                    # Start of silence
                    in_silence = True
                    silence_start = time
                elif not is_silent and in_silence:
                    # End of silence
                    in_silence = False
                    silence_duration = time - silence_start

                    if silence_duration >= min_duration:
                        silent_segments.append((silence_start, time))

            # Handle if ending in silence
            if in_silence:
                silence_duration = frame_time[-1] - silence_start
                if silence_duration >= min_duration:
                    silent_segments.append((silence_start, frame_time[-1]))

            return silent_segments

        finally:
            # Clean up temp file
            try:
                os.remove(temp_path)
            except:
                pass

        return []

    def detect_beats(self, audio_data: np.ndarray) -> List[float]:
        """
        Detect beat timestamps in audio.

        Args:
            audio_data: Audio samples

        Returns:
            List of beat timestamps in seconds
        """
        # Convert to mono if stereo
        if len(audio_data.shape) > 1:
            audio_data = np.mean(audio_data, axis=1)

        # Detect beats
        tempo, beat_frames = librosa.beat.beat_track(y=audio_data,
                                                   sr=self.sample_rate)

        # Convert beat frames to timestamps
        beat_times = librosa.frames_to_time(beat_frames, sr=self.sample_rate)

        return list(beat_times)

    def analyze_frequency_spectrum(self, audio_data: np.ndarray,
                                 window_size: float = 0.1) -> Dict[str, Any]:
        """
        Analyze frequency spectrum over time.

        Args:
            audio_data: Audio samples
            window_size: Analysis window size in seconds

        Returns:
            Dictionary with frequency analysis results
        """
        # Convert to mono if stereo
        if len(audio_data.shape) > 1:
            audio_data = np.mean(audio_data, axis=1)

        # Calculate STFT
        n_fft = int(self.sample_rate * window_size)
        hop_length = n_fft // 4

        stft = librosa.stft(audio_data, n_fft=n_fft, hop_length=hop_length)
        spectrogram = np.abs(stft)

        # Calculate frequency bands
        freqs = librosa.fft_frequencies(sr=self.sample_rate, n_fft=n_fft)
        times = librosa.times_like(spectrogram, sr=self.sample_rate,
                                 hop_length=hop_length)

        # Define frequency bands
        bands = {
            "sub_bass": (20, 60),
            "bass": (60, 250),
            "low_mid": (250, 500),
            "mid": (500, 2000),
            "high_mid": (2000, 4000),
            "high": (4000, 20000)
        }

        # Analyze each frequency band
        band_energies = {}
        for band_name, (low_freq, high_freq) in bands.items():
            # Find frequency indices for the band
            band_mask = (freqs >= low_freq) & (freqs <= high_freq)
            band_spec = spectrogram[band_mask]

            # Calculate band energy over time
            band_energy = np.sum(band_spec, axis=0)

            band_energies[band_name] = {
                "mean": float(np.mean(band_energy)),
                "max": float(np.max(band_energy)),
                "std": float(np.std(band_energy))
            }

        return {
            "frequency_bands": band_energies,
            "analysis_params": {
                "window_size": window_size,
                "n_fft": n_fft,
                "hop_length": hop_length
            }
        }

class ContentAnalyzer:
    """Class for analyzing video content characteristics."""

    def __init__(self):
        """Initialize content analyzer."""
        self._brightness_history = []
        self._motion_history = []
        self._color_history = []

    def analyze_frame(self, frame: Image.Image, time: float) -> Dict[str, Any]:
        """
        Analyze frame characteristics.

        Args:
            frame: PIL Image frame
            time: Frame timestamp in seconds

        Returns:
            Dictionary with frame analysis results
        """
        # Convert frame to numpy array
        frame_array = np.array(frame)

        # Analyze brightness
        brightness = np.mean(frame_array) / 255.0
        self._brightness_history.append((time, brightness))

        # Analyze color distribution
        if len(frame_array.shape) == 3:
            colors = {
                "red": np.mean(frame_array[:, :, 0]) / 255.0,
                "green": np.mean(frame_array[:, :, 1]) / 255.0,
                "blue": np.mean(frame_array[:, :, 2]) / 255.0
            }
            self._color_history.append((time, colors))
        else:
            colors = None

        # Analyze motion (if previous frame exists)
        motion_score = None
        if len(self._motion_history) > 0:
            prev_frame = np.array(self._motion_history[-1][1])

            if prev_frame.shape == frame_array.shape:
                diff = cv2.absdiff(prev_frame, frame_array)
                motion_score = np.mean(diff) / 255.0

        self._motion_history.append((time, frame_array))

        # Keep history length reasonable
        max_history = 300  # 10 seconds at 30 fps
        if len(self._motion_history) > max_history:
            self._motion_history.pop(0)
        if len(self._brightness_history) > max_history:
            self._brightness_history.pop(0)
        if len(self._color_history) > max_history:
            self._color_history.pop(0)

        return {
            "brightness": brightness,
            "colors": colors,
            "motion": motion_score
        }

    def get_summary(self) -> Dict[str, Any]:
        """
        Get summary of content analysis.

        Returns:
            Dictionary with analysis summary
        """
        brightness_values = [b for _, b in self._brightness_history]

        summary = {
            "brightness": {
                "mean": float(np.mean(brightness_values)),
                "std": float(np.std(brightness_values)),
                "min": float(np.min(brightness_values)),
                "max": float(np.max(brightness_values))
            }
        }

        # Analyze color trends if available
        if self._color_history:
            color_data = {
                "red": [c["red"] for _, c in self._color_history],
                "green": [c["green"] for _, c in self._color_history],
                "blue": [c["blue"] for _, c in self._color_history]
            }

            summary["colors"] = {
                color: {
                    "mean": float(np.mean(values)),
                    "std": float(np.std(values)),
                    "min": float(np.min(values)),
                    "max": float(np.max(values))
                }
                for color, values in color_data.items()
            }

        # Analyze motion if available
        motion_scores = [m for m in [m[1] for _, m in self._motion_history] if m is not None]
        if motion_scores:
            summary["motion"] = {
                "mean": float(np.mean(motion_scores)),
                "std": float(np.std(motion_scores)),
                "min": float(np.min(motion_scores)),
                "max": float(np.max(motion_scores))
            }

        return summary

    def reset(self):
        """Reset the analyzer state."""
        self._brightness_history = []
        self._motion_history = []
        self._color_history = []
