import copy
import hashlib
import json
import logging
import os
import uuid

import jq
import jsonpointer
import yaml

from yadageschemas.utils import WithJsonRefEncoder


class outputReference(object):
    def __init__(self, stepid, pointer):
        self.stepid = stepid
        self.pointer = pointer

    def __repr__(self):
        return "outputReference {}#{}".format(self.stepid, self.pointer.path)

    # (de-)serialization
    @classmethod
    def fromJSON(cls, data):
        return cls(data["stepid"], jsonpointer.JsonPointer(data["pointer_path"]))

    def json(self):
        return {"stepid": self.stepid, "pointer_path": self.pointer.path}


log = logging.getLogger(__name__)

DEFAULT_ID_METHOD = "uuid"


def json_hash(jsonable):
    """
    returns a content-based hash of a json-able structure (serialized with WithJsonRefEncoder)

    :param jsonable: a json-serializable object
    :return: the hash
    """
    the_hash = hashlib.sha1(
        json.dumps(jsonable, cls=WithJsonRefEncoder, sort_keys=True).encode("utf-8")
    ).hexdigest()
    return the_hash


def get_id_fromjson(jsonobject, method=DEFAULT_ID_METHOD):
    method = os.environ.get("YADAGE_ID_METHOD", method)
    if method == "uuid":
        return str(uuid.uuid4())
    elif method == "jsonhash":
        return json_hash(jsonobject)
    else:
        raise NotImplementedError("unkown id generation method {}".format(method))


def get_obj_id(obj_with_json_method, method=DEFAULT_ID_METHOD):
    return get_id_fromjson(obj_with_json_method.json(), method)


def process_refs(x, dag):
    if type(x) == dict:
        for k, v in x.items():
            x[k] = process_refs(v, dag)
        return x
    elif type(x) == list:
        for i, e in enumerate(x):
            x[i] = process_refs(e, dag)
        return x
    elif type(x) == outputReference:
        return x.pointer.resolve(dag.getNode(x.stepid).result)
    else:
        return x


def leaf_iterator_jsonlike(jsonlike, path=None):
    """'
    :param jsonlike: a jsonlike object (i.e. nested lists/arrays: leaf-types must not be JSONable)
    :return: iterator that yields (JSON path, leaf value) tuples
    """
    path = path or []
    if type(jsonlike) == list:
        for i, x in enumerate(jsonlike):
            thispath = copy.deepcopy(path)
            thispath.append(i)
            for leaf in leaf_iterator_jsonlike(x, path=thispath):
                yield leaf
    elif type(jsonlike) == dict:
        for k, v in jsonlike.items():
            thispath = copy.deepcopy(path)
            thispath.append(k)
            for leaf in leaf_iterator_jsonlike(v, path=thispath):
                yield leaf
    else:
        yield jsonpointer.JsonPointer.from_parts(path), jsonlike


def process_jsonlike(jsonlike, jq_obj_selector, callback):
    wflowrefs = [
        jsonpointer.JsonPointer.from_parts(x[1:])
        for x in jq.jq(
            "paths(if objects then {} else false end)".format(jq_obj_selector)
        ).transform({"value": jsonlike}, multiple_output=True)
    ]
    for wflowref in wflowrefs:
        value = callback(wflowref.resolve(jsonlike))
        if wflowref.path == "":
            return value
        else:
            wflowref.set(jsonlike, value)
    return jsonlike


def options_from_eqdelimstring(opts):
    options = {}
    for x in opts:
        key, value = x.split("=", 1)
        options[key] = yaml.safe_load(value)
    return options


def getinit_data(initfiles, parameters):
    """
    get initial data from both a list of files and a list of 'pname=pvalue'
    strings as they are passed in the command line <pvalue> is assumed to be a
    YAML parsable string.
    """

    initdata = {}
    for initfile in initfiles:
        log.debug("loading initialization data from file %s", initfile)
        initdata.update(**yaml.safe_load(open(initfile)))

    initdata.update(**options_from_eqdelimstring(parameters))
    return initdata


def setupbackend_fromstring(backend, backendopts=None):
    backendopts = backendopts or {}
    log.debug("setting up backend %s with opts %s", backend, backendopts)
    import yadage.backends.packtivitybackend as pb

    return pb.PacktivityBackend(
        packtivity_backendstring=backend, backendopts=backendopts
    )


def init_stage_spec(
    parameters, discover, used_inputs, name, nodename=None, relative=False
):
    return {
        "name": name,
        "dependencies": {"dependency_type": "jsonpath_ready", "expressions": []},
        "scheduler": {
            "scheduler_type": "init-stage",
            "parameters": parameters,
            "inputs": used_inputs,
            "nodename": nodename,
            "step": {
                "process": None,
                "environment": None,
                "publisher": {
                    "publisher_type": "fromparjq-pub",
                    "script": ".",
                    "tryExact": True,
                    "glob": discover,
                    "relative_paths": relative,
                },
            },
        },
    }


def stages_in_scope(workflow, scope):
    return jsonpointer.JsonPointer(scope).resolve(workflow.bookkeeping)["_meta"][
        "stages"
    ]


def rule_steps_indices(workflow):
    rule_to_steps_index = {}
    steps_to_rule_index = {}
    rule_to_subscopes_index = {}
    for rule in workflow.rules + workflow.applied_rules:
        path = "/".join([rule.offset, rule.rule.name])
        p = jsonpointer.JsonPointer(path)
        try:
            steps_of_rule = [
                x["_nodeid"] for x in p.resolve(workflow.stepsbystage) if "_nodeid" in x
            ]
        except jsonpointer.JsonPointerException:
            steps_of_rule = []

        try:
            a = p.resolve(workflow.stepsbystage)
            subscopes_of_rule = [
                # ['{}/{}'.format(x['_offset'],substage) for substage in x.keys() if not substage== '_offset' ]
                x["_offset"]
                for x in a
                if "_offset" in x
            ]
        except jsonpointer.JsonPointerException:
            subscopes_of_rule = []

        rule_to_steps_index[rule.identifier] = steps_of_rule
        rule_to_subscopes_index[rule.identifier] = subscopes_of_rule
        for step in steps_of_rule:
            steps_to_rule_index[step] = rule.identifier
    return rule_to_steps_index, steps_to_rule_index, rule_to_subscopes_index


def advance_coroutine(coroutine):
    try:
        return coroutine.next()
    except AttributeError:
        return coroutine.__next__()


def prepare_meta(metadir, accept=False):
    """
    prepare workflow meta-data directory

    :param metadir: the meta-data directory name
    :param accept: whether to accept an existing metadata directory
    """
    if os.path.exists(metadir):
        if not accept:
            raise RuntimeError(
                "yadage meta directory %s exists. Allow overwrite by using the command line option  --accept-metadir"
                % metadir
            )
    else:
        os.makedirs(metadir)


def pointerize(data, asref=False, stepid=None):
    def callback(p):
        return (
            outputReference(stepid, p)
            if asref
            else {"$wflowpointer": {"step": stepid, "result": p.path}}
            if stepid
            else p.path
        )

    return data.asrefs(callback=callback)
