# Licensed under a 3-clause BSD style license - see LICENSE.rst


# TODO: this file should be refactored to use a more thread-safe and
# race-condition-safe lockfile mechanism.

import datetime
import os
import stat
import warnings
import xmlrpc.client as xmlrpc
from contextlib import suppress
from pathlib import Path
from urllib.parse import urlparse

from astropy import log
from astropy.utils.data import get_readable_fileobj

from .errors import SAMPHubError, SAMPWarning


def read_lockfile(lockfilename):
    """
    Read in the lockfile given by ``lockfilename`` into a dictionary.
    """
    # lockfilename may be a local file or a remote URL, but
    # get_readable_fileobj takes care of this.
    lockfiledict = {}
    with get_readable_fileobj(lockfilename) as f:
        for line in f:
            if not line.startswith("#"):
                kw, val = line.split("=")
                lockfiledict[kw.strip()] = val.strip()
    return lockfiledict


def write_lockfile(lockfilename, lockfiledict):
    lockfile = open(lockfilename, "w")
    lockfile.close()
    os.chmod(lockfilename, stat.S_IREAD + stat.S_IWRITE)

    lockfile = open(lockfilename, "w")
    now_iso = datetime.datetime.now().isoformat()
    lockfile.write(f"# SAMP lockfile written on {now_iso}\n")
    lockfile.write("# Standard Profile required keys\n")
    lockfile.writelines(f"{key}={value}\n" for key, value in lockfiledict.items())
    lockfile.close()


def create_lock_file(lockfilename=None, mode=None, hub_id=None, hub_params=None):
    # Remove lock-files of dead hubs
    remove_garbage_lock_files()

    lockfiledir = ""

    # CHECK FOR SAMP_HUB ENVIRONMENT VARIABLE
    if "SAMP_HUB" in os.environ:
        # For the time being I assume just the std profile supported.
        if os.environ["SAMP_HUB"].startswith("std-lockurl:"):
            lockfilename = os.environ["SAMP_HUB"][len("std-lockurl:") :]
            lockfile_parsed = urlparse(lockfilename)

            if lockfile_parsed[0] != "file":
                warnings.warn(
                    f"Unable to start a Hub with lockfile {lockfilename}. "
                    "Start-up process aborted.",
                    SAMPWarning,
                )
                return False
            else:
                lockfilename = lockfile_parsed[2]
    else:
        # If it is a fresh Hub instance
        if lockfilename is None:
            log.debug("Running mode: " + mode)

            if mode == "single":
                lockfilename = os.path.join(Path.home(), ".samp")
            else:
                lockfiledir = os.path.join(Path.home(), ".samp-1")

                # If missing create .samp-1 directory
                try:
                    os.mkdir(lockfiledir)
                except OSError:
                    pass  # directory already exists
                finally:
                    os.chmod(lockfiledir, stat.S_IREAD + stat.S_IWRITE + stat.S_IEXEC)

                lockfilename = os.path.join(lockfiledir, f"samp-hub-{hub_id}")

        else:
            log.debug("Running mode: multiple")

    hub_is_running, lockfiledict = check_running_hub(lockfilename)

    if hub_is_running:
        warnings.warn(
            "Another SAMP Hub is already running. Start-up process aborted.",
            SAMPWarning,
        )
        return False

    log.debug("Lock-file: " + lockfilename)

    write_lockfile(lockfilename, hub_params)

    return lockfilename


def get_main_running_hub():
    """
    Get either the hub given by the environment variable SAMP_HUB, or the one
    given by the lockfile .samp in the user home directory.
    """
    hubs = get_running_hubs()

    if not hubs:
        raise SAMPHubError("Unable to find a running SAMP Hub.")

    # CHECK FOR SAMP_HUB ENVIRONMENT VARIABLE
    if "SAMP_HUB" in os.environ:
        # For the time being I assume just the std profile supported.
        if os.environ["SAMP_HUB"].startswith("std-lockurl:"):
            lockfilename = os.environ["SAMP_HUB"][len("std-lockurl:") :]
        else:
            raise SAMPHubError("SAMP Hub profile not supported.")
    else:
        lockfilename = os.path.join(Path.home(), ".samp")

    return hubs[lockfilename]


