import argparse
import json
import logging
import os
import queue
import random
import tempfile
import threading
import traceback
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple

import boto3
import cv2
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from tqdm import tqdm

from badas.training.utils import download_s3_file, get_frames_ffmpeg, get_video_duration, get_video_fps
from badas.core.preprocessing import resize_frame


@dataclass
class Config:
    """Configuration class for video processing parameters"""

    # Input/Output paths
    video_dir: str = "../nexar-competition/train"
    csv_path: str = "../nexar-competition/train.csv"
    output_dir: str = "balanced_dataset"
    use_temp_dir: bool = False
    restart: bool = False

    # Video processing parameters
    target_fps: int = 8
    window_size: int = 16  # frames per window (2 seconds at 8fps)
    window_stride: int = 2  # frames to skip between windows
    img_size: Tuple[int, int] = (256, 256)

    # Dataset splitting
    split: str = None
    test_size: float = None
    seed: int = 42

    # Video encoding
    video_format: str = "mp4"
    codec: str = "H264"  # H.264 for macOS/QuickTime compatibility
    use_ffmpeg_encoding: bool = True  # Use ffmpeg for reliable H.264 encoding

    # Performance settings
    num_threads: Optional[int] = None
    max_videos: Optional[int] = None
    buffer_size: int = 48  # windows to keep in memory

    # Time-based labeling (for positive samples)
    positive_window_before_event: float = (
        1.5  # seconds before event to mark as positive
    )

    # Balanced dataset parameters
    max_negative_examples_per_video: int = (
        6  # Maximum negative examples to extract per video
    )

    # Extract negatives from positive videos
    extract_negatives_from_positive: bool = False  # Extract early clips as negatives
    negative_buffer_before_positive: float = (
        1.5  # seconds gap between negative clips and positive window
    )
    max_negatives_from_positive_video: int = (
        3  # Maximum negative clips to extract from each positive video
    )

    balance_dataset: bool = False
    use_csv_splits: bool = False  # Use 'split' column from CSV instead of config

    # Incremental metadata writing
    clips_metadata_full_path: Optional[str] = None  # Path to clips_metadata_full.csv
    metadata_flush_interval: int = 100  # Flush metadata to disk every N clips

    # Classification mode settings (extract clips containing the event itself)
    classification_mode: bool = False  # Enable classification extraction mode
    classification_event_stride: int = 2  # Frame stride for event position in clips
    classification_pre_event_buffer: float = 3.0  # Seconds before event for pre-event clips
    classification_normal_clips: int = 2  # Number of random clips from normal_driving videos
    classification_skip_event_types: Tuple[str, ...] = ("hard_brake_unverified", "other")

    def __post_init__(self):
        if self.num_threads is None:
            self.num_threads = min(os.cpu_count() * 2, 16)

        if self.use_csv_splits:
            # Will use splits from CSV - don't set default
            self.split = None
        elif self.test_size is None and self.split is None:
            self.split = "data"
        elif self.test_size is not None and self.split is not None:
            raise ValueError("Specify either test_size or split, not both.")

    def to_dict(self):
        def is_json_serializable(v):
            try:
                json.dumps(v)
                return True
            except (TypeError, OverflowError):
                return False

        return {k: v for k, v in self.__dict__.items() if is_json_serializable(v)}

    @property
    def splits(self):
        if self.test_size is not None:
            return ("train", "val")
        return (self.split,) if self.split else ("data",)


