import threading
import logging
import pathlib
import shutil
from glob import glob

from pyanaconda.core import constants
from pyanaconda.threading import threadMgr
from pykickstart.errors import KickstartValueError

from org_fedora_oscap import data_fetch
from org_fedora_oscap import common
from org_fedora_oscap import content_handling

log = logging.getLogger("anaconda")


def is_network(scheme):
    return any(
        scheme.startswith(net_prefix)
        for net_prefix in data_fetch.NET_URL_PREFIXES)


class Model:
    CONTENT_DOWNLOAD_LOCATION = pathlib.Path(common.INSTALLATION_CONTENT_DIR) / "content-download"
    TAILORING_DOWNLOAD_LOCATION = pathlib.Path(common.INSTALLATION_CONTENT_DIR) / "tailoring-download"

    CONTENT_READY_LOCATION = pathlib.Path(common.INSTALLATION_CONTENT_DIR) / "content-ready"
    TAILORING_READY_LOCATION = pathlib.Path(common.INSTALLATION_CONTENT_DIR) / "tailoring-ready"

    def __init__(self, policy_data):
        self.content_uri_scheme = ""
        self.content_uri_path = ""
        self.fetched_content = ""

        self.tailoring_uri_scheme = ""
        self.tailoring_uri_path = ""
        self.fetched_tailoring = ""

        self.activity_lock = threading.Lock()
        self.now_fetching_or_processing = False

        self.CONTENT_DOWNLOAD_LOCATION.mkdir(parents=True, exist_ok=True)
        self.TAILORING_DOWNLOAD_LOCATION.mkdir(parents=True, exist_ok=True)

        self.CONTENT_READY_LOCATION.mkdir(parents=True, exist_ok=True)
        self.TAILORING_READY_LOCATION.mkdir(parents=True, exist_ok=True)

        # self.have_system_content = self._get_system_content()
        self.policy_data = policy_data

        self.content_action = self.use_current_content
        self.files_dir = pathlib.Path(common.INSTALLATION_CONTENT_DIR)
        self.files_dir.mkdir(parents=True, exist_ok=True)

    def _get_system_content(self):
        if common.ssg_available():
            return common.get_ssg_path()

    def use_current_content(self):
        pass

    def use_downloaded_content(self):
        datastream = pathlib.Path(self.downloaded_files_content.xccdf)
        dest = self.files_dir / datastream.name
        datastream.replace(dest)
        self.policy_data.content_type = "datastream"
        self.policy_data._content_path = str(dest)

    def use_system_content(self):
        self.policy_data.content_type = "scap-security-guide"
        self.policy_data._content_path = self._get_system_content()

    def stage_stuff(self):
        self.content_action()

    def get_content_type(self, url):
        if url.endswith(".rpm"):
            return "rpm"
        elif any(url.endswith(arch_type) for arch_type in common.SUPPORTED_ARCHIVES):
            return "archive"
        else:
            return "file"

    @property
    def content_uri(self):
        return self.content_uri_scheme + "://" + self.content_uri_path

    @content_uri.setter
    def content_uri(self, uri):
        scheme, path = uri.split("://", 1)
        self.content_uri_path = path
        self.content_uri_scheme = scheme

    @property
    def tailoring_uri(self):
        return self.tailoring_uri_scheme + self.content_uri_path

    @tailoring_uri.setter
    def tailoring_uri(self, uri):
        scheme, path = uri.split("://", 1)
        self.tailoring_uri_path = path
        self.tailoring_uri_scheme = scheme

    def fetch_content(self, cert, what_if_fail):
        shutil.rmtree(self.CONTENT_DOWNLOAD_LOCATION, ignore_errors=True)
        self.CONTENT_DOWNLOAD_LOCATION.mkdir(parents=True, exist_ok=True)
        return self.fetch_files(self.content_uri_scheme, self.content_uri_path, self.CONTENT_DOWNLOAD_LOCATION, cert, what_if_fail)

    def fetch_files(self, scheme, path, destdir, cert, what_if_fail):

        with self.activity_lock:
            if self.now_fetching_or_processing:
                msg = "Strange, it seems that we are already fetching something."
                log.warn(msg)
                return
            self.now_fetching_or_processing = True

        thread_name = None
        try:
            thread_name = self._start_actual_fetch(scheme, path, destdir, cert)
        except Exception as exc:
            with self.activity_lock:
                self.now_fetching_or_processing = False
            what_if_fail(exc)

        # We are not finished yet with the fetch
        return thread_name

    def _start_actual_fetch(self, scheme, path, destdir, cert):
        thread_name = None
        url = scheme + "://" + path

        if "/" not in path:
            msg = f"Missing the path component of the '{url}' URL"
            raise KickstartValueError(msg)
        basename = path.rsplit("/", 1)[1]
        if not basename:
            msg = f"Unable to deduce basename from the '{url}' URL"
            raise KickstartValueError(msg)

        dest = destdir / basename

        if is_network(scheme):
            thread_name = common.wait_and_fetch_net_data(
                url,
                dest,
                cert
            )
        else:  # invalid schemes are handled down the road
            thread_name = common.fetch_local_data(
                url,
                dest,
            )
        return thread_name

    def finish_content_fetch(self, thread_name, fingerprint, report_callback, dest_filename, after_fetch, what_if_fail):
        """
        Args:
            what_if_fail: Callback accepting exception.
            after_fetch: Callback accepting the content class.
        """
        try:
            content = self._finish_actual_fetch(thread_name, fingerprint, report_callback, dest_filename)
        except Exception as exc:
            what_if_fail(exc)
            content = None
        finally:
            with self.activity_lock:
                self.now_fetching_or_processing = False

        after_fetch(content)

        return content

    def _verify_fingerprint(self, dest_filename, fingerprint=""):
        if fingerprint:
            hash_obj = utils.get_hashing_algorithm(fingerprint)
            digest = utils.get_file_fingerprint(dest_filename,
                                                hash_obj)
            if digest != fingerprint:
                log.error(
                    "File {dest_filename} failed integrity check - assumed a "
                    "{hash_obj.name} hash and '{fingerprint}', got '{digest}'"
                )
                msg = f"Integrity check of the content failed - {hash_obj.name} hash didn't match"
                raise content_handling.ContentCheckError(msg)

    def _finish_actual_fetch(self, wait_for, fingerprint, report_callback, dest_filename):
        threadMgr.wait(wait_for)

        log.info(f"Finished waiting for {wait_for}")
        self._verify_fingerprint(dest_filename, fingerprint)

        content = ObtainedContent(self.CONTENT_DOWNLOAD_LOCATION)
        if fingerprint:
            content.verified = dest_filename

        if not wait_for:  # then we haven't fetched anything, and we probably just want local content.
            fpaths = [f"{common.SSG_DIR}/{common.SSG_CONTENT}"]
            labelled_files = content_handling.identify_files(fpaths)
        else:
            dest_filename = pathlib.Path(dest_filename)
            # RPM is an archive at this phase
            content_type = self.get_content_type(str(dest_filename))
            if content_type in ("archive", "rpm"):
                # extract the content
                content.add_content_archive(dest_filename)
                try:
                    fpaths = common.extract_data(
                        str(dest_filename),
                        str(dest_filename.parent)
                    )
                except common.ExtractionError as err:
                    msg = f"Failed to extract the '{dest_filename}' archive: {str(err)}"
                    log.error(msg)
                    raise err

                # and populate missing fields
                labelled_files = content_handling.identify_files(fpaths)

            elif content_type == "file":
                labelled_files = content_handling.identify_files([str(dest_filename)])
            else:
                raise common.OSCAPaddonError("Unsupported content type")

        for f, l in labelled_files.items():
            content.add_file(f, l)

        return content

    def get_rule_data(self, profile_id):
        rules = common.get_fix_rules_pre(self.profile_id,
                                         self.preinst_content_path,
                                         self.datastream_id, self.xccdf_id,
                                         self.preinst_tailoring_path)

        # parse and store rules with a clean RuleData instance
        rule_data = rule_handling.RuleData()
        for rule in rules.splitlines():
            rule_data.new_rule(rule)
        return rule_data


