import hashlib
import logging
import os
import re
import tarfile
import tempfile
from collections import defaultdict
from distutils.dir_util import copy_tree
from functools import partial
from shutil import copyfile
from zipfile import ZipFile

from kapitan.errors import GitSubdirNotFoundError
from kapitan.utils import make_request

from git import Repo

logger = logging.getLogger(__name__)

# this is used just to change the root directory of save_dir in testing
DEPENDENCY_OUTPUT_CONFIG = {}


def fetch_dependencies(target_objs, pool):
    """
    parses through the dependencies parameters in target_objs and fetches them
    all dependencies are first fetched into tmp dir, after which they are copied to their respective output_path.
    dependencies whose output_path already exists are skipped
    """
    temp_dir = tempfile.mkdtemp()
    # there could be multiple dependency items per source_uri due to reclass inheritance or
    # other user requirements. So create a mapping from source_uri to a set of dependencies with
    # that source_uri
    git_deps = defaultdict(list)
    http_deps = defaultdict(list)

    # this dict is to make sure no duplicated output_paths exist per source
    deps_output_paths = defaultdict(set)
    for target_obj in target_objs:
        try:
            dependencies = target_obj["dependencies"]
            for item in dependencies:
                dependency_type = item["type"]
                source_uri = item["source"]
                if "root_dir" in DEPENDENCY_OUTPUT_CONFIG:
                    # should only be used in test environment
                    item["output_path"] = os.path.join(DEPENDENCY_OUTPUT_CONFIG["root_dir"], item["output_path"])
                output_path = item["output_path"]
                if os.path.exists(os.path.abspath(output_path)) and not item.get('unpack', False):
                    # if unpack: True, then allow the user to specify the parent directory as output_path where the
                    # files will be extracted to
                    logger.info("Dependency {} : already exists. Ignoring".format(output_path))
                    continue

                if output_path in deps_output_paths[source_uri]:
                    # if the output_path is duplicated for the same source_uri
                    continue
                else:
                    deps_output_paths[source_uri].add(output_path)

                if dependency_type == "git":
                    git_deps[source_uri].append(item)
                elif dependency_type in ("http", "https"):
                    http_deps[source_uri].append(item)

        except KeyError:
            continue

    git_worker = partial(fetch_git_dependency, save_dir=temp_dir)
    http_worker = partial(fetch_http_dependency, save_dir=temp_dir)
    [p.get() for p in pool.imap_unordered(http_worker, http_deps.items()) if p]
    [p.get() for p in pool.imap_unordered(git_worker, git_deps.items()) if p]


def fetch_git_dependency(dep_mapping, save_dir):
    """
    fetches a git repository at source into save_dir, and copy the repository into output_path
    ref is used to checkout if exists
    only subdir is copied into output_path if specified
    """
    source, deps = dep_mapping
    fetch_git_source(source, save_dir)
    for dep in deps:
        repo_path = os.path.join(save_dir, os.path.basename(source))
        repo = Repo(repo_path)
        output_path = dep["output_path"]
        copy_src_path = repo_path
        if 'ref' in dep:
            ref = dep['ref']
            repo.git.checkout(ref)
        else:
            repo = Repo(repo_path)
            repo.git.checkout("master")  # default ref

        if 'subdir' in dep:
            sub_dir = dep['subdir']
            full_subdir = os.path.join(repo_path, sub_dir)
            if os.path.isdir(full_subdir):
                copy_src_path = full_subdir
            else:
                raise GitSubdirNotFoundError("Dependency {} : subdir {} not found in repo".format(source, sub_dir))

        if not os.path.exists(os.path.abspath(output_path)):
            copy_tree(copy_src_path, output_path)
            logger.info("Dependency {} : saved to {}".format(source, output_path))


def fetch_git_source(source, save_dir):
    """clones a git repository at source and saves into save_dir"""
    logger.info("Dependency {} : fetching now".format(source))
    Repo.clone_from(source, os.path.join(save_dir, os.path.basename(source)))
    logger.info('Dependency {} : successfully fetched'.format(source))


def fetch_http_dependency(dep_mapping, save_dir):
    """
    fetches a http[s] file at source and saves into save_dir, after which it is copied into
    the output_path stored in dep_mapping
    """
    source, deps = dep_mapping
    content_type = fetch_http_source(source, save_dir)
    path_hash = hashlib.sha256(os.path.dirname(source).encode()).hexdigest()[:8]
    copy_src_path = os.path.join(save_dir, path_hash + os.path.basename(source))
    for dep in deps:
        output_path = dep["output_path"]
        if dep.get('unpack', False):
            # ensure that the directory we are extracting to exists
            os.makedirs(output_path, exist_ok=True)
            is_unpacked = False
            if content_type == 'application/x-tar':
                tar = tarfile.open(copy_src_path)
                tar.extractall(path=output_path)
                tar.close()
                is_unpacked = True
            elif content_type == 'application/zip':
                zfile = ZipFile(copy_src_path)
                zfile.extractall(output_path)
                zfile.close()
                is_unpacked = True
            elif content_type in ['application/octet-stream', 'application/x-gzip']:
                if re.search(r'(\.tar\.gz|\.tgz)$', source):
                    tar = tarfile.open(copy_src_path)
                    tar.extractall(path=output_path)
                    tar.close()
                    is_unpacked = True
            if is_unpacked:
                logger.info("Dependency {} : extracted to {}".format(source, output_path))
            else:
                logger.info("Dependency {} : Content-Type {} is not supported for unpack. Ignoring save".
                            format(source, content_type))
        else:
            # we are downloading a single file
            parent_dir = os.path.dirname(output_path)
            if parent_dir != '':
                os.makedirs(parent_dir, exist_ok=True)
            copyfile(copy_src_path, output_path)
            logger.info("Dependency {} : saved to {}".format(source, output_path))


def fetch_http_source(source, save_dir):
    """downloads a http[s] file from source and saves into save_dir"""
    logger.info("Dependency {} : fetching now".format(source))
    content, content_type = make_request(source)
    logger.info("Dependency {} : successfully fetched".format(source))
    if content is not None:
        basename = os.path.basename(source)
        # to avoid collisions between basename(source)
        path_hash = hashlib.sha256(os.path.dirname(source).encode()).hexdigest()[:8]
        full_save_path = os.path.join(save_dir, path_hash + basename)
        with open(full_save_path, 'wb') as f:
            f.write(content)
        return content_type
    else:
        logger.warning("Dependency {} : failed to fetch".format(source))
        return None
