import logging
import os
import shutil
from six import string_types
import zipfile
from packtivity.statecontexts import load_state
from packtivity.statecontexts.posixfs_context import LocalFSState
import fnmatch

log = logging.getLogger(__name__)


import requests

try:
    # For Python 3.0 and later
    from urllib.request import urlopen
except ImportError:
    # Fall back to Python 2's urllib2
    from urllib2 import urlopen


def download_file(url, local_filename):
    if not "http" in url:
        f = urlopen(url)
        with open(local_filename, "wb") as lf:
            lf.write(f.read())
    else:
        h = (
            {}
            if not "YADAGE_INIT_TOKEN" in os.environ
            else {"PRIVATE-TOKEN": os.environ["YADAGE_INIT_TOKEN"]}
        )
        r = requests.get(url, stream=True, headers=h)
        with open(local_filename, "wb") as f:
            shutil.copyfileobj(r.raw, f)


def prepare_workdir_from_archive(initdir, inputarchive, match=None):
    url = inputarchive
    os.makedirs(initdir)
    localzipfile = "{}/.yadage_inputarchive.zip".format(initdir)
    download_file(url, localzipfile)

    if not match:
        with zipfile.ZipFile(localzipfile) as zf:
            zf.extractall(path=initdir)
    else:
        assert match.endswith("/")
        with zipfile.ZipFile(localzipfile) as zf:
            filtered = fnmatch.filter(zf.namelist(), match)
            assert len(filtered) == 1
            basepath = filtered[0]
            for x in zf.namelist():
                if x != basepath and basepath in x:
                    target = x.replace(basepath, "")
                    if target.endswith("/"):
                        os.makedirs(os.path.join(initdir, target))
                    else:
                        with open(os.path.join(initdir, target), "wb") as f:
                            f.write(zf.read(x))
    os.remove(localzipfile)


def _merge_states(lhs, rhs):
    return LocalFSState(lhs.readwrite + rhs.readwrite, lhs.readonly + rhs.readonly)


class LocalFSProvider(object):
    def __init__(self, *base_states, **kwargs):
        base_states = list(base_states)
        self.nest = kwargs.get("nest", True)
        self.ensure = kwargs.get("ensure")
        self.init_states = kwargs.get("init_states")
        self.sub_inits = kwargs.get("sub_init_states", {})

        first = base_states.pop()
        assert first

        self.base = first

        while base_states:
            next_state = base_states.pop()
            if not next_state:
                continue
            self.base = _merge_states(self.base, next_state)

    def new_provider(self, name, init_states=None):
        new_base_ro = self.base.readwrite + self.base.readonly
        new_base_rw = [os.path.join(self.base.readwrite[0], name)]

        sub_init = self.sub_inits.get(name, {}).get("_data")
        sub_inits = [sub_init] if sub_init else []
        init_states = init_states or []

        newsinits = {
            k: v for k, v in self.sub_inits.get(name, {}).items() if k != "_data"
        }
        return LocalFSProvider(
            LocalFSState(new_base_rw, new_base_ro),
            nest=self.nest,
            ensure=self.ensure,
            init_states=init_states + sub_inits,
            sub_init_states=newsinits,
        )

    def new_state(self, name, dependencies, readonly=False):
        """
        creates a new context from an existing context.

        if subdir is True it declares a new read-write nested under the old
        context's read-write and adds all read-write and read-only locations
        of the old context as read-only. This is recommended as it makes rolling
        back changes to the global state made in this context easy.

        else the same readwrite/readonly configuration as the parent context is used
        """

        readwrites = []
        if not readonly:
            readwrites = (
                ["{}/{}".format(self.base.readwrite[0], name)]
                if self.nest
                else self.base.readwrite
            )

        log.debug("new state is: rw: %s", readwrites)
        state_identifier = name.replace("/", "_")  # replace in case name is nested path
        newstate = LocalFSState(
            readwrite=readwrites, identifier=state_identifier, dependencies=dependencies
        )

        if not readonly and self.ensure:
            newstate.ensure()

        return newstate

    def json(self):
        def subinit_json(subinits):
            def handle(k, v):
                if k == "_data":
                    return v.json() if v else None
                return {kk: handle(kk, vv) for kk, vv in v.items()}

            return {k: handle(k, v) for k, v in subinits.items()}

        d = {
            "state_provider_type": "localfs_provider",
            "base_state": self.base.json(),
            "init_states": [s.json() for s in self.init_states]
            if self.init_states
            else [],
            "nest": self.nest,
            "ensure": self.ensure,
            "sub_init_states": subinit_json(self.sub_inits),
        }
        return d

    @classmethod
    def fromJSON(cls, jsondata, deserialization_opts):
        def subinit_load(subinit_data):
            def handle(k, v):
                if k == "_data":
                    return load_state(v, deserialization_opts) if v else None
                return {kk: handle(kk, vv) for kk, vv in v.items()}

            return {k: handle(k, v) for k, v in subinit_data.items()}

        instance = cls(
            load_state(jsondata["base_state"], deserialization_opts),
            nest=jsondata["nest"],
            ensure=jsondata["ensure"],
            init_states=[
                load_state(x, deserialization_opts) for x in jsondata["init_states"]
            ],
            sub_init_states=subinit_load(jsondata["sub_init_states"]),
        )
        return instance


def setup_provider(dataarg, dataopts):
    workdir = dataarg.split(":", 2)[1]
    read = dataopts.get("read", None)
    nest = dataopts.get("nest", True)
    ensure = dataopts.get("ensure", True)
    overwrite = dataopts.get("overwrite", False)
    subinits = dataopts.get("subinits", {})
    pathbase = dataopts.get("pathbase")

    if overwrite and os.path.exists(workdir):
        log.info("work directory %s will be purged and overwritten", workdir)
        shutil.rmtree(workdir)

    init_states = []

    initdir = os.path.join(pathbase or workdir, dataopts.get("initdir", "init"))
    inputarchive = dataopts.get("inputarchive", None)
    if initdir:
        if inputarchive:
            prepare_workdir_from_archive(
                initdir, inputarchive, match=dataopts.get("archivematch")
            )
        init_state = LocalFSState(readonly=[initdir])
        init_states.append(init_state)

    def handle_init_spec(pathbase, spec):
        subinits = {}
        for k, v in spec.items():
            if isinstance(v, string_types):
                subinits[k] = {
                    "_data": LocalFSState(readonly=[os.path.join(pathbase, v)])
                }
            if isinstance(v, dict):
                subinits.setdefault(k, {})["_data"] = (
                    LocalFSState(readonly=[os.path.join(pathbase, v.pop("_data"))])
                    if "_data" in v
                    else None
                )
                for kk, vv in handle_init_spec(pathbase, v).items():
                    subinits.setdefault(k, {})[kk] = vv
        return subinits

    if subinits:
        subinits = handle_init_spec(pathbase or workdir, subinits)

    writable_state = LocalFSState([workdir])
    return LocalFSProvider(
        read,
        writable_state,
        ensure=ensure,
        nest=nest,
        init_states=init_states,
        sub_init_states=subinits,
    )