def get_running_hubs():
    """
    Return a dictionary containing the lock-file contents of all the currently
    running hubs (single and/or multiple mode).

    The dictionary format is:

    ``{<lock-file>: {<token-name>: <token-string>, ...}, ...}``

    where ``{<lock-file>}`` is the lock-file name, ``{<token-name>}`` and
    ``{<token-string>}`` are the lock-file tokens (name and content).

    Returns
    -------
    running_hubs : dict
        Lock-file contents of all the currently running hubs.
    """
    hubs = {}
    lockfilename = ""

    # HUB SINGLE INSTANCE MODE

    # CHECK FOR SAMP_HUB ENVIRONMENT VARIABLE
    if "SAMP_HUB" in os.environ:
        # For the time being I assume just the std profile supported.
        if os.environ["SAMP_HUB"].startswith("std-lockurl:"):
            lockfilename = os.environ["SAMP_HUB"][len("std-lockurl:") :]
    else:
        lockfilename = os.path.join(Path.home(), ".samp")

    hub_is_running, lockfiledict = check_running_hub(lockfilename)

    if hub_is_running:
        hubs[lockfilename] = lockfiledict

    # HUB MULTIPLE INSTANCE MODE

    lockfiledir = ""

    lockfiledir = os.path.join(Path.home(), ".samp-1")

    if os.path.isdir(lockfiledir):
        for filename in os.listdir(lockfiledir):
            if filename.startswith("samp-hub"):
                lockfilename = os.path.join(lockfiledir, filename)
                hub_is_running, lockfiledict = check_running_hub(lockfilename)
                if hub_is_running:
                    hubs[lockfilename] = lockfiledict

    return hubs


def check_running_hub(lockfilename):
    """
    Test whether a hub identified by ``lockfilename`` is running or not.

    Parameters
    ----------
    lockfilename : str
        Lock-file name (path + file name) of the Hub to be tested.

    Returns
    -------
    is_running : bool
        Whether the hub is running
    hub_params : dict
        If the hub is running this contains the parameters from the lockfile
    """
    is_running = False
    lockfiledict = {}

    # Check whether a lockfile already exists
    try:
        lockfiledict = read_lockfile(lockfilename)
    except OSError:
        return is_running, lockfiledict

    if "samp.hub.xmlrpc.url" in lockfiledict:
        try:
            proxy = xmlrpc.ServerProxy(
                lockfiledict["samp.hub.xmlrpc.url"].replace("\\", ""), allow_none=1
            )
            proxy.samp.hub.ping()
            is_running = True
        except xmlrpc.ProtocolError:
            # There is a protocol error (e.g. for authentication required),
            # but the server is alive
            is_running = True
        except OSError:
            pass

    return is_running, lockfiledict


def remove_garbage_lock_files():
    lockfilename = ""

    # HUB SINGLE INSTANCE MODE

    lockfilename = os.path.join(Path.home(), ".samp")

    hub_is_running, lockfiledict = check_running_hub(lockfilename)

    if not hub_is_running:
        # If lockfilename belongs to a dead hub, then it is deleted
        if os.path.isfile(lockfilename):
            with suppress(OSError):
                os.remove(lockfilename)

    # HUB MULTIPLE INSTANCE MODE

    lockfiledir = os.path.join(Path.home(), ".samp-1")

    if os.path.isdir(lockfiledir):
        for filename in os.listdir(lockfiledir):
            if filename.startswith("samp-hub"):
                lockfilename = os.path.join(lockfiledir, filename)
                hub_is_running, lockfiledict = check_running_hub(lockfilename)
                if not hub_is_running:
                    # If lockfilename belongs to a dead hub, then it is deleted
                    if os.path.isfile(lockfilename):
                        with suppress(OSError):
                            os.remove(lockfilename)
