import copy
import logging
import os.path
import re
import xml.etree.ElementTree as ET
from typing import Union

from remotemanager.connection.computers import BaseComputer, Substitution

logger = logging.getLogger(__name__)


class JUBETemplate(BaseComputer):
    """Extends BaseComputer to provide compatibility with JUBE platforms"""

    def __init__(self, template: str, platform: str):

        if os.path.exists(template):
            with open(template) as o:
                template = o.read()

        self._platform_path = ""
        self.platform_path = platform

        super().__init__(template=template)

    @classmethod
    def from_repo(
        cls,
        name: str,
        branch: str = "develop",
        repo: str = "https://gitlab.com/max-centre/JUBE4MaX",
        platform_name: str = "platform.xml",
        template_name: str = "submit.job",
        local_dir: Union[None, str] = None,
        **url_args,
    ):
        url = os.path.join("/", branch, "max-inputs/platforms/", name)
        url = f"{repo}/-/raw" + url

        if local_dir is None:
            local_dir = os.path.split(name.strip("/"))[-1]

            if local_dir == "" or local_dir is None:
                local_dir = os.getcwd()

        print(f"searching for {platform_name} & {template_name} at {url}")

        platform_path = os.path.join(local_dir, platform_name)
        cls.download_file(os.path.join(url, platform_name), platform_path)
        template_path = os.path.join(local_dir, template_name)
        cls.download_file(os.path.join(url, template_name), template_path)

        return cls(template=template_path, platform=platform_path)

    @property
    def platform_path(self) -> str:
        """Path to the platform file"""
        return self._platform_path

    @platform_path.setter
    def platform_path(self, path: str):
        if not os.path.exists(path):
            raise ValueError(f"Platform file {path} not found")

        self._platform_path = path

    def _extract_subs(self, link: bool = True) -> None:
        """
        Overrides BaseComputer sub extraction to instead parse from platform.xml
        """
        super()._extract_subs(link=False)
        logger.debug("generating subs specified in platform.xml")
        tree = ET.parse(self.platform_path)
        root = tree.getroot()
        # platform.xml specifies "substitutesets" which translate what's
        # in the template to the actual parameter name. Apply these changes here.
        empty = {}
        for subsituteset in root.findall("substituteset"):
            for change in subsituteset.iter():
                if change.tag != "sub":
                    logger.debug("skip row %s", change)
                    continue
                source = change.attrib["source"].strip("#").lower()
                dest = change.attrib["dest"].strip("$")

                if dest is None or dest == "":
                    # store this attribute for reporting if the destination is empty
                    empty[source] = change.attrib
                    continue

                if source in empty:
                    # if the source was previously flagged as missing a destination,
                    # but has one now, clear that flag here
                    del empty[source]

                # don't make unnecessary changes
                if source == dest:
                    continue

                if source in self.substitution_dict:

                    logger.debug("updating target %s to %s", source, dest)
                    obj = self.substitution_dict[source]

                    delattr(self, obj.name)

                    obj.target = dest
                    obj._name = dest

                    setattr(self, obj.name, obj)

        for attrib in empty.values():
            print(f"empty destination for {attrib}")

        cache = {}
        for parameterset in root.findall("parameterset"):
            for parameter in parameterset.iter():
                name = parameter.attrib["name"]
                mode = parameter.attrib.get("mode", None)

                default = parameter.text

                cache[name] = {"mode": mode, "default": default}

        for name, args in cache.items():
            default = args["default"]
            mode = args["mode"]

            if isinstance(default, str):
                for tmp in cache:
                    if f"${tmp}" in default:
                        mode = "python"

                default = default.strip()

            sub = Substitution(
                name=name, mode=mode, target=f"#{name}#", default=default
            )
            if hasattr(self, sub.name):
                delattr(self, sub.name)
            setattr(self, sub.name, sub)

    def reformat_template(self, platform: Union[None, str] = None) -> str:
        """
        converts the original JUBE template to one that Substitutions can interact with
        """

        if platform is not None:
            self.platform_path = platform

        template = copy.deepcopy(self.template)

        tree = ET.parse(self.platform_path)
        root = tree.getroot()
        for substituteset in root.findall("substituteset"):
            for substitution in substituteset.iter():
                if "source" in substitution.attrib:
                    source = substitution.attrib["source"]
                    dest = substitution.attrib.get("dest", None)

                    if dest is not None:
                        dest = f"#{dest.strip('$')}#"

                        template = template.replace(source, dest)

        return template

    def get_unevaluated_links(self, sub: Substitution) -> list:
        """
        Performs a regex search for unlinked JUBE $parameters within a value

        Args:
            sub: Substitution object to check
        """
        # only strings are valid targets
        if not isinstance(sub.value, str):
            return []
        # can lazily ignore any value not containing a $ char
        if "$" not in sub.value:
            return []
        # need to check for exact links using regex
        # this avoids bash $evaluations raising false positives
        cache = []
        for name in self.arguments:
            search = r"\$" + name + r"\b"
            if re.search(search, sub.value) is not None:
                cache.append(name)
        return cache

    def link_sub(self, sub: Substitution) -> None:
        """Performs recursive linking for target sub"""
        links = self.get_unevaluated_links(sub)

        # if the sub is fully linked, attempt an evaluation and return
        if len(links) == 0:
            logger.debug("%s has no children, evaluating and completing", sub.name)
            # set the temporary value, so these links do not permanently update values
            sub.temporary_value = attempt_eval(sub.value)
            return

        logger.debug("Performing recursive linking for sub %s", sub.name)
        # otherwise, we need to recursively continue
        value = sub.value  # store the value for treatment
        for name in links:
            logger.debug("\tevaluating child %s", name)
            obj = self.argument_dict[name]  # get the actual associated object
            # if this sub child object has children of itself, recurse the call
            sub_links = self.get_unevaluated_links(obj)  # search for any children
            if len(sub_links) != 0:
                logger.debug("\tlinked to: %s, recursing...", sub_links)
                self.link_sub(obj)
            # recursion ensures that this child has a valid value, collect it
            update = str(obj.value)
            # search and replace an exact match
            search = r"\$" + name + r"\b"
            # search for the exact string, continuing if not found
            # this prevents $tasks matching $taskspernode, for example
            if re.search(search, value) is None:
                continue
            value = re.sub(search, update, value)
        # set the temporary value
        sub.temporary_value = attempt_eval(value)

    def link_jube_subs(self) -> None:
        """Ensures proper linking of JUBE substitutions"""
        for sub in self.substitution_objects:
            self.link_sub(sub)

    def script(self, empty_treatment="local", **kwargs) -> str:
        """
        Performs a translation evaluation step
        before calling the BaseComputer script() method
        """
        for k, v in kwargs.items():
            if k in self.arguments:
                self.argument_dict[k].temporary_value = v

        # ensure substitutions are linked
        self.link_jube_subs()

        # update the template
        self._template = self.reformat_template()

        return super().script(empty_treatment=empty_treatment)


def attempt_eval(value: str):
    """Attempt to evaluate `value`"""
    try:
        return eval(value)
    except Exception:
        return value