class ObtainedContent:
    def __init__(self, root):
        self.labelled_files = dict()
        self.datastream = ""
        self.xccdf = ""
        self.oval = ""
        self.tailoring = ""
        self.archive = ""
        self.verified = ""
        self.root = pathlib.Path(root)

    def add_content_archive(self, fname):
        path = pathlib.Path(fname)
        self.labelled_files[path] = None
        self.archive = path

    def assign_content(self, attribute_name, new_value):
        old_value = getattr(self, attribute_name)
        if old_value:
            msg = f"When dealing with {attribute_name}, there was already the {old_value.name} when setting the new {new_value.name}"
            raise RuntimeError(msg)
        setattr(self, attribute_name, new_value)

    def add_file(self, fname, label):
        path = pathlib.Path(fname)
        if label == content_handling.CONTENT_TYPES["TAILORING"]:
            self.assign_content("tailoring", path)
        elif label == content_handling.CONTENT_TYPES["DATASTREAM"]:
            self.assign_content("datastream", path)
        elif label == content_handling.CONTENT_TYPES["OVAL"]:
            self.assign_content("oval", path)
        elif label == content_handling.CONTENT_TYPES["XCCDF_CHECKLIST"]:
            self.assign_content("xccdf", path)
        self.labelled_files[path] = label

    def _datastream_content(self):
        if not self.datastream:
            return None
        if not self.datastream.exists():
            return None
        return self.datastream

    def _xccdf_content(self):
        if not self.xccdf or not self.oval:
            return None
        if not (self.xccdf.exists() and self.oval.exists()):
            return None
        return self.xccdf

    def find_expected_usable_content(self, relative_main_content_path):
        content_path = self.root / relative_main_content_path
        elligible_main_content = (self._datastream_content(), self._xccdf_content())

        if content_path in elligible_main_content:
            return content_path
        else:
            msg = f"Couldn't find '{content_type}' among the available content"
            raise content_handling.ContentHandlingError(msg)

    def select_main_usable_content(self):
        elligible_main_content = (self._datastream_content(), self._xccdf_content())
        if not any(elligible_main_content):
            msg = "Couldn't find a datastream or XCCDF-OVAL file tuple among the available content"
            raise content_handling.ContentHandlingError(msg)
        if elligible_main_content[0]:
            return elligible_main_content[0]
        else:
            return elligible_main_content[1]

    def get_file_handler(self, path):
        if path == self.datastream:
            return content_handling.DataStreamHandler
        elif path == self.xccdf:
            return content_handling.BenchmarkHandler
        else:
            msg = f"We don't know '{path}' so we can't make claims regarding its handler."
            raise content_handling.ContentHandlingError(msg)
