import argparse
import configparser
import contextlib
from datetime import datetime, timezone
import difflib
import itertools
import os
import shlex
import shutil
import sys
import tarfile
import tempfile
import time
import subprocess

import jinja2
import requests
import tqdm

from . import Metadata, licensing, __version__
from .metadata import normalize_deps

DEFAULT_EDITOR = "vi"
XDG_CACHE_HOME = os.getenv("XDG_CACHE_HOME", os.path.expanduser("~/.cache"))
CACHEDIR = os.path.join(XDG_CACHE_HOME, "rust2rpm")
API_URL = "https://crates.io/api/v1/"
JINJA_ENV = jinja2.Environment(
    loader=jinja2.ChoiceLoader([
        jinja2.FileSystemLoader(["/"]),
        jinja2.PackageLoader("rust2rpm", "templates"),
    ]),
    extensions=["jinja2.ext.do"],
    trim_blocks=True,
    lstrip_blocks=True)

def get_default_target():
    try:
        os_release_file = open('/etc/os-release')
    except FileNotFoundError:
        os_release_file = open('/usr/lib/os-release')
    with os_release_file:
        conf = configparser.ConfigParser()
        conf.read_file(itertools.chain(["[os-release]"], os_release_file))
        os_release = conf["os-release"]
    os_id = os_release.get("ID")
    os_like = os_release.get("ID_LIKE")
    if os_like is not None:
        os_like = shlex.split(os_like)
    else:
        os_like = []

    # Order matters here!
    if os_id == "mageia" or ("mageia" in os_like):
        return "mageia"
    elif os_id == "fedora" or ("fedora" in os_like):
        return "fedora"
    elif "suse" in os_like:
        return "opensuse"
    else:
        return "plain"

def detect_editor():
    terminal = os.getenv("TERM")
    terminal_is_dumb = terminal is None or terminal == "dumb"
    editor = None
    if not terminal_is_dumb:
        editor = os.getenv("VISUAL")
    if editor is None:
        editor = os.getenv("EDITOR")
    if editor is None:
        if terminal_is_dumb:
            raise Exception("Terminal is dumb, but EDITOR unset")
        else:
            editor = DEFAULT_EDITOR
    return editor

def detect_packager():
    # If we're forcing the fallback...
    if os.getenv("RUST2RPM_NO_DETECT_PACKAGER") is not None:
        return None

    # If we're supplying packager identity through an environment variable...
    packager_identity = os.getenv("RUST2RPM_PACKAGER")
    if packager_identity is not None:
        return packager_identity

    # If we're detecting packager identity through rpmdev-packager...
    rpmdev_packager = shutil.which("rpmdev-packager")
    if rpmdev_packager is not None:
        return subprocess.check_output(rpmdev_packager, universal_newlines=True).strip()

    # If we're detecting packager identity through git configuration...
    git = shutil.which("git")
    if git is not None:
        name = subprocess.check_output([git, "config", "user.name"], universal_newlines=True).strip()
        email = subprocess.check_output([git, "config", "user.email"], universal_newlines=True).strip()
        return f"{name} <{email}>"

    return None

def file_mtime(path):
    return datetime.fromtimestamp(os.stat(path).st_mtime, timezone.utc).isoformat()

@contextlib.contextmanager
def remove_on_error(path):
    try:
        yield
    except: # this is supposed to include ^C
        os.unlink(path)
        raise

def local_toml(toml, version):
    if os.path.isdir(toml):
        toml = os.path.join(toml, "Cargo.toml")

    return toml, None, version

def local_crate(crate, version):
    cratename, version = os.path.basename(crate)[:-6].rsplit("-", 1)
    return crate, cratename, version

def download(crate, version):
    if version is None:
        # Now we need to get latest version
        url = requests.compat.urljoin(API_URL, f"crates/{crate}/versions")
        req = requests.get(url, headers={"User-Agent": "rust2rpm"})
        req.raise_for_status()
        versions = req.json()["versions"]
        version = next(version["num"] for version in versions if not version["yanked"])

    os.makedirs(CACHEDIR, exist_ok=True)
    cratef_base = f"{crate}-{version}.crate"
    cratef = os.path.join(CACHEDIR, cratef_base)
    if not os.path.isfile(cratef):
        url = requests.compat.urljoin(API_URL, f"crates/{crate}/{version}/download#")
        req = requests.get(url, stream=True, headers={"User-Agent": "rust2rpm"})
        req.raise_for_status()
        total = int(req.headers["Content-Length"])
        with remove_on_error(cratef), open(cratef, "wb") as f:
            for chunk in tqdm.tqdm(req.iter_content(), f"Downloading {cratef_base}".format(cratef_base),
                                   total=total, unit="B", unit_scale=True):
                f.write(chunk)
    return cratef, crate, version

