import re
import signal
import time
import subprocess
from datetime import timedelta
from pathlib import Path

import django_rq
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.conf import settings
from django.utils import timezone
from dateutil.parser import parse as parse_datetime
from insta360.rtmp import Client

from whitebox.db import get_model
from whitebox.utils import get_plugin_logger
from .utils import (
    ffprobe_get_video_duration,
)


logger = get_plugin_logger(__name__)


DeviceConnection = get_model("device.DeviceConnection")
FlightSession = get_model("flight.FlightSession")
FlightSessionRecording = get_model("flight.FlightSessionRecording")


def _get_client_for_device_connection(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(
        # host=host,
        # port=port,
    )

    logger.info("Checking if camera is connected...")
    client.ensure_camera_connected()

    return client


def _get_base_flight_recording_path(flight_session, device_connection):
    return (
        settings.MEDIA_ROOT
        / "flight_recordings"
        / f"{flight_session.id}_{device_connection.id}"
    )


def _get_next_flight_recording_path(flight_session, device_connection):
    # As the HLS recordings have multiple files, each of them will be saved into
    # its own directory. Considering that there might be crashes etc, so there
    # could be multiple recordings for a single flight session, every single
    # recording is going to have its own directory.
    base_path = _get_base_flight_recording_path(
        flight_session,
        device_connection,
    )

    i = 0
    while True:
        path = base_path / f"{i}"
        if not path.exists():
            path.mkdir(parents=True)
            return path
        i += 1


# region recording during flight


def _get_file_birth_time(file_path):
    # We need to get file's birth time, which is when the file was created. On
    # Linux, that's stored in `st_birthtime`, which was not exposed on Python
    # until version 3.15 using `os.statx`.
    # Refence: https://github.com/python/cpython/issues/83714
    #
    # Until we bump to 3.15, we'll invoke `stat` directly and parse its output.
    # We are explicitly using `%w` for getting (non-standard) ISO format
    # timestamp, as it returns timezone (which `%W` - unix timestamp - doesn't).
    #
    # Note that opening an existing file in `w` mode merely truncates it, so if
    # this helper will not return you THAT moment, but always the moment when
    # it was first created on disk.
    try:
        command = ["stat", "-c", "%w", file_path]
        birth_time = subprocess.check_output(command).decode().strip()

        if birth_time == "-":
            logger.error(
                f"Cannot get file birth time (unavailable): {file_path}",
            )
            return None

        return parse_datetime(birth_time)
    except FileNotFoundError:
        logger.warning(
            f"Cannot get file birth time (file not found): {file_path}",
        )
        return None
    except Exception as e:
        logger.exception(f"Failed to get file birth time for {file_path}")
        return None


def _handle_camera_connection_for_flight_session(
    flight_session,
    device_connection,
    output_path,
):
    playlist_file = f"{output_path}/playlist.m3u8"

    # Create a FlightSessionRecording to keep track of existing files in case
    # there's some failure within the thread, so that we can track used space
    recording = FlightSessionRecording(
        flight_session=flight_session,
    )
    recording.file.name = playlist_file
    recording.provided_by = device_connection
    recording.save()

    stream_name = f"device_connection_{device_connection.id}"
    input_target = f"rtmp://{settings.SRS_HOST}/live/{stream_name}"

    ffmpeg_stats_file = f"{output_path}/ffmpeg-stats.txt"
    ffmpeg_stderr_file = f"{output_path}/ffmpeg-stderr.txt"

    # fmt: off
    ffmpeg_command = [
        "ffmpeg",
        "-i", input_target,
        "-c", "copy",

        "-f", "hls",
        "-hls_time", "5",
        "-hls_list_size", "0",
        "-hls_segment_type", "fmp4",
        "-hls_flags", "independent_segments+temp_file",
        "-hls_segment_filename", f"{output_path}/segment_%03d.m4s",

        # Save stats to a file - used for ascertaining the start time of the
        # recording, as well as general logging for later debugging
        "-progress", ffmpeg_stats_file,
        "-stats_period", "5",

        playlist_file,
    ]
    # fmt: on

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

    with open(ffmpeg_stderr_file, "wb") as stderr_file:
        process = subprocess.Popen(
            ffmpeg_command,
            stdin=subprocess.PIPE,
            stdout=subprocess.DEVNULL,
            stderr=stderr_file,
        )

        logger.info("Recording started, waiting for flight session to end...")
        while True:
            flight_session.refresh_from_db()
            if not flight_session.is_active:
                break

            time.sleep(0.2)

        # FFmpeg seems to not wrap HLS properly with SIGTERM, so we use SIGINT
        logger.info("Flight stopped, stopping stream...")
        process.send_signal(signal.SIGQUIT)

        try:
            process.wait(timeout=5)
        except subprocess.TimeoutExpired:
            logger.warning(
                "FFmpeg process did not exit within 5s timeout, killing...",
            )
            process.kill()
            process.wait(timeout=2)

    logger.info(
        "Recording stopped, file should be saved now. Determining start time...",
    )

    start_time = _get_file_birth_time(ffmpeg_stats_file)
    if start_time:
        recording.started_at = start_time
        recording.status = FlightSessionRecording.STATUSES.READY

    recording.save()


def handle_recording_for_flight_session(
    flight_session_id,
    device_connection_id,
):
    flight_session = FlightSession.objects.get(id=flight_session_id)
    device_connection = DeviceConnection.objects.get(id=device_connection_id)

    output_path = _get_next_flight_recording_path(
        flight_session=flight_session,
        device_connection=device_connection,
    )

    _handle_camera_connection_for_flight_session(
        flight_session=flight_session,
        device_connection=device_connection,
        output_path=output_path,
    )

    logger.info("Recording handler finished, enqueuing post-flight operations...")
    # TODO: Create a small API for this (e.g. `whitebox.tasks.run_task`)
    #       which would allow us to transition to other task queues in the
    #       future without requiring individual plugin changes
    django_rq.enqueue(
        post_flight_operations,
        flight_session_id=flight_session_id,
        device_connection_id=device_connection_id,
        # RQ queue settings
        job_timeout="24h",
    )


# endregion recording during flight


# region handling media post flight


re_camera_file_date_time = re.compile(
    r"_"
    r"(?P<year>\d{4})"
    r"(?P<month>\d{2})"
    r"(?P<day>\d{2})"
    r"_"
    r"(?P<hour>\d{2})"
    r"(?P<minute>\d{2})"
    r"(?P<second>\d{2})"
    r"_",
)


def _filter_files_eligible_for_flight_session(flight_session, file_list):
    from_ts = timezone.make_naive(flight_session.started_at)
    to_ts = timezone.make_naive(flight_session.ended_at)

    eligible = []

    for file_path in file_list:
        parsed = Path(file_path)

        # Only download high resolution videos, we don't need any others
        if parsed.suffix.lower() != ".insv":
            continue

        filename = parsed.name
        regex_match = re_camera_file_date_time.search(filename)

        if not regex_match:
            continue

        # TODO: HOW TO ASCERTAIN TIMEZONE?
        # Now that we got the file's creation timestamp, we can figure out
        # whether it's eligible to be downloaded & associated with the actual
        # flight session or not. The filestamp indicates the moment when the
        # file was created, e.g. VID_20250903_165835_00_001.insv

        file_timestamp = timezone.datetime(
            year=int(regex_match.group("year")),
            month=int(regex_match.group("month")),
            day=int(regex_match.group("day")),
            hour=int(regex_match.group("hour")),
            minute=int(regex_match.group("minute")),
            second=int(regex_match.group("second")),
            # We do not have microseconds available
        )

        if from_ts <= file_timestamp <= to_ts:
            eligible.append(file_path)

    return eligible


def _update_recording_durations(flight_session):
    logger.debug(
        f"Updating recording durations for flight session {flight_session.id}...",
    )
    recordings = flight_session.recordings.filter(ended_at=None)

    for recording in recordings:
        if not recording.file:
            logger.warning(
                "Recording is has no assigned file, skipping... "
                f"(ID: {recording.pk})",
            )
            continue

        file_path = recording.file.path

        if not Path(file_path).exists():
            logger.warning(
                "Recording file does not exist, skipping... " f"(ID: {recording.pk})",
            )
            continue

        if not recording.started_at:
            logger.warning(
                f"Recording is missing start time, skipping... (ID: {recording.pk})",
            )
            continue

        try:
            seconds_duration = ffprobe_get_video_duration(file_path)
        except Exception as e:
            logger.exception(
                f"Failed to get video duration for recording {recording.id}",
            )
            continue

        delta = timedelta(seconds=seconds_duration)
        ended_at = recording.started_at + delta
        recording.ended_at = ended_at
        recording.status = FlightSessionRecording.STATUSES.READY
        recording.save(update_fields=["ended_at", "status"])

        logger.debug(
            f"Updated recording {recording.id} duration to {delta}",
        )


def post_flight_operations(
    flight_session_id,
    device_connection_id,
):
    """
    Post-flight operations for Insta360 cameras.

    Updates live-streamed recording durations, then lists files on the camera,
    filters for eligible clips within the flight timeframe, and emits a
    transfer queue event so the transfer manager daemon can handle downloading
    and processing.
    """
    logger.info(
        f"Post-flight tasks - F {flight_session_id} - D {device_connection_id}",
    )

    flight_session = FlightSession.objects.get(id=flight_session_id)
    device_connection = DeviceConnection.objects.get(id=device_connection_id)

    # First, update the live-streamed flight session recording with its actual
    # video duration
    _update_recording_durations(flight_session)

    # List files on the camera and queue eligible ones for transfer.
    # get_files_list() uses HTTP (not RTMP), so no open()/close() needed.
    # Retry for up to 10 minutes — camera may be briefly unresponsive after
    # recording stops while it finalizes files on the SD card.
    max_duration = 600  # 10 minutes
    retry_delay = 5  # seconds
    file_list = None
    start_time = time.monotonic()
    attempt = 0

    while True:
        attempt += 1
        try:
            client = _get_client_for_device_connection(device_connection)
            file_list = client.get_files_list()
            break
        except Exception as e:
            elapsed = time.monotonic() - start_time
            if elapsed + retry_delay >= max_duration:
                logger.error(
                    f"Failed to list files after {attempt} attempts over "
                    f"{elapsed:.0f}s for flight {flight_session_id}, "
                    f"device {device_connection_id}: {e}"
                )
                return
            logger.warning(
                f"get_files_list attempt {attempt} failed "
                f"({elapsed:.0f}s elapsed): {e}"
            )
            time.sleep(retry_delay)

    eligible_files = _filter_files_eligible_for_flight_session(
        flight_session,
        file_list,
    )

    logger.info(
        f"Found {len(eligible_files)} files for "
        f"flight session {flight_session_id}, queueing for transfer...",
    )

    if not eligible_files:
        logger.info("No eligible files found, nothing to transfer.")
        return

    # Emit event for the transfer manager daemon to queue these files
    channel_layer = get_channel_layer()
    async_to_sync(channel_layer.group_send)(
        "command.transfer.queue_batch",
        {
            "type": "command.transfer.queue_batch",
            "data": {
                "device_connection_id": device_connection.pk,
                "flight_session_id": flight_session.pk,
                "files": [{"path": fp} for fp in eligible_files],
            },
        },
    )

    logger.info(
        f"Emitted transfer queue event for {len(eligible_files)} files",
    )

    logger.info("Post-flight tasks finished (transfers queued).")


# endregion handling media post flight
