"""
Insta360 Transfer Adapter for the Transfer Manager plugin.

Implements the TransferAdapter protocol for Insta360 cameras,
handling file downloads and processing (transcoding/stitching).
"""

import signal
import subprocess
import threading
from pathlib import Path
from typing import Callable

import requests
from insta360.rtmp import Client

from whitebox.plugins import get_class
from whitebox.utils import get_plugin_logger
from whitebox_plugin_device_insta360.utils import ffprobe_get_video_duration


logger = get_plugin_logger(__name__)

# Import protocol and RemoteFile from transfer manager plugin
TransferAdapter = get_class("transfer.TransferAdapter", proxy=False)
RemoteFile = get_class("transfer.RemoteFile", proxy=False)


class Insta360TransferAdapter(TransferAdapter):
    """
    Transfer adapter for Insta360 cameras.

    Handles downloading .insv files and processing them with FFmpeg.
    """

    def _get_client(self, device_connection) -> Client:
        """Get an Insta360 client for the device connection."""
        # TODO: When we add support for multiple Insta360's at once, resolve proper
        #       target by `device_connection` for the `Client` to connect to
        client = Client()
        client.ensure_camera_connected()
        return client

    def list_files(self, device_connection) -> list:
        """List files available on device using HTTP directory listing."""
        client = self._get_client(device_connection)
        file_list = client.get_files_list()
        return [
            RemoteFile(
                path=path,
                size=None,
                created_at=None,
                checksum=None,
                metadata={},
            )
            for path in file_list
        ]

    def get_file_info(self, device_connection, path: str):
        """Get metadata for a specific file."""
        return RemoteFile(
            path=path,
            size=None,
            created_at=None,
            checksum=None,
            metadata={},
        )

    def download_file(
        self,
        device_connection,
        source_path: str,
        destination_path: Path,
        progress_callback: Callable[[int, int | None], None],
        cancel_event: threading.Event,
    ) -> bool:
        """
        Download file from Insta360 camera with progress reporting.

        Uses HTTP download matching the insta360 library's URL format
        (http://<camera_ip>:80/<path>) with added progress and cancellation.
        """
        client = self._get_client(device_connection)
        destination_path.parent.mkdir(parents=True, exist_ok=True)

        logger.info(f"Downloading {source_path} to {destination_path}")

        try:
            download_ok = self._download_with_progress(
                client=client,
                source_path=source_path,
                destination_path=destination_path,
                progress_callback=progress_callback,
                cancel_event=cancel_event,
            )

            if download_ok:
                logger.info(f"Download completed: {destination_path}")
            else:
                logger.warning(f"Download failed: {source_path}")

            return download_ok

        except Exception as e:
            logger.exception(f"Download error for {source_path}: {e}")
            return False

    def _download_with_progress(
        self,
        client: Client,
        source_path: str,
        destination_path: Path,
        progress_callback: Callable[[int, int | None], None],
        cancel_event: threading.Event,
    ) -> bool:
        """
        Download file with progress reporting and cancellation support.

        Uses the same HTTP URL format as the insta360 library's download_file
        (http://<connect_host>:80/<file_path>) but adds progress callbacks
        and cancel event checking.
        """
        download_url = f"http://{client.connect_host}:80{source_path}"

        try:
            response = requests.get(download_url, stream=True, timeout=30)
            response.raise_for_status()

            total_size = int(response.headers.get("content-length", 0))
            bytes_downloaded = 0
            chunk_size = 8192

            with open(destination_path, "wb") as f:
                for chunk in response.iter_content(chunk_size=chunk_size):
                    if cancel_event.is_set():
                        logger.info("Download cancelled")
                        return False

                    if chunk:
                        f.write(chunk)
                        bytes_downloaded += len(chunk)

                        if progress_callback:
                            progress_callback(bytes_downloaded, total_size)

            return True

        except requests.exceptions.RequestException as e:
            logger.error(f"HTTP download error: {e}")
            return False

    def supports_resume(self) -> bool:
        """Insta360 cameras support HTTP range requests."""
        return True

    def process_file(
        self,
        source_path: Path,
        destination_path: Path,
        progress_callback: Callable[[int, str | None], None],
        cancel_event: threading.Event,
    ) -> bool:
        """
        Process downloaded .insv file (transcode to MP4).

        Insta360 .insv files are dual-lens fisheye videos that need to be:
        1. Stitched together (for 360 content)
        2. Transcoded to a standard format

        For now, we do a basic transcode. Full stitching would require
        the Insta360 stitcher SDK or ffmpeg's v360 filter.
        """
        destination_path.parent.mkdir(parents=True, exist_ok=True)

        logger.info(f"Processing {source_path} -> {destination_path}")

        if progress_callback:
            progress_callback(0, "starting")

        try:
            result = self._run_ffmpeg_processing(
                source_path=source_path,
                destination_path=destination_path,
                progress_callback=progress_callback,
                cancel_event=cancel_event,
            )

            if result:
                logger.info(f"Processing completed: {destination_path}")
                if progress_callback:
                    progress_callback(100, "completed")
            else:
                logger.warning(f"Processing failed: {source_path}")

            return result

        except Exception as e:
            logger.exception(f"Processing error for {source_path}: {e}")
            return False

    def _run_ffmpeg_processing(
        self,
        source_path: Path,
        destination_path: Path,
        progress_callback: Callable[[int, str | None], None],
        cancel_event: threading.Event,
    ) -> bool:
        """Run FFmpeg to transcode the video file."""
        cmd = [
            "ffmpeg",
            "-y",
            "-i",
            str(source_path),
            "-c:v",
            "libx264",
            "-preset",
            "medium",
            "-crf",
            "23",
            "-c:a",
            "aac",
            "-b:a",
            "192k",
            "-progress",
            "pipe:1",
            "-nostats",
            str(destination_path),
        ]

        logger.info(f"FFmpeg command: {' '.join(cmd)}")

        try:
            process = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.DEVNULL,
                universal_newlines=True,
            )

            duration = ffprobe_get_video_duration(source_path) or 0
            logger.info(
                f"Processing started, source duration: {duration:.1f}s",
            )

            while True:
                if cancel_event.is_set():
                    logger.info("Processing cancelled, stopping FFmpeg...")
                    process.send_signal(signal.SIGQUIT)
                    try:
                        process.wait(timeout=5)
                    except subprocess.TimeoutExpired:
                        logger.warning(
                            "FFmpeg did not exit within 5s timeout, killing...",
                        )
                        process.kill()
                        process.wait(timeout=2)
                    return False

                line = process.stdout.readline()
                if not line and process.poll() is not None:
                    break

                if line.startswith("out_time_ms="):
                    try:
                        time_ms = int(line.split("=")[1].strip())
                        time_s = time_ms / 1_000_000

                        if duration > 0:
                            percent = min(99, int((time_s / duration) * 100))
                            if progress_callback:
                                progress_callback(percent, "transcoding")
                    except (ValueError, IndexError):
                        pass

            exit_code = process.poll()
            if exit_code == 0:
                logger.info(f"Processing completed: {destination_path}")
            else:
                logger.error(
                    f"FFmpeg exited with code {exit_code}: {source_path}",
                )
            return exit_code == 0

        except FileNotFoundError:
            logger.error("FFmpeg not found. Please install FFmpeg.")
            return False
        except Exception as e:
            logger.exception(f"FFmpeg error: {e}")
            return False
