"""A wrapper around the Beaker CLI."""

import json
import logging
from pathlib import Path
import subprocess
import time
from xml.etree import ElementTree as ET

logger = logging.getLogger(__name__)

from broker import helpers
from broker.exceptions import BeakerBindError


def _elementree_to_dict(etree):
    """Convert an ElementTree object to a dictionary."""
    data = {}
    if etree.attrib:
        data.update(etree.attrib)
    if etree.text:
        data["text"] = etree.text
    for child in etree:
        child_data = _elementree_to_dict(child)
        if (tag := child.tag) in data:
            if not isinstance(data[tag], list):
                data[tag] = [data[tag]]
            data[tag].append(child_data)
        else:
            data[tag] = child_data
    return data


def _curate_job_info(job_info_dict):
    curated_info = {
        "job_id": "id",
        # "reservation_id": "current_reservation/recipe_id",
        "whiteboard": "whiteboard/text",
        "hostname": "recipeSet/recipe/system",
        "distro": "recipeSet/recipe/distro",
    }
    return helpers.dict_from_paths(job_info_dict, curated_info)


class BeakerBind:
    """A bind class providing a basic interface to the Beaker CLI."""

    def __init__(self, hub_url, auth="krbv", **kwargs):
        self.hub_url = hub_url
        self._base_args = ["--insecure", f"--hub={self.hub_url}"]
        if auth == "basic":
            # If we're not using system kerberos auth, add in explicit basic auth
            self.username = kwargs.pop("username", None)
            self.password = kwargs.pop("password", None)
            self._base_args.extend(
                [
                    f"--username {self.username}",
                    f"--password {self.password}",
                ]
            )
        self.__dict__.update(kwargs)

    def _exec_command(self, *cmd_args, **cmd_kwargs):
        """Execute a beaker command and return the result.

        cmd_args: Expanded into feature flags for the beaker command
        cmd_kwargs: Expanded into args and values for the beaker command
        """
        raise_on_error = cmd_kwargs.pop("raise_on_error", True)
        exec_cmd, cmd_args = ["bkr"], list(cmd_args)
        # check through kwargs and if any are True add to cmd_args
        del_keys = []
        for k, v in cmd_kwargs.items():
            if isinstance(v, bool) or v is None:
                del_keys.append(k)
            if v is True:
                cmd_args.append(f"--{k}" if not k.startswith("--") else k)
        for k in del_keys:
            del cmd_kwargs[k]
        exec_cmd.extend(cmd_args)
        exec_cmd.extend(self._base_args)
        exec_cmd.extend([f"--{k.replace('_', '-')}={v}" for k, v in cmd_kwargs.items()])
        logger.debug(f"Executing beaker command: {exec_cmd}")
        proc = subprocess.Popen(
            exec_cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        stdout, stderr = proc.communicate()
        result = helpers.Result(
            stdout=stdout.decode(),
            stderr=stderr.decode(),
            status=proc.returncode,
        )
        if result.status != 0 and raise_on_error:
            raise BeakerBindError(
                f"Beaker command failed:\nCommand={' '.join(exec_cmd)}\nResult={result}",
            )
        logger.debug(f"Beaker command result: {result.stdout}")
        return result

    def job_submit(self, job_xml, wait=False):
        """Submit a job to Beaker and optionally wait for it to complete."""
        # wait behavior seems buggy to me, so best to avoid it
        if not Path(job_xml).exists():
            raise FileNotFoundError(f"Job XML file {job_xml} not found")
        result = self._exec_command("job-submit", job_xml, wait=wait)
        if not wait:
            # get the job id from the output
            # format is "Submitted: ['J:7849837'] where the number is the job id
            for line in result.stdout.splitlines():
                if line.startswith("Submitted:"):
                    return line.split("'")[1].replace("J:", "")

    def job_watch(self, job_id):
        """Watch a job via the job-watch command. This can be buggy."""
        job_id = f"J:{job_id}" if not job_id.startswith("J:") else job_id
        return self._exec_command("job-watch", job_id)

    def job_results(self, job_id, format="beaker-results-xml", pretty=False):
        """Get the results of a job in the specified format."""
        job_id = f"J:{job_id}" if not job_id.startswith("J:") else job_id
        return self._exec_command("job-results", job_id, format=format, prettyxml=pretty)

    def job_clone(self, job_id, wait=False, **kwargs):
        """Clone a job by the specified job id."""
        job_id = f"J:{job_id}" if not job_id.startswith("J:") else job_id
        return self._exec_command("job-clone", job_id, wait=wait, **kwargs)

    def job_list(self, *args, **kwargs):
        """List jobs matching the criteria specified by args and kwargs."""
        return self._exec_command("job-list", *args, **kwargs)

    def job_cancel(self, job_id):
        """Cancel a job by the specified job id."""
        if not job_id.startswith("J:") and not job_id.startswith("RS:"):
            job_id = f"J:{job_id}"
        return self._exec_command("job-cancel", job_id)

    def job_delete(self, job_id):
        """Delete a job by the specified job id."""
        job_id = f"J:{job_id}" if not job_id.startswith("J:") else job_id
        return self._exec_command("job-delete", job_id)

    def system_release(self, system_id):
        """Release a system by the specified system id."""
        return self._exec_command("system-release", system_id)

    def system_list(self, **kwargs):
        """Due to the number of arguments, we will not validate before submitting.

        Accepted arguments are:
        available                       available to be used by this user
        free                            available to this user and not currently being used
        removed                         which have been removed
        mine                            owned by this user
        type=TYPE                       of TYPE
        status=STATUS                   with STATUS
        pool=POOL                       in POOL
        arch=ARCH                       with ARCH
        dev-vendor-id=VENDOR-ID         with a device that has VENDOR-ID
        dev-device-id=DEVICE-ID         with a device that has DEVICE-ID
        dev-sub-vendor-id=SUBVENDOR-ID  with a device that has SUBVENDOR-ID
        dev-sub-device-id=SUBDEVICE-ID  with a device that has SUBDEVICE-ID
        dev-driver=DRIVER               with a device that has DRIVER
        dev-description=DESCRIPTION     with a device that has DESCRIPTION
        xml-filter=XML                  matching the given XML filter
        host-filter=NAME                matching pre-defined host filter
        """
        # convert the flags passed in kwargs to arguments
        args = [
            f"--{key}" for key in ("available", "free", "removed", "mine") if kwargs.pop(key, False)
        ]
        return self._exec_command("system-list", *args, **kwargs)

    def user_systems(self):
        """Return a list of system ids owned by the current user.

        This is used for inventory syncing against Beaker.
        """
        result = self.system_list(mine=True, raise_on_error=False)
        if result.status != 0:
            return []
        else:
            return result.stdout.splitlines()

    def system_details(self, system_id, format="json"):
        """Get details about a system by the specified system id."""
        return self._exec_command("system-details", system_id, format=format)

    def execute_job(self, job, max_wait="24h"):
        """Submit a job, periodically checking the status until it completes.

        return: a dictionary of the results.
        """
        if Path(job).exists():  # job xml path passed in
            job_id = self.job_submit(job, wait=False)
        else:  # using a job id
            job_id = self.job_clone(job)
        logger.info(f"Submitted job: {job_id}")
        _max_wait = time.time() + helpers.translate_timeout(max_wait or "24h")
        while time.time() < _max_wait:
            time.sleep(60)
            result = self.job_results(job_id, pretty=True)
            if 'result="Pass"' in result.stdout:
                return _curate_job_info(_elementree_to_dict(ET.fromstring(result.stdout)))
            elif 'result="Fail"' in result.stdout or "Exception: " in result.stdout:
                raise BeakerBindError(f"Job {job_id} failed:\n{result}")
            elif 'result="Warn"' in result.stdout:
                res_dict = _elementree_to_dict(ET.fromstring(result.stdout))
                raise BeakerBindError(
                    f"Job {job_id} was resulted in a warning. Status: {res_dict['status']}"
                )
        raise BeakerBindError(f"Job {job_id} did not complete within {max_wait}")

    def system_details_curated(self, system_id):
        """Return a curated dictionary of system details."""
        full_details = json.loads(self.system_details(system_id).stdout)
        curated_details = {
            "hostname": full_details["fqdn"],
            "mac_address": full_details["mac_address"],
            "owner": "{display_name} <{email_address}>".format(
                display_name=full_details["owner"]["display_name"],
                email_address=full_details["owner"]["email_address"],
            ),
            "id": full_details["id"],
        }
        if current_res := full_details.get("current_reservation"):
            curated_details.update(
                {
                    "reservation_id": current_res["recipe_id"],
                    "reserved_on": current_res.get("start_time"),
                    "expires_on": current_res.get("finish_time"),
                    "reserved_for": "{display_name} <{email_address}>".format(
                        display_name=current_res["user"]["display_name"],
                        email_address=current_res["user"]["email_address"],
                    ),
                }
            )
        return curated_details

    def jobid_from_system(self, system_hostname):
        """Return the job id for the current reservation on the system."""
        for job_id in json.loads(self.job_list(mine=True).stdout):
            job_result = self.job_results(job_id, pretty=True)
            job_detail = _curate_job_info(_elementree_to_dict(ET.fromstring(job_result.stdout)))
            if job_detail["hostname"] == system_hostname:
                return job_id
