"""
Base classes for pose extraction.
"""

import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import numpy as np

from sltk.data.core import PoseSequence

logger = logging.getLogger(__name__)


@dataclass
class ExtractionConfig:
    """Base configuration for extraction."""

    device: str = "cuda:0"
    batch_size: int = 32
    confidence_threshold: float = 0.5
    output_format: str = "h5"
    compress: bool = True
    verbose: bool = True
    extra: dict[str, Any] = field(default_factory=dict)


@dataclass
class ExtractionResult:
    """Result from pose extraction."""

    poses: PoseSequence | None = None
    metadata: dict[str, Any] = field(default_factory=dict)
    num_frames: int = 0
    num_detections: int = 0
    fps: float = 25.0
    errors: list[str] = field(default_factory=list)

    @property
    def success(self) -> bool:
        return self.poses is not None and len(self.errors) == 0


class PoseExtractor(ABC):
    """
    Abstract base class for pose extraction.

    All extractors should inherit from this class and implement
    the required methods for consistent API.
    """

    def __init__(self, config: ExtractionConfig | None = None):
        """
        Initialize the extractor.

        Args:
            config: Extraction configuration
        """
        self.config = config or ExtractionConfig()
        self._model = None
        self._initialized = False

    def __del__(self):
        """Destructor to ensure resources are released."""
        try:
            self.close()
        except Exception:
            pass

    def close(self) -> None:
        """Release resources. Override in subclasses for specific cleanup."""
        self._model = None
        self._initialized = False

    @property
    @abstractmethod
    def name(self) -> str:
        """Extractor name."""
        pass

    @property
    @abstractmethod
    def output_format(self) -> str:
        """Output pose format (e.g., 'mediapipe', 'wilor', 'smplx')."""
        pass

    @property
    def is_initialized(self) -> bool:
        """Check if model is loaded."""
        return self._initialized

    @abstractmethod
    def load_model(self, **kwargs) -> None:
        """Load the extraction model."""
        pass

    @abstractmethod
    def extract_from_video(
        self,
        video_path: str | Path,
        output_path: str | Path | None = None,
    ) -> ExtractionResult:
        """
        Extract poses from a video file.

        Args:
            video_path: Path to input video
            output_path: Optional path for saving results

        Returns:
            ExtractionResult with poses and metadata
        """
        pass

    @abstractmethod
    def extract_from_frames(
        self,
        frames: np.ndarray,
        fps: float = 25.0,
    ) -> ExtractionResult:
        """
        Extract poses from numpy frames.

        Args:
            frames: (T, H, W, 3) uint8 RGB frames
            fps: Frame rate

        Returns:
            ExtractionResult with poses and metadata
        """
        pass

    def save_results(
        self,
        result: ExtractionResult,
        output_path: str | Path,
    ) -> Path:
        """
        Save extraction results to file.

        Args:
            result: ExtractionResult to save
            output_path: Output file path

        Returns:
            Path to saved file
        """
        from sltk.io.h5 import save_poses_h5

        output_path = Path(output_path)

        if result.poses is not None:
            save_poses_h5(
                result.poses,
                output_path,
                metadata=result.metadata,
                compress=self.config.compress,
            )

        return output_path

    def process_batch(
        self,
        video_paths: list[str | Path],
        output_dir: str | Path,
        skip_existing: bool = True,
    ) -> dict[str, ExtractionResult]:
        """
        Process multiple videos.

        Args:
            video_paths: List of video paths
            output_dir: Output directory
            skip_existing: Skip if output exists

        Returns:
            Dictionary mapping video path to result
        """
        output_dir = Path(output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)

        results = {}
        for video_path in video_paths:
            video_path = Path(video_path)
            output_path = output_dir / f"{video_path.stem}.h5"

            if skip_existing and output_path.exists():
                if self.config.verbose:
                    print(f"Skipping {video_path.name} (exists)")
                continue

            if self.config.verbose:
                print(f"Processing {video_path.name}...")

            try:
                result = self.extract_from_video(video_path, output_path)
                results[str(video_path)] = result
            except FileNotFoundError as e:
                error_msg = f"Video file not found: {e}"
                logger.error(error_msg)
                results[str(video_path)] = ExtractionResult(errors=[error_msg])
                if self.config.verbose:
                    print(f"  Error: {error_msg}")
            except PermissionError as e:
                error_msg = f"Permission denied accessing video: {e}"
                logger.error(error_msg)
                results[str(video_path)] = ExtractionResult(errors=[error_msg])
                if self.config.verbose:
                    print(f"  Error: {error_msg}")
            except OSError as e:
                error_msg = f"IO error processing video: {e}"
                logger.error(error_msg)
                results[str(video_path)] = ExtractionResult(errors=[error_msg])
                if self.config.verbose:
                    print(f"  Error: {error_msg}")
            except ValueError as e:
                error_msg = f"Invalid video data: {e}"
                logger.error(error_msg)
                results[str(video_path)] = ExtractionResult(errors=[error_msg])
                if self.config.verbose:
                    print(f"  Error: {error_msg}")
            except RuntimeError as e:
                error_msg = f"Runtime error during extraction: {e}"
                logger.error(error_msg)
                results[str(video_path)] = ExtractionResult(errors=[error_msg])
                if self.config.verbose:
                    print(f"  Error: {error_msg}")
            except Exception as e:
                # Catch any other unexpected errors, log with full traceback
                error_msg = f"Unexpected error processing {video_path.name}: {e}"
                logger.exception(error_msg)
                results[str(video_path)] = ExtractionResult(errors=[error_msg])
                if self.config.verbose:
                    print(f"  Error: {error_msg}")

        return results


def get_video_info(video_path: str | Path) -> dict[str, Any]:
    """
    Get video metadata.

    Args:
        video_path: Path to video file

    Returns:
        Dictionary with fps, num_frames, width, height
    """
    try:
        import cv2
    except ImportError:
        raise ImportError("opencv-python required: pip install opencv-python")

    cap = cv2.VideoCapture(str(video_path))
    try:
        info = {
            "fps": cap.get(cv2.CAP_PROP_FPS),
            "num_frames": int(cap.get(cv2.CAP_PROP_FRAME_COUNT)),
            "width": int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
            "height": int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
        }
        return info
    finally:
        cap.release()


def read_video_frames(
    video_path: str | Path,
    start_frame: int = 0,
    end_frame: int | None = None,
    step: int = 1,
) -> np.ndarray:
    """
    Read video frames as numpy array.

    Args:
        video_path: Path to video file
        start_frame: Starting frame index
        end_frame: Ending frame index (exclusive)
        step: Frame step

    Returns:
        (T, H, W, 3) uint8 RGB array
    """
    try:
        import cv2
    except ImportError:
        raise ImportError("opencv-python required: pip install opencv-python")

    cap = cv2.VideoCapture(str(video_path))

    if end_frame is None:
        end_frame = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

    frames = []
    cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)

    for i in range(start_frame, end_frame, step):
        ret, frame = cap.read()
        if not ret:
            break
        # Convert BGR to RGB
        frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))

    cap.release()
    return np.array(frames)