@contextlib.contextmanager
def toml_from_crate(cratef, crate, version):
    with tempfile.TemporaryDirectory() as tmpdir:
        target_dir = f"{tmpdir}/"
        with tarfile.open(cratef, "r") as archive:
            for n in archive.getnames():
                if not os.path.abspath(os.path.join(target_dir, n)).startswith(target_dir):
                    raise Exception("Unsafe filenames!")
            archive.extractall(target_dir)
        toml_relpath = f"{crate}-{version}/Cargo.toml"
        toml = f"{tmpdir}/{toml_relpath}"
        if not os.path.isfile(toml):
            raise IOError("crate does not contain Cargo.toml file")
        yield toml

def make_patch(toml, enabled=True, tmpfile=False):
    if not enabled:
        return []

    editor = detect_editor()

    mtime_before = file_mtime(toml)
    toml_before = open(toml).readlines()

    # When we are editing a git checkout, we should not modify the real file.
    # When we are editing an unpacked crate, we are free to edit anything.
    # Let's keep the file name as close as possible to make editing easier.
    if tmpfile:
        tmpfile = tempfile.NamedTemporaryFile("w+t", dir=os.path.dirname(toml),
                                              prefix="Cargo.", suffix=".toml")
        tmpfile.writelines(toml_before)
        tmpfile.flush()
        fname = tmpfile.name
    else:
        fname = toml
    subprocess.check_call([editor, fname])
    mtime_after = file_mtime(toml)
    toml_after = open(fname).readlines()
    toml_relpath = "/".join(toml.split("/")[-2:])
    diff = list(difflib.unified_diff(toml_before, toml_after,
                                     fromfile=toml_relpath, tofile=toml_relpath,
                                     fromfiledate=mtime_before, tofiledate=mtime_after))
    return diff

def _is_path(path):
    return "/" in path or path in {".", ".."}

def make_diff_metadata(crate, version, patch=False, store=False):
    if _is_path(crate):
        # Only things that look like a paths are considered local arguments
        if crate.endswith(".crate"):
            cratef, crate, version = local_crate(crate, version)
        else:
            if store:
                raise ValueError("--store-crate can only be used for a crate")

            toml, crate, version = local_toml(crate, version)
            diff = make_patch(toml, enabled=patch, tmpfile=True)
            metadata = Metadata.from_file(toml)
            return metadata.name, diff, metadata
    else:
        cratef, crate, version = download(crate, version)

    with toml_from_crate(cratef, crate, version) as toml:
        diff = make_patch(toml, enabled=patch)
        metadata = Metadata.from_file(toml)
    if store:
        shutil.copy2(cratef, os.path.join(os.getcwd(), f"{metadata.name}-{version}.crate"))
    return crate, diff, metadata

def to_list(s):
    if not s:
        return []
    return list(filter(None, (l.strip() for l in s.splitlines())))