class VideoProcessor:
    def __init__(self, config: Config):
        self.config = config
        self.setup_logging()
        self.df = pd.read_csv(config.csv_path)

        if "s3_path" in self.df.columns and self.df["s3_path"].iloc[0].startswith(
            "s3://"
        ):
            # verify sso
            boto3.client("sts").get_caller_identity()

        if config.max_videos:
            self.df = self.df.head(config.max_videos)

        self._init_directories()
        self.metadata_queue = queue.Queue()
        self.lock = threading.Lock()
        self.writer_cache = {}

        # Detect available H.264 encoder (cached for performance)
        self._h264_encoder = self._detect_h264_encoder()

        # Incremental metadata writing
        self._metadata_buffer = []
        self._metadata_buffer_lock = threading.Lock()
        self._clips_written = 0
        self._init_incremental_metadata()

        # Handle restart logic
        if self.config.restart:
            self._filter_processed_videos()

    def _init_incremental_metadata(self):
        """Initialize incremental metadata CSV writing."""
        if not self.config.clips_metadata_full_path:
            # Default path if not specified
            self.config.clips_metadata_full_path = os.path.join(
                self.config.output_dir, "clips_metadata_full.csv"
            )

        self._metadata_csv_path = self.config.clips_metadata_full_path

        # Field names differ slightly between modes
        if self.config.classification_mode:
            self._metadata_fieldnames = [
                "path", "label", "split", "source",
                "source_event_time", "source_event_type",
                "start_time", "end_time", "duration", "num_frames",
                "extraction_type", "event_frame_index"
            ]
        else:
            self._metadata_fieldnames = [
                "path", "label", "split", "source", "window_index",
                "source_label", "source_event_time", "source_event_type",
                "start_time", "end_time", "duration", "num_frames", "extraction_type"
            ]

        # Check if file exists and has content
        self._metadata_file_exists = (
            os.path.exists(self._metadata_csv_path) and
            os.path.getsize(self._metadata_csv_path) > 0
        )

        if not self._metadata_file_exists:
            # Create new file with header
            with open(self._metadata_csv_path, 'w', newline='') as f:
                import csv
                writer = csv.DictWriter(f, fieldnames=self._metadata_fieldnames)
                writer.writeheader()
            self.logger.info(f"Created new metadata file: {self._metadata_csv_path}")
        else:
            self.logger.info(f"Will append to existing metadata file: {self._metadata_csv_path}")

    def _flush_metadata_to_csv(self, force: bool = False):
        """Flush buffered metadata to CSV file.

        Args:
            force: If True, flush regardless of buffer size
        """
        with self._metadata_buffer_lock:
            if not self._metadata_buffer:
                return

            if not force and len(self._metadata_buffer) < self.config.metadata_flush_interval:
                return

            # Write buffered metadata to CSV
            import csv
            with open(self._metadata_csv_path, 'a', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=self._metadata_fieldnames)
                for item in self._metadata_buffer:
                    # Extract only the fields we need
                    row = {k: item.get(k, '') for k in self._metadata_fieldnames}
                    writer.writerow(row)

            count = len(self._metadata_buffer)
            self._clips_written += count
            self._metadata_buffer.clear()
            self.logger.info(f"Flushed {count} clips to CSV (total: {self._clips_written})")

    def _add_metadata_incremental(self, metadata: dict):
        """Add metadata to buffer and flush if needed."""
        with self._metadata_buffer_lock:
            self._metadata_buffer.append(metadata)

        # Check if we should flush (outside lock to avoid holding it during I/O)
        if len(self._metadata_buffer) >= self.config.metadata_flush_interval:
            self._flush_metadata_to_csv()

    def _filter_processed_videos(self):
        """Filter out already processed videos when restarting"""
        metadata_path = os.path.join(self.config.output_dir, "metadata.csv")

        if not os.path.exists(metadata_path):
            raise FileNotFoundError(
                f"Cannot restart: metadata.csv not found at {metadata_path}. "
                "Please ensure you're restarting from a valid output directory with existing processed data."
            )

        self.logger.info(f"Reading existing metadata from {metadata_path}")
        existing_metadata = pd.read_csv(metadata_path)

        if "source" not in existing_metadata.columns:
            raise ValueError("metadata.csv must contain 'source' column with video IDs")

        # Get already processed video IDs
        processed_video_ids = set(existing_metadata["source"].unique())
        self.logger.info(f"Found {len(processed_video_ids)} already processed videos")

        # Filter input dataframe to exclude already processed videos
        original_count = len(self.df)
        self.df = self.df[~self.df["id"].isin(processed_video_ids)]
        remaining_count = len(self.df)

        filtered_count = original_count - remaining_count
        self.logger.info(f"Filtered out {filtered_count} already processed videos")
        self.logger.info(f"Remaining videos to process: {remaining_count}")

        if remaining_count == 0:
            self.logger.info("All videos have already been processed!")

    def setup_logging(self):
        """Setup logging configuration"""
        logging.basicConfig(
            level=logging.WARNING, format="%(asctime)s - %(levelname)s - %(message)s"
        )
        self.logger = logging.getLogger(__name__)

    def _detect_h264_encoder(self) -> tuple:
        """Detect available H.264 encoder. Returns (encoder_name, extra_args)."""
        import subprocess
        result = subprocess.run(
            ['ffmpeg', '-encoders'],
            capture_output=True, text=True
        )
        if 'libx264' in result.stdout:
            return ('libx264', ['-preset', 'fast', '-crf', '18'])
        elif 'libopenh264' in result.stdout:
            return ('libopenh264', [])
        else:
            self.logger.warning("No H.264 encoder found, will use OpenCV fallback")
            return (None, [])

    def _init_directories(self):
        """Initialize output directories and clean existing files (unless restarting)"""
        self.logger.info(f"Initializing output directories in {self.config.output_dir}")

        # Flat structure: single 'clips' folder, labels managed via CSV
        clips_path = os.path.join(self.config.output_dir, "clips")
        os.makedirs(clips_path, exist_ok=True)

        # Check if directory has existing files
        has_existing = any(
            os.path.isfile(os.path.join(clips_path, f))
            for f in os.listdir(clips_path)
        )

        if has_existing and not self.config.restart:
            # In non-interactive mode, just preserve existing files
            import sys
            if not sys.stdin.isatty():
                self.logger.info("Non-interactive mode: preserving existing files, adding new clips")
            else:
                # Interactive mode - ask for confirmation
                should_delete = input(
                    f"Output directory has existing files. Delete them? (y/n): "
                )
                if should_delete.lower() == "y":
                    for f in os.listdir(clips_path):
                        file_path = os.path.join(clips_path, f)
                        if os.path.isfile(file_path):
                            os.remove(file_path)
                    self.logger.info("Deleted existing files")
                else:
                    self.logger.info("Preserving existing files, adding new clips")
        elif self.config.restart:
            self.logger.info("Restart mode: preserving existing processed files")

        config_path = os.path.join(self.config.output_dir, "dataset_config.json")
        with open(config_path, "w") as f:
            json.dump(self.config.to_dict(), f, indent=4)

    def _extract_frames(self, video_path: str, start_time=None, duration=None):
        """Extract frames from video using float accumulation sampling.

        Uses the SAME code as inference for consistency:
        - Float accumulation for frame selection (same as MultiBufferPreprocessor)
        - resize_frame() from core/preprocessing (same as VideoPreprocessor)
        """
        # Get source video fps for float accumulation sampling
        source_fps = get_video_fps(video_path)
        target_fps = self.config.target_fps

        # Extract frames at native resolution - we'll resize with shared code
        frame_gen = get_frames_ffmpeg(
            video_path,
            size=None,  # Native resolution
            fps=None,   # Native fps - we sample ourselves
            start_time=start_time,
            duration=duration,
        )

        # Float accumulation sampling (same algorithm as inference)
        sample_interval = source_fps / target_fps
        next_sample_position = 0.0
        start_time = start_time or 0.0
        sampled_count = 0

        # Use first dimension for square resize (same as inference)
        img_size = self.config.img_size[0] if isinstance(self.config.img_size, tuple) else self.config.img_size

        try:
            for frame_index, frame in enumerate(frame_gen):
                # Sample this frame if we've reached the next sample position
                if frame_index >= int(next_sample_position):
                    # Use shared resize_frame from core/preprocessing (same as inference)
                    frame = resize_frame(frame, img_size)
                    timestamp = start_time + sampled_count / target_fps
                    yield frame, timestamp
                    next_sample_position += sample_interval
                    sampled_count += 1
        finally:
            frame_gen.close()

    def _extract_window_frames(
        self, video_path: str, start_time: float, num_frames: int
    ) -> List[np.ndarray]:
        """Extract a window of frames using float accumulation sampling.

        Uses the SAME code as inference for consistency:
        - Float accumulation for frame selection (same as MultiBufferPreprocessor)
        - resize_frame() from core/preprocessing (same as VideoPreprocessor)

        Args:
            video_path: Path to the video file
            start_time: Start time in seconds
            num_frames: Number of frames to extract

        Returns:
            List of frames (may be fewer than num_frames if video is too short)
        """
        source_fps = get_video_fps(video_path)
        target_fps = self.config.target_fps

        # Calculate duration needed (with extra margin)
        duration_needed = (num_frames / target_fps) + 1.0

        # Extract frames at native resolution
        frame_gen = get_frames_ffmpeg(
            video_path,
            size=None,  # Native resolution
            fps=None,   # Native fps
            start_time=start_time,
            duration=duration_needed,
        )

        # Float accumulation sampling (same algorithm as inference)
        sample_interval = source_fps / target_fps
        next_sample_position = 0.0
        sampled_frames = []

        # Use first dimension for square resize (same as inference)
        img_size = self.config.img_size[0] if isinstance(self.config.img_size, tuple) else self.config.img_size

        try:
            for frame_index, frame in enumerate(frame_gen):
                if frame_index >= int(next_sample_position):
                    # Use shared resize_frame from core/preprocessing (same as inference)
                    frame = resize_frame(frame, img_size)
                    sampled_frames.append(frame)
                    next_sample_position += sample_interval
                    if len(sampled_frames) >= num_frames:
                        break
        finally:
            frame_gen.close()

        return sampled_frames

    def _get_video_writer(self, save_path: str) -> Tuple[cv2.VideoWriter, bool]:
        """Get or create video writer with fallback codecs"""
        cache_key = os.path.abspath(save_path)

        if cache_key in self.writer_cache:
            return self.writer_cache[cache_key], True

        # Try primary codec
        fourcc = cv2.VideoWriter_fourcc(*self.config.codec)
        writer = cv2.VideoWriter(
            save_path, fourcc, self.config.target_fps, self.config.img_size
        )

        # Try backup codecs if primary fails
        if not writer.isOpened():
            # check that path directory exists
            if not os.path.exists(os.path.dirname(save_path)):
                raise FileNotFoundError(
                    f"Directory does not exist: {os.path.dirname(save_path)}"
                )

            backup_codecs = ["MJPG", "DIVX", "MPEG", "X264"]
            for codec in backup_codecs:
                fourcc = cv2.VideoWriter_fourcc(*codec)
                writer = cv2.VideoWriter(
                    save_path, fourcc, self.config.target_fps, self.config.img_size
                )
                if writer.isOpened():
                    self.logger.info(f"Using backup codec {codec} for {save_path}")
                    break

        # Last resort: uncompressed
        if not writer.isOpened():
            writer = cv2.VideoWriter(
                save_path, 0, self.config.target_fps, self.config.img_size
            )
            self.logger.warning(f"Using uncompressed format for {save_path}")

        self.writer_cache[cache_key] = writer
        return writer, False

    def _release_writer(self, save_path: str):
        """Release video writer for specified path"""
        cache_key = os.path.abspath(save_path)
        if cache_key in self.writer_cache:
            self.writer_cache[cache_key].release()
            del self.writer_cache[cache_key]

    def _save_video_window(self, frames: List[np.ndarray], save_path: str) -> bool:
        """Save video window using ffmpeg (H.264) or OpenCV VideoWriter."""
        try:
            if self.config.use_ffmpeg_encoding:
                return self._save_video_ffmpeg(frames, save_path)
            else:
                return self._save_video_opencv(frames, save_path)
        except KeyboardInterrupt:
            raise
        except Exception as e:
            self.logger.error(f"Error saving video {save_path}: {str(e)}")
            return False

    def _save_video_ffmpeg(self, frames: List[np.ndarray], save_path: str) -> bool:
        """Save video using ffmpeg with H.264 codec (macOS/QuickTime compatible)."""
        import subprocess

        encoder, extra_args = self._h264_encoder
        if encoder is None:
            # Fallback to OpenCV if no H.264 encoder
            return self._save_video_opencv(frames, save_path)

        h, w = frames[0].shape[:2]

        cmd = [
            'ffmpeg', '-y',
            '-f', 'rawvideo',
            '-vcodec', 'rawvideo',
            '-s', f'{w}x{h}',
            '-pix_fmt', 'rgb24',
            '-r', str(self.config.target_fps),
            '-i', '-',
            '-c:v', encoder,
            '-pix_fmt', 'yuv420p',  # Required for QuickTime
            *extra_args,
            '-loglevel', 'error',
            save_path
        ]

        proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
        for frame in frames:
            proc.stdin.write(frame.tobytes())
        proc.stdin.close()
        proc.wait()

        return proc.returncode == 0

    def _save_video_opencv(self, frames: List[np.ndarray], save_path: str) -> bool:
        """Save video using OpenCV VideoWriter (fallback)."""
        writer, _ = self._get_video_writer(save_path)

        for frame in frames:
            frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
            writer.write(frame)

        self._release_writer(save_path)
        return True

    def _process_buffer(self, buffer: List[Dict[str, Any]]) -> int:
        """Process buffered window data"""
        processed_count = 0

        for item in buffer:
            save_success = self._save_video_window(item["frames"], item["save_path"])

            if save_success:
                self.metadata_queue.put(item["metadata"])
                # Also add to incremental CSV writer
                self._add_metadata_incremental(item["metadata"])
                processed_count += 1

        return processed_count

    def _determine_label(
        self,
        window_end_time: float,
        event_time: float,
        is_positive_video: bool,
    ) -> int:
        """Determine label for a video window based on timing"""
        if not is_positive_video:
            return 0

        if window_end_time >= event_time:
            return -1  # Signal to stop processing this video

        # Mark as positive if window ends within specified time before event
        if (
            (event_time - self.config.positive_window_before_event)
            <= window_end_time
            < event_time
        ):
            return 1
        return 0

    def process_video(self, row: pd.Series):
        video_id = row["id"]
        split = row["split"]
        event_time = row["time_of_event"]
        event_type = row.get("event_type", None)  # Optional field
        is_positive = row["target"] == 1
        try:
            if "s3_path" in row:
                s3_path = row["s3_path"]
                # Check if s3_path is actually a local path
                if os.path.isabs(s3_path) and not s3_path.startswith("s3://"):
                    # It's a local absolute path, use directly
                    local_path = s3_path
                    if not os.path.exists(local_path):
                        self.logger.warning(f"Local video file not found: {local_path}")
                        return
                    self._process_video(
                        video_id,
                        local_path,
                        is_positive,
                        event_time,
                        split=split,
                        event_type=event_type,
                    )
                else:
                    # It's an S3 path, download it
                    basename = os.path.basename(s3_path)
                    local_path = os.path.join(self.config.video_dir, basename)
                    exists = os.path.exists(local_path)
                    if exists or not self.config.use_temp_dir:
                        if not exists:
                            download_s3_file(s3_path, local_path)
                        self._process_video(
                            video_id,
                            local_path,
                            is_positive,
                            event_time,
                            split=split,
                            event_type=event_type,
                        )
                    else:
                        with tempfile.NamedTemporaryFile(suffix=".mp4") as tmp_file:
                            download_s3_file(s3_path, tmp_file.name)
                            self._process_video(
                                video_id,
                                tmp_file.name,
                                is_positive,
                                event_time,
                                split=split,
                                event_type=event_type,
                            )
            else:
                local_path = os.path.join(self.config.video_dir, f"{video_id:05}.mp4")
                self._process_video(
                    video_id,
                    local_path,
                    is_positive,
                    event_time,
                    split=split,
                    event_type=event_type,
                )
        except KeyboardInterrupt:
            raise
        except Exception as e:
            self.logger.error(
                f"Failed to process {video_id}: {str(e)}" f"\n{traceback.format_exc()}"
            )

    def _process_video(
        self,
        video_id: str,
        filepath: str,
        is_positive: bool,
        event_time: float,
        split: str,
        event_type: str = None,
    ):
        """Process a single video file with balanced extraction"""
        if not os.path.exists(filepath):
            self.logger.warning(f"Video file not found: {filepath}")
            return

        # Classification mode: use separate processing pipeline
        if self.config.classification_mode:
            window_count = self._process_video_classification(
                video_id, filepath, event_time, split, event_type
            )
        elif is_positive:
            window_count = self._process_positive_video(
                video_id, filepath, event_time, split, event_type
            )
        else:
            window_count = self._process_negative_video(
                video_id, filepath, event_time, split, event_type
            )

        self.logger.info(f"Processed {video_id}: generated {window_count} windows")

    def _process_positive_video(
        self, video_id: str, filepath: str, event_time: float, split: str,
        event_type: str = None
    ):
        """Process positive video: extract positive windows before event.

        Optionally also extracts negative clips from early part of the video
        (before the dangerous situation develops) when extract_negatives_from_positive=True.
        """
        window_length = self.config.window_size / self.config.target_fps
        window_count = 0

        # Timeline:
        # [---early negatives---][buffer][---positive window---][EVENT]
        #                        ^       ^                      ^
        #                        |       |                      event_time
        #                        |       earliest_positive_time
        #                        negative_cutoff_time

        earliest_positive_time = max(
            0.0, event_time - self.config.positive_window_before_event - window_length
        )

        # Extract negatives from early part of positive video if enabled
        if self.config.extract_negatives_from_positive:
            negative_count = self._extract_negatives_from_positive_video(
                video_id, filepath, earliest_positive_time, split, event_time, event_type
            )
            window_count += negative_count

        # Extract positive windows (original logic)
        duration = event_time + 1 - earliest_positive_time

        frame_gen = self._extract_frames(
            filepath, start_time=earliest_positive_time, duration=duration
        )
        try:
            frames, timestamps = map(list, zip(*frame_gen)) if frame_gen else ([], [])
        finally:
            frame_gen.close()

        if len(frames) < self.config.window_size:
            self.logger.warning(f"Not enough frames in positive video {video_id}")
            return window_count

        buffer = []

        # Extract all possible positive windows with stride
        i = 0
        while i + self.config.window_size <= len(frames):
            window_frames = frames[i : i + self.config.window_size]
            window_times = timestamps[i : i + self.config.window_size]
            last_time = window_times[-1]

            label = self._determine_label(last_time, event_time, True)
            if label == -1:
                break
            elif label == 0:
                i += self.config.window_stride
                continue

            output = self._save_video(
                "positive",
                video_id,
                label,
                split,
                i,
                window_frames,
                window_times,
                last_time,
                extraction_type="positive_event",
                source_label=1,
                source_event_time=event_time,
                source_event_type=event_type,
            )
            buffer.append(output)

            if len(buffer) >= self.config.buffer_size:
                window_count += self._process_buffer(buffer)
                buffer = []

            i += self.config.window_stride

        # Process remaining buffer
        if buffer:
            window_count += self._process_buffer(buffer)

        return window_count

    def _extract_negatives_from_positive_video(
        self, video_id: str, filepath: str, earliest_positive_time: float, split: str,
        source_event_time: float = None, source_event_type: str = None
    ) -> int:
        """Extract negative clips from early part of a positive video.

        These are clips that occur well before the dangerous event,
        representing normal driving conditions.

        Args:
            video_id: Video identifier
            filepath: Path to video file
            earliest_positive_time: Time when positive windows start
            split: Dataset split (train/val)
            source_event_time: Event time from source positive video
            source_event_type: Event type from source video

        Returns:
            Number of negative clips extracted
        """
        window_length = self.config.window_size / self.config.target_fps

        # Cutoff time: must end before (earliest_positive_time - buffer)
        negative_cutoff_time = earliest_positive_time - self.config.negative_buffer_before_positive

        # Need at least one full window before cutoff
        if negative_cutoff_time < window_length:
            self.logger.debug(f"Not enough time for negatives in positive video {video_id}")
            return 0

        # Extract frames from start of video up to cutoff
        frame_gen = self._extract_frames(
            filepath, start_time=0.0, duration=negative_cutoff_time + 0.5
        )
        try:
            result = list(zip(*frame_gen)) if frame_gen else ([], [])
            frames, timestamps = (list(result[0]), list(result[1])) if result else ([], [])
        finally:
            frame_gen.close()

        if len(frames) < self.config.window_size:
            return 0

        # Find all valid window start positions
        valid_starts = []
        for i in range(0, len(frames) - self.config.window_size + 1, self.config.window_stride):
            window_end_time = timestamps[i + self.config.window_size - 1]
            if window_end_time <= negative_cutoff_time:
                valid_starts.append(i)

        if not valid_starts:
            return 0

        # Randomly sample windows
        max_samples = min(
            len(valid_starts),
            self.config.max_negatives_from_positive_video
        )

        random.seed(self.config.seed + hash(video_id) % 10000)
        selected_starts = random.sample(valid_starts, max_samples)
        selected_starts.sort()

        buffer = []
        window_count = 0

        for start_idx in selected_starts:
            window_frames = frames[start_idx : start_idx + self.config.window_size]
            window_times = timestamps[start_idx : start_idx + self.config.window_size]

            output = self._save_video(
                "negative",  # Save as negative
                video_id,
                0,  # Label 0 = negative
                split,
                start_idx,
                window_frames,
                window_times,
                window_times[-1],
                extraction_type="negative_from_positive",
                source_label=1,  # Original video is positive
                source_event_time=source_event_time,
                source_event_type=source_event_type,
            )
            buffer.append(output)

            if len(buffer) >= self.config.buffer_size:
                window_count += self._process_buffer(buffer)
                buffer = []

        if buffer:
            window_count += self._process_buffer(buffer)

        return window_count

    def _process_negative_video(
        self, video_id: str, filepath: str, event_time: float, split: str,
        event_type: str = None
    ):
        """Process negative video: extract incident windows and/or random samples.

        If event_time is provided (not None/NaN), extracts windows around the incident
        to teach the model "this looks like something might happen, but it's NOT a collision".

        Also extracts random windows from the video (avoiding the incident area if present).
        """
        import math

        window_count = 0
        window_length_s = self.config.window_size / self.config.target_fps

        # Check if this negative video has an incident/event time
        has_event = event_time is not None and not (
            isinstance(event_time, float) and math.isnan(event_time)
        )

        # Define incident exclusion zone (if event exists)
        incident_start_time = None
        incident_end_time = None

        if has_event:
            # Extract incident windows (same logic as positive, but labeled negative)
            incident_count = self._extract_incident_windows(
                video_id, filepath, event_time, split, event_type
            )
            window_count += incident_count

            # Define exclusion zone for random sampling
            # Exclude from (event - positive_window_before_event - window_length) to event
            incident_start_time = max(
                0.0,
                event_time - self.config.positive_window_before_event - window_length_s
            )
            incident_end_time = event_time

        # Extract random windows (avoiding incident zone if present)
        random_count = self._extract_random_negative_windows(
            video_id, filepath, split, incident_start_time, incident_end_time,
            source_event_time=event_time if has_event else None,
            source_event_type=event_type,
        )
        window_count += random_count

        return window_count

    def _extract_incident_windows(
        self, video_id: str, filepath: str, event_time: float, split: str,
        event_type: str = None
    ) -> int:
        """Extract windows around an incident in a negative video.

        These are near-miss/incident clips that look dangerous but are NOT collisions.
        Uses the same timing logic as positive videos (windows ending within
        positive_window_before_event seconds before the event).

        Args:
            video_id: Video identifier
            filepath: Path to video file
            event_time: Time of the incident
            split: Dataset split
            event_type: Event type from source video

        Returns:
            Number of incident windows extracted
        """
        window_length = self.config.window_size / self.config.target_fps

        # Same timing as positive videos
        earliest_incident_time = max(
            0.0, event_time - self.config.positive_window_before_event - window_length
        )
        duration = event_time + 1 - earliest_incident_time

        frame_gen = self._extract_frames(
            filepath, start_time=earliest_incident_time, duration=duration
        )
        try:
            result = list(zip(*frame_gen)) if frame_gen else ([], [])
            frames, timestamps = (list(result[0]), list(result[1])) if result else ([], [])
        finally:
            frame_gen.close()

        if len(frames) < self.config.window_size:
            self.logger.warning(f"Not enough frames for incident in negative video {video_id}")
            return 0

        buffer = []
        window_count = 0

        # Extract windows using same logic as positive (but label=0)
        i = 0
        while i + self.config.window_size <= len(frames):
            window_frames = frames[i : i + self.config.window_size]
            window_times = timestamps[i : i + self.config.window_size]
            last_time = window_times[-1]

            # Check if window ends within the "incident zone" (same as positive logic)
            # but don't include windows that pass the event
            if last_time >= event_time:
                break

            # Only extract if window ends within positive_window_before_event of incident
            if (event_time - self.config.positive_window_before_event) <= last_time < event_time:
                output = self._save_video(
                    "negative",  # Save as negative (this is the key difference!)
                    video_id,
                    0,  # Label 0 = negative
                    split,
                    i,
                    window_frames,
                    window_times,
                    last_time,
                    extraction_type="negative_incident",
                    source_label=0,  # Original video is negative
                    source_event_time=event_time,
                    source_event_type=event_type,
                )
                buffer.append(output)

                if len(buffer) >= self.config.buffer_size:
                    window_count += self._process_buffer(buffer)
                    buffer = []

            i += self.config.window_stride

        if buffer:
            window_count += self._process_buffer(buffer)

        return window_count

    def _extract_random_negative_windows(
        self,
        video_id: str,
        filepath: str,
        split: str,
        exclude_start: float = None,
        exclude_end: float = None,
        source_event_time: float = None,
        source_event_type: str = None,
    ) -> int:
        """Extract random negative windows, optionally avoiding an exclusion zone.

        Args:
            video_id: Video identifier
            filepath: Path to video file
            split: Dataset split
            exclude_start: Start of exclusion zone (seconds), or None
            exclude_end: End of exclusion zone (seconds), or None
            source_event_time: Event time from source video (if any)
            source_event_type: Event type from source video (if any)

        Returns:
            Number of windows extracted
        """
        duration = get_video_duration(filepath)
        total_downsampled_frames = int(duration * self.config.target_fps)
        window_length = self.config.window_size
        window_length_s = window_length / self.config.target_fps

        # Build list of valid start positions
        possible_starts = []
        for start_frame in range(
            0, total_downsampled_frames - window_length, self.config.window_stride
        ):
            start_time = start_frame / self.config.target_fps
            end_time = start_time + window_length_s

            # Check if this window overlaps with exclusion zone
            if exclude_start is not None and exclude_end is not None:
                # Window overlaps if it doesn't end before exclusion starts
                # and doesn't start after exclusion ends
                if not (end_time <= exclude_start or start_time >= exclude_end):
                    continue  # Skip this window - it overlaps with incident zone

            possible_starts.append(start_frame)

        if not possible_starts:
            self.logger.debug(f"No valid random windows in negative video {video_id}")
            return 0

        # Sample random windows
        if len(possible_starts) <= self.config.max_negative_examples_per_video:
            selected_starts = possible_starts
        else:
            random.seed(self.config.seed + hash(video_id) % 10000)
            selected_starts = random.sample(
                possible_starts, self.config.max_negative_examples_per_video
            )
            selected_starts.sort()

        buffer = []
        window_count = 0

        for start_frame in selected_starts:
            start_time = start_frame / self.config.target_fps

            frames = self._extract_window_frames(
                filepath, start_time, self.config.window_size
            )

            if len(frames) < self.config.window_size:
                self.logger.warning(f"Not enough frames for window in {video_id}")
                continue

            window_times = [
                start_time + i / self.config.target_fps
                for i in range(self.config.window_size)
            ]

            output = self._save_video(
                "negative",
                video_id,
                0,
                split,
                start_frame,
                frames,
                window_times,
                window_times[-1],
                extraction_type="negative_random",
                source_label=0,  # Original video is negative
                source_event_time=source_event_time,
                source_event_type=source_event_type,
            )
            buffer.append(output)

            if len(buffer) >= self.config.buffer_size:
                window_count += self._process_buffer(buffer)
                buffer = []

        if buffer:
            window_count += self._process_buffer(buffer)

        return window_count

    # ==================== Classification Mode Methods ====================

    def _process_video_classification(
        self,
        video_id: str,
        filepath: str,
        event_time: float,
        split: str,
        event_type: str,
    ):
        """Process video for classification task - extract clips containing the event.

        For event videos: Extract sliding window clips where event appears at different
        positions (from last frame moving backward), plus pre-event normal clips.

        For normal_driving: Extract random clips.

        Args:
            video_id: Video identifier
            filepath: Path to video file
            event_time: Time of the event (None/NaN for normal_driving)
            split: Dataset split
            event_type: The event type label
        """
        import math

        # Skip excluded event types
        if event_type in self.config.classification_skip_event_types:
            self.logger.debug(f"Skipping {video_id}: event_type '{event_type}' is excluded")
            return 0

        has_event = event_time is not None and not (
            isinstance(event_time, float) and math.isnan(event_time)
        )

        if event_type == "normal_driving" or not has_event:
            # Extract random clips for normal driving
            return self._extract_classification_random_clips(
                video_id, filepath, split, event_type
            )
        else:
            # Extract event clips + pre-event normal clips
            window_count = 0

            # 1. Extract clips containing the event at various positions
            window_count += self._extract_classification_event_clips(
                video_id, filepath, event_time, split, event_type
            )

            # 2. Extract pre-event normal clips (labeled as normal_driving)
            window_count += self._extract_classification_pre_event_clips(
                video_id, filepath, event_time, split, event_type
            )

            return window_count

    def _extract_classification_event_clips(
        self,
        video_id: str,
        filepath: str,
        event_time: float,
        split: str,
        event_type: str,
    ) -> int:
        """Extract clips where the event appears at different frame positions.

        Creates sliding windows where the event frame moves from the last position
        toward the first, with configurable stride.

        For 2-second clips at 8fps (16 frames) with stride 2:
        - Clip 1: event at frame 15 (last)
        - Clip 2: event at frame 13
        - Clip 3: event at frame 11
        - ... until event at frame 1
        """
        window_length_s = self.config.window_size / self.config.target_fps
        stride = self.config.classification_event_stride
        window_count = 0
        buffer = []

        # Get video duration to ensure we have enough video after the event
        video_duration = get_video_duration(filepath)
        if video_duration is None:
            self.logger.warning(f"Could not get duration for {video_id}")
            return 0

        # For each position where event can appear in the clip
        # Position 0 = first frame, position (window_size-1) = last frame
        # We go from last frame (window_size-1) down to frame 0, with stride
        for event_frame_idx in range(self.config.window_size - 1, -1, -stride):
            # Calculate clip start time based on event position
            # If event is at frame idx, clip starts at: event_time - (idx / fps)
            clip_start_time = event_time - (event_frame_idx / self.config.target_fps)

            # Skip if clip would start before video beginning
            if clip_start_time < 0:
                continue

            # Skip if clip would extend beyond video end
            clip_end_time = clip_start_time + window_length_s
            if clip_end_time > video_duration:
                continue

            # Extract frames for this window
            frames = self._extract_window_frames(
                filepath, clip_start_time, self.config.window_size
            )

            if len(frames) < self.config.window_size:
                self.logger.warning(f"Not enough frames for event clip in {video_id}")
                continue

            window_times = [
                clip_start_time + i / self.config.target_fps
                for i in range(self.config.window_size)
            ]

            # Create unique window index based on event frame position
            window_idx = self.config.window_size - 1 - event_frame_idx

            output = self._save_video_classification(
                video_id=video_id,
                label=event_type,
                split=split,
                window_idx=window_idx,
                window_frames=frames,
                window_times=window_times,
                extraction_type=f"event_at_frame_{event_frame_idx}",
                event_frame_index=event_frame_idx,
                source_event_time=event_time,
                source_event_type=event_type,
            )
            buffer.append(output)

            if len(buffer) >= self.config.buffer_size:
                window_count += self._process_buffer(buffer)
                buffer = []

        if buffer:
            window_count += self._process_buffer(buffer)

        return window_count

    def _extract_classification_pre_event_clips(
        self,
        video_id: str,
        filepath: str,
        event_time: float,
        split: str,
        source_event_type: str,
    ) -> int:
        """Extract pre-event clips labeled as normal_driving.

        These clips end at least classification_pre_event_buffer seconds before the event.
        Extracts classification_normal_clips random clips from this region.
        """
        window_length_s = self.config.window_size / self.config.target_fps
        buffer_before_event = self.config.classification_pre_event_buffer
        num_clips = self.config.classification_normal_clips

        # Cutoff: clips must END before (event_time - buffer)
        cutoff_time = event_time - buffer_before_event

        # Need at least one full window before cutoff
        if cutoff_time < window_length_s:
            self.logger.debug(f"Not enough time for pre-event clips in {video_id}")
            return 0

        # Calculate valid start time range
        # Clip can start from 0 to (cutoff_time - window_length)
        max_start_time = cutoff_time - window_length_s
        if max_start_time <= 0:
            return 0

        # Generate random start times
        random.seed(self.config.seed + hash(video_id + "_pre") % 10000)
        num_possible = int(max_start_time * self.config.target_fps)
        if num_possible <= 0:
            return 0

        num_samples = min(num_clips, num_possible)
        start_frames = random.sample(range(num_possible), num_samples)
        start_frames.sort()

        buffer = []
        window_count = 0

        for idx, start_frame in enumerate(start_frames):
            start_time = start_frame / self.config.target_fps

            frames = self._extract_window_frames(
                filepath, start_time, self.config.window_size
            )

            if len(frames) < self.config.window_size:
                continue

            window_times = [
                start_time + i / self.config.target_fps
                for i in range(self.config.window_size)
            ]

            # Window index for pre-event clips: use 900+ to distinguish
            window_idx = 900 + idx

            output = self._save_video_classification(
                video_id=video_id,
                label="normal_driving",  # Label as normal_driving
                split=split,
                window_idx=window_idx,
                window_frames=frames,
                window_times=window_times,
                extraction_type="pre_event",
                event_frame_index=-1,
                source_event_time=event_time,
                source_event_type=source_event_type,
            )
            buffer.append(output)

            if len(buffer) >= self.config.buffer_size:
                window_count += self._process_buffer(buffer)
                buffer = []

        if buffer:
            window_count += self._process_buffer(buffer)

        return window_count

    def _extract_classification_random_clips(
        self,
        video_id: str,
        filepath: str,
        split: str,
        event_type: str,
    ) -> int:
        """Extract random clips from normal_driving videos."""
        window_length_s = self.config.window_size / self.config.target_fps
        num_clips = self.config.classification_normal_clips

        video_duration = get_video_duration(filepath)
        if video_duration is None or video_duration < window_length_s:
            self.logger.warning(f"Video too short for clips: {video_id}")
            return 0

        max_start_time = video_duration - window_length_s
        if max_start_time <= 0:
            return 0

        random.seed(self.config.seed + hash(video_id + "_rand") % 10000)
        num_possible = int(max_start_time * self.config.target_fps)
        if num_possible <= 0:
            return 0

        num_samples = min(num_clips, num_possible)
        start_frames = random.sample(range(num_possible), num_samples)
        start_frames.sort()

        buffer = []
        window_count = 0

        for idx, start_frame in enumerate(start_frames):
            start_time = start_frame / self.config.target_fps

            frames = self._extract_window_frames(
                filepath, start_time, self.config.window_size
            )

            if len(frames) < self.config.window_size:
                continue

            window_times = [
                start_time + i / self.config.target_fps
                for i in range(self.config.window_size)
            ]

            # Window index for random clips: use 800+ to distinguish
            window_idx = 800 + idx

            output = self._save_video_classification(
                video_id=video_id,
                label=event_type,  # Keep original label (normal_driving)
                split=split,
                window_idx=window_idx,
                window_frames=frames,
                window_times=window_times,
                extraction_type="random",
                event_frame_index=-1,
                source_event_time=None,
                source_event_type=event_type,
            )
            buffer.append(output)

            if len(buffer) >= self.config.buffer_size:
                window_count += self._process_buffer(buffer)
                buffer = []

        if buffer:
            window_count += self._process_buffer(buffer)

        return window_count

    def _save_video_classification(
        self,
        video_id: str,
        label: str,
        split: str,
        window_idx: int,
        window_frames: List[np.ndarray],
        window_times: List[float],
        extraction_type: str,
        event_frame_index: int,
        source_event_time: float,
        source_event_type: str,
    ):
        """Save video clip for classification task.

        Args:
            video_id: Source video identifier
            label: Event type label (string, e.g., 'collision_front', 'normal_driving')
            split: Dataset split
            window_idx: Window index for filename
            window_frames: List of frames
            window_times: List of timestamps
            extraction_type: How this clip was extracted
            event_frame_index: Frame index where event appears (-1 if N/A)
            source_event_time: Original event time
            source_event_type: Original event type
        """
        # Sanitize label for filename (replace spaces, special chars)
        label_safe = label.replace(" ", "_").replace("/", "_")
        save_name = f"{video_id}_win{window_idx:03d}_{label_safe}.{self.config.video_format}"
        path = os.path.join("clips", save_name)

        metadata = {
            "path": path,
            "label": label,
            "split": split,
            "source": video_id,
            "source_event_time": source_event_time,
            "source_event_type": source_event_type,
            "start_time": window_times[0],
            "end_time": window_times[-1],
            "duration": window_times[-1] - window_times[0],
            "num_frames": len(window_frames),
            "extraction_type": extraction_type,
            "event_frame_index": event_frame_index,
        }

        save_path = os.path.join(self.config.output_dir, path)

        return {
            "frames": window_frames,
            "save_path": save_path,
            "metadata": metadata,
        }

    # ==================== End Classification Mode Methods ====================

    def _save_video(
        self,
        label_dir,
        video_id,
        label,
        split,
        i,
        window_frames,
        window_times,
        last_time,
        extraction_type: str = None,
        source_label: int = None,
        source_event_time: float = None,
        source_event_type: str = None,
    ):
        """Save video clip and create metadata.

        Args:
            label_dir: Unused (kept for compatibility), all clips go to 'clips' folder
            video_id: Source video identifier
            label: Clip label (0 or 1)
            split: Dataset split (train/val)
            i: Window index
            window_frames: List of frames
            window_times: List of timestamps
            last_time: End timestamp
            extraction_type: How this clip was extracted:
                - 'positive_event': Positive window before collision
                - 'negative_incident': Incident window from negative video (near-miss)
                - 'negative_random': Random sample from negative video
                - 'negative_from_positive': Early clip from positive video
            source_label: Original video label (0 or 1)
            source_event_time: Event time from source video (if any)
            source_event_type: Event type from source video (if any)
        """
        save_name = f"{video_id}_win{i:03d}_l{label}.{self.config.video_format}"

        # Flat structure: all clips in 'clips' folder, labels only in CSV
        path = os.path.join("clips", save_name)

        metadata = {
            "path": path,
            "label": label,
            "source": video_id,
            "source_label": source_label,
            "source_event_time": source_event_time,
            "source_event_type": source_event_type,
            "start_time": window_times[0],
            "end_time": last_time,
            "duration": last_time - window_times[0],
            "num_frames": len(window_frames),
            "extraction_type": extraction_type,
            "split": split,
        }

        save_path = os.path.join(self.config.output_dir, path)
        return {
            "frames": window_frames.copy(),
            "save_path": save_path,
            "metadata": metadata,
        }

    def _balance_dataset(self):
        """Balance the dataset by equalizing positive and negative samples"""
        self.logger.info("Starting dataset balancing...")

        meta_df = pd.DataFrame(self.metadata)
        to_delete = []

        for split in self.config.splits:
            sub = (
                meta_df.loc[meta_df["split"] == split]
                if "split" in meta_df.columns
                else meta_df
            )

            pos_mask = sub["label"] == 1
            pos_samples = sub[pos_mask]
            neg_mask = sub["label"] == 0
            neg_samples = sub[neg_mask]

            sample_num = min(len(pos_samples), len(neg_samples))

            if sample_num == 0:
                continue

            if len(neg_samples) > sample_num:
                neg_selected = neg_samples.sample(
                    sample_num, random_state=self.config.seed
                )
                to_delete.extend(
                    neg_samples[~neg_samples.index.isin(neg_selected.index)]["path"]
                )
                drop_index = neg_samples[
                    ~neg_samples.index.isin(neg_selected.index)
                ].index
                meta_df = meta_df.drop(index=drop_index)

        # Batch delete files
        for path in to_delete:
            full_path = os.path.join(self.config.output_dir, path)
            if os.path.exists(full_path):
                os.remove(full_path)

        meta_df.to_csv(
            os.path.join(self.config.output_dir, "metadata.csv"), index=False
        )
        self.metadata = meta_df.to_dict("records")

        self.logger.info("Dataset balancing completed")

    def _visualize_samples(self):
        """Create visualization of sample videos"""
        if len(self.metadata) == 0:
            self.logger.warning("No metadata available for visualization")
            return

        plt.figure(figsize=(15, 5))
        samples = random.sample(self.metadata, min(3, len(self.metadata)))

        for idx, sample in enumerate(samples):
            full_path = os.path.join(self.config.output_dir, sample["path"])

            cap = cv2.VideoCapture(full_path)
            ret, frame = cap.read()
            if ret:
                plt.subplot(1, 3, idx + 1)
                plt.imshow(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
                label_text = "Positive" if sample["label"] else "Negative"
                plt.title(f"{label_text} Sample\n{sample['source']}")
                plt.axis("off")
            cap.release()

        plt.tight_layout()
        plt.savefig(os.path.join(self.config.output_dir, "sample_visualization.png"))
        plt.close()  # Use plt.close() instead of plt.show() to avoid terminal issues

    def _cleanup_resources(self):
        """Clean up all resources"""
        for key in list(self.writer_cache.keys()):
            self.writer_cache[key].release()
        self.writer_cache.clear()

    def print_statistics(self):
        """Print dataset statistics"""
        if not hasattr(self, "metadata") or len(self.metadata) == 0:
            self.logger.warning("No metadata available for statistics")
            return

        meta_df = pd.DataFrame(self.metadata)

        print("\n" + "=" * 50)
        print("DATASET STATISTICS")
        print("=" * 50)

        for split in self.config.splits:
            split_data = meta_df[meta_df["path"].str.startswith(split)]
            pos_count = len(split_data[split_data["label"] == 1])
            neg_count = len(split_data[split_data["label"] == 0])
            total = len(split_data)

            print(f"\n{split.upper()} SET:")
            print(f"  Positive samples: {pos_count}")
            print(f"  Negative samples: {neg_count}")
            print(f"  Total samples: {total}")
            if total > 0:
                print(f"  Balance ratio: {pos_count/total:.2%} positive")

    def run(self):
        """Main processing pipeline"""
        self.logger.info("Starting video processing pipeline...")
        self.logger.info(f"Configuration: {self.config}")

        video_ids = self.df["id"].unique()
        if self.config.test_size is not None:
            # Create new train/val split based on test_size
            train_ids, _ = train_test_split(
                video_ids,
                test_size=self.config.test_size,
                random_state=self.config.seed,
            )
            train_ids_set = set(train_ids)
            self.df["split"] = self.df["id"].apply(
                lambda x: "train" if x in train_ids_set else "val"
            )
        elif self.config.split is not None:
            # Override with specified split
            self.df["split"] = self.config.split
        elif "split" not in self.df.columns:
            # No split specified and none in CSV - use default
            self.df["split"] = "data"
        # else: preserve existing 'split' column from CSV

        self.logger.info(
            f"Processing {len(self.df)} video records with {self.config.num_threads} threads"
        )

        with ThreadPoolExecutor(max_workers=self.config.num_threads) as executor:
            futures = []
            for _, row in self.df.iterrows():
                futures.append(executor.submit(self.process_video, row=row))

            for future in tqdm(futures, desc="Processing videos", total=len(futures)):
                future.result()

        # Collect metadata from queue
        self.metadata = []
        while not self.metadata_queue.empty():
            self.metadata.append(self.metadata_queue.get())

        self._cleanup_resources()

        if len(self.metadata) > 0:
            # Always save metadata.csv - append to existing if present
            meta_df = pd.DataFrame(self.metadata)
            metadata_path = os.path.join(self.config.output_dir, "metadata.csv")

            # Check if existing metadata.csv exists and append to it
            if os.path.exists(metadata_path) and os.path.getsize(metadata_path) > 0:
                try:
                    existing_df = pd.read_csv(metadata_path)
                    meta_df = pd.concat([existing_df, meta_df], ignore_index=True)
                    self.logger.info(f"Appending {len(self.metadata)} new clips to existing {len(existing_df)} clips")
                except Exception as e:
                    self.logger.warning(f"Could not read existing metadata.csv, overwriting: {e}")

            meta_df.to_csv(metadata_path, index=False)
            self.logger.info(f"Saved metadata to {metadata_path} (total: {len(meta_df)} clips)")

            if self.config.balance_dataset:
                self._balance_dataset()
            self.print_statistics()
            self._visualize_samples()
        else:
            self.logger.warning("No video windows were generated")

        # Final flush of incremental metadata
        self._flush_metadata_to_csv(force=True)
        self.logger.info(f"Total clips written to {self._metadata_csv_path}: {self._clips_written}")

        self.logger.info("Processing pipeline completed successfully")


def create_config_from_args():
    """Create configuration from command line arguments"""
    parser = argparse.ArgumentParser(description="Generic Video Dataset Processor")

    # Input/Output paths
    parser.add_argument(
        "--video-dir",
        default="../nexar-competition/train",
        help="Directory containing input videos",
    )
    parser.add_argument(
        "--csv-path",
        default="../nexar-competition/train.csv",
        help="Path to CSV file with video metadata",
    )
    parser.add_argument(
        "--output-dir",
        default="balanced_dataset",
        help="Output directory for processed dataset",
    )
    parser.add_argument(
        "--use-temp-dir",
        action="store_true",
        help="Use temporary directory for videos.",
    )

    # Video processing parameters
    parser.add_argument(
        "--target-fps", type=int, default=8, help="Target FPS for output videos"
    )
    parser.add_argument(
        "--window-size", type=int, default=16, help="Number of frames per window (default: 16 = 2 seconds at 8fps)"
    )
    parser.add_argument(
        "--window-stride", type=int, default=2, help="Frame stride between windows"
    )
    parser.add_argument("--img-width", type=int, default=256, help="Output image width")
    parser.add_argument(
        "--img-height", type=int, default=256, help="Output image height"
    )

    # Dataset parameters
    parser.add_argument(
        "--test-size", type=float, default=None, help="Fraction of data for validation"
    )
    parser.add_argument(
        "--split",
        type=str,
        default=None,
        help="Dataset split to process (overrides test_size)",
    )
    parser.add_argument(
        "--seed", type=int, default=42, help="Random seed for reproducibility"
    )

    # Video encoding
    parser.add_argument("--video-format", default="mp4", help="Output video format")
    parser.add_argument("--codec", default="H264", help="Video codec")
    parser.add_argument(
        "--no-ffmpeg-encoding",
        action="store_true",
        help="Use OpenCV instead of ffmpeg for encoding (ffmpeg recommended for H.264)",
    )

    # Performance settings
    parser.add_argument(
        "--num-threads", type=int, help="Number of processing threads (default: auto)"
    )
    parser.add_argument(
        "--max-videos",
        type=int,
        help="Maximum number of videos to process (for testing)",
    )
    parser.add_argument(
        "--buffer-size",
        type=int,
        default=48,
        help="Number of windows to buffer in memory",
    )

    # Time-based labeling
    parser.add_argument(
        "--positive-window-before-event",
        type=float,
        default=1.5,
        help="Seconds before event to mark windows as positive",
    )

    # Balanced dataset parameters
    parser.add_argument(
        "--max-negative-examples-per-video",
        type=int,
        default=6,
        help="Maximum number of negative examples to extract per video",
    )

    parser.add_argument(
        "--skip-balancing",
        action="store_true",
        help="Skip dataset balancing",
    )

    # Extract negatives from positive videos
    parser.add_argument(
        "--extract-negatives-from-positive",
        action="store_true",
        help="Extract negative clips from early part of positive videos",
    )
    parser.add_argument(
        "--negative-buffer-before-positive",
        type=float,
        default=1.5,
        help="Seconds gap between negative clips and positive window (default: 1.5)",
    )
    parser.add_argument(
        "--max-negatives-from-positive-video",
        type=int,
        default=3,
        help="Maximum negative clips to extract from each positive video (default: 3)",
    )

    # Restart option
    parser.add_argument(
        "--restart",
        action="store_true",
        help="Restart processing from existing metadata.csv, skipping already processed videos",
    )

    # Use splits from CSV
    parser.add_argument(
        "--use-csv-splits",
        action="store_true",
        help="Use 'split' column from CSV (train/val/test) instead of config split",
    )

    # Incremental metadata writing
    parser.add_argument(
        "--clips-metadata-full-path",
        type=str,
        default=None,
        help="Path to clips_metadata_full.csv for incremental writing (default: output_dir/clips_metadata_full.csv)",
    )
    parser.add_argument(
        "--metadata-flush-interval",
        type=int,
        default=100,
        help="Flush metadata to CSV every N clips (default: 100)",
    )

    # Classification mode arguments
    parser.add_argument(
        "--classification-mode",
        action="store_true",
        help="Enable classification mode: extract clips containing the event itself",
    )
    parser.add_argument(
        "--classification-event-stride",
        type=int,
        default=2,
        help="Frame stride for event position in classification clips (default: 2)",
    )
    parser.add_argument(
        "--classification-pre-event-buffer",
        type=float,
        default=3.0,
        help="Seconds before event for pre-event normal clips (default: 3.0)",
    )
    parser.add_argument(
        "--classification-normal-clips",
        type=int,
        default=2,
        help="Number of random clips from normal_driving videos (default: 2)",
    )
    parser.add_argument(
        "--classification-skip-event-types",
        type=str,
        nargs="+",
        default=["hard_brake_unverified", "other"],
        help="Event types to skip in classification mode (default: hard_brake_unverified other)",
    )

    args = parser.parse_args()

    return Config(
        video_dir=args.video_dir,
        csv_path=args.csv_path,
        output_dir=args.output_dir,
        use_temp_dir=args.use_temp_dir,
        target_fps=args.target_fps,
        window_size=args.window_size,
        window_stride=args.window_stride,
        img_size=(args.img_width, args.img_height),
        test_size=args.test_size,
        split=args.split,
        seed=args.seed,
        video_format=args.video_format,
        codec=args.codec,
        use_ffmpeg_encoding=not args.no_ffmpeg_encoding,
        num_threads=args.num_threads,
        max_videos=args.max_videos,
        buffer_size=args.buffer_size,
        positive_window_before_event=args.positive_window_before_event,
        max_negative_examples_per_video=args.max_negative_examples_per_video,
        extract_negatives_from_positive=args.extract_negatives_from_positive,
        negative_buffer_before_positive=args.negative_buffer_before_positive,
        max_negatives_from_positive_video=args.max_negatives_from_positive_video,
        restart=args.restart,
        balance_dataset=not args.skip_balancing,
        use_csv_splits=args.use_csv_splits,
        clips_metadata_full_path=args.clips_metadata_full_path,
        metadata_flush_interval=args.metadata_flush_interval,
        classification_mode=args.classification_mode,
        classification_event_stride=args.classification_event_stride,
        classification_pre_event_buffer=args.classification_pre_event_buffer,
        classification_normal_clips=args.classification_normal_clips,
        classification_skip_event_types=tuple(args.classification_skip_event_types),
    )


if __name__ == "__main__":
    # You can either use command line arguments or create config manually
    config = create_config_from_args()

    # Example of manual configuration:
    # config = Config(
    #     video_dir="path/to/your/videos",
    #     csv_path="path/to/your/metadata.csv",
    #     output_dir="my_dataset",
    #     target_fps=10,
    #     window_size=32
    # )

    processor = VideoProcessor(config)
    processor.run()
