import atexit
import logging
import os
import re
import signal
import subprocess

import psutil
from django.template.loader import render_to_string

from aircox.conf import settings

from ..connector import Connector
from .sources import PlaylistSource, QueueSource


__all__ = ("Streamer",)

logger = logging.getLogger("aircox")


class Streamer:
    connector = None
    process = None

    station = None
    template_name = "aircox_streamer/scripts/station.liq"
    path = None
    """Config path."""
    sources = None
    """List of all monitored sources."""
    source = None
    """Current source being played on air."""
    # note: we disable on_air rids since we don't have use of it for the
    # moment
    # on_air = None
    # """ On-air request ids (rid) """
    inputs = None
    """Queryset to input ports."""
    outputs = None
    """Queryset to output ports."""

    def __init__(self, station, connector=None):
        self.station = station
        self.inputs = self.station.port_set.active().input()
        self.outputs = self.station.port_set.active().output()

        self.id = self.station.slug.replace("-", "_")
        self.path = os.path.join(station.path, "station.liq")
        self.connector = connector or Connector(
            os.path.join(station.path, "station.sock")
        )
        self.init_sources()

    @property
    def socket_path(self):
        """Path to Unix socket file."""
        return self.connector.address

    @property
    def is_ready(self):
        """If external program is ready to use, returns True."""
        return self.send("list") != ""

    @property
    def is_running(self):
        """True if holds a running process."""
        if self.process is None:
            return False

        returncode = self.process.poll()
        if returncode is None:
            return True

        self.process = None
        logger.debug("process died with return code %s" % returncode)
        return False

    @property
    def playlists(self):
        return (s for s in self.sources if isinstance(s, PlaylistSource))

    @property
    def queues(self):
        return (s for s in self.sources if isinstance(s, QueueSource))

    # Sources and config ###############################################
    def send(self, *args, **kwargs):
        return self.connector.send(*args, **kwargs) or ""

    def init_sources(self):
        streams = self.station.program_set.filter(stream__isnull=False)
        self.dealer = QueueSource(self, "dealer")
        self.sources = [self.dealer] + [
            PlaylistSource(self, program=program) for program in streams
        ]

    def make_config(self):
        """Make configuration files and directory (and sync sources)"""
        data = render_to_string(
            self.template_name,
            {
                "station": self.station,
                "streamer": self,
                "settings": settings,
            },
        )
        data = re.sub("[\t ]+\n", "\n", data)
        data = re.sub("\n{3,}", "\n\n", data)

        os.makedirs(os.path.dirname(self.path), exist_ok=True)
        with open(self.path, "w+") as file:
            file.write(data)

        self.sync()

    def sync(self):
        """Sync all sources."""
        for source in self.sources:
            source.sync()

    def fetch(self):
        """Fetch data from liquidsoap."""
        for source in self.sources:
            source.fetch()

        # request.on_air is not ordered: we need to do it manually
        self.source = next(
            iter(
                sorted(
                    (
                        source
                        for source in self.sources
                        if source.request_status == "playing"
                        and source.air_time
                    ),
                    key=lambda o: o.air_time,
                    reverse=True,
                )
            ),
            None,
        )

    # Process ##########################################################
    def get_process_args(self):
        return ["liquidsoap", "-v", self.path]

    def check_zombie_process(self):
        if not os.path.exists(self.socket_path):
            return

        conns = [
            conn
            for conn in psutil.net_connections(kind="unix")
            if conn.laddr == self.socket_path
        ]
        for conn in conns:
            if conn.pid is not None:
                os.kill(conn.pid, signal.SIGKILL)

    def run_process(self):
        """Execute the external application with corresponding informations.

        This function must make sure that all needed files have been
        generated.
        """
        if self.process:
            return

        args = self.get_process_args()
        if not args:
            return

        self.check_zombie_process()
        self.process = subprocess.Popen(args, stderr=subprocess.STDOUT)
        atexit.register(self.kill_process)

    def kill_process(self):
        if self.process:
            logger.debug(
                "kill process %s: %s",
                self.process.pid,
                " ".join(self.get_process_args()),
            )
            self.process.kill()
            self.process = None
            atexit.unregister(self.kill_process)

    def wait_process(self):
        """Wait for the process to terminate if there is a process."""
        if self.process:
            self.process.wait()
            self.process = None