def main():
    parser = argparse.ArgumentParser("rust2rpm",
                                     formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument("--show-license-map", action="store_true",
                        help="Print license mappings and exit")
    parser.add_argument("--translate-license", action="store_true",
                        help="Print mapping for specified license and exit")
    parser.add_argument("--no-auto-changelog-entry", action="store_true",
                        help="Do not generate a changelog entry")
    parser.add_argument("-", "--stdout", action="store_true",
                        help="Print spec and patches into stdout")
    parser.add_argument("-t", "--target", action="store",
                        choices=("plain", "fedora", "mageia", "opensuse"), default=get_default_target(),
                        help="Distribution target")
    parser.add_argument("-p", "--patch", action="store_true",
                        help="Do initial patching of Cargo.toml")
    parser.add_argument("-s", "--store-crate", action="store_true",
                        help="Store crate in current directory")
    parser.add_argument("--all-features", action="store_true",
                        help="Activate all available features")
    parser.add_argument("--dynamic-buildrequires", action="store_true",
                        help="Use dynamic BuildRequires feature")
    parser.add_argument("--no-dynamic-buildrequires", action="store_true",
                        help="Do not use dynamic BuildRequires feature")
    parser.add_argument("--suffix", action="store",
                        help="Package suffix")
    parser.add_argument("crate", help="crates.io name\n"
                                      "path/to/local.crate\n"
                                      "path/to/project/",
                        nargs="?")
    parser.add_argument("version", nargs="?", help="crates.io version")
    args = parser.parse_args()

    if args.show_license_map:
        licensing.dump_sdpx_to_fedora_map(sys.stdout)
        return

    if args.translate_license:
        license, comments = licensing.translate_license(args.target, args.crate)
        if comments:
            print(comments)
        print(license)
        return

    if args.crate is None:
        parser.error("required crate/path argument missing")

    crate, diff, metadata = make_diff_metadata(args.crate, args.version,
                                               patch=args.patch,
                                               store=args.store_crate)

    JINJA_ENV.globals["normalize_deps"] = normalize_deps
    JINJA_ENV.globals["to_list"] = to_list
    template = JINJA_ENV.get_template("main.spec")

    if args.patch and len(diff) > 0:
        patch_file = f"{metadata.name}-fix-metadata.diff"
    else:
        patch_file = None

    kwargs = {}
    kwargs["generator_version"] = __version__
    kwargs["crate"] = crate
    kwargs["target"] = args.target
    kwargs["all_features"] = args.all_features
    bins = [tgt for tgt in metadata.targets if tgt.kind == "bin"]
    libs = [tgt for tgt in metadata.targets if tgt.kind in {"lib", "rlib", "proc-macro"}]
    is_bin = len(bins) > 0
    is_lib = len(libs) > 0
    if is_bin:
        kwargs["include_main"] = True
        kwargs["bins"] = bins
    elif is_lib:
        kwargs["include_main"] = False
    else:
        raise ValueError("No bins and no libs")
    kwargs["include_devel"] = is_lib

    if args.no_auto_changelog_entry:
        kwargs["auto_changelog_entry"] = False
    else:
        kwargs["auto_changelog_entry"] = True

    if args.target in {"fedora", "mageia", "opensuse"}:
        kwargs["include_build_requires"] = True
        kwargs["include_provides"] = False
        kwargs["include_requires"] = False
    elif args.target == "plain":
        kwargs["include_build_requires"] = True
        kwargs["include_provides"] = True
        kwargs["include_requires"] = True
    else:
        assert False, f"Unknown target {args.target!r}"

    if args.target == "mageia":
        kwargs["pkg_release"] = "%mkrel 1"
        kwargs["rust_group"] = "Development/Rust"
    elif args.target == "opensuse":
        kwargs["spec_copyright_year"] = time.strftime("%Y")
        kwargs["pkg_release"] = "0"
        kwargs["rust_group"] = "Development/Libraries/Rust"
    else:
        kwargs["pkg_release"] = "1%{?dist}"

    if args.target == "fedora" and not args.no_dynamic_buildrequires:
        args.dynamic_buildrequires = True

    kwargs["generate_buildrequires"] = args.dynamic_buildrequires

    if args.target in {"opensuse", "fedora"}:
        kwargs["date"] = time.strftime("%a %b %d %T %Z %Y")
    else:
        kwargs["date"] = time.strftime("%a %b %d %Y")
    packager_identity = detect_packager()
    if packager_identity is not None:
        kwargs["packager"] = packager_identity

    if metadata.license is not None:
        license, comments = licensing.translate_license(args.target, metadata.license)
        kwargs["license"] = license
        kwargs["license_comments"] = comments

    conf = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())
    conf.read(["_rust2rpm.conf", ".rust2rpm.conf"])
    if args.target not in conf:
        conf.add_section(args.target)

    kwargs["distconf"] = conf[args.target]

    if args.suffix is not None:
        if metadata.name[-1].isdigit():
            suffix = f'-{args.suffix}'
        else:
            suffix = args.suffix
    else:
        suffix = ""
    kwargs["pkg_suffix"] = suffix
    spec_file = f"rust-{metadata.name}{suffix}.spec"
    spec_contents = template.render(md=metadata, patch_file=patch_file, **kwargs)
    if args.stdout:
        print(f"# {spec_file}")
        print(spec_contents)
        if patch_file is not None:
            print(f"# {patch_file}")
            print("".join(diff), end="")
    else:
        with open(spec_file, "w") as fobj:
            fobj.write(spec_contents)
            fobj.write("\n")
        if patch_file is not None:
            with open(patch_file, "w") as fobj:
                fobj.writelines(diff)

if __name__ == "__main__":
    main()
