#!/usr/bin/python3
"""Download Python module and build Debian package."""

import configparser
import json
import os
import re
import shutil
import subprocess
import sys
import tarfile
from typing import Any, Optional, Pattern

import appdirs  # type: ignore
import click
import in_place
import requests
from github import Github

re_github: Pattern[str] = re.compile(r"^https?://github.com/([^/]+/[^/]+?)(\.git|/)?$")
re_edugit: Pattern[str] = re.compile(r"^https://edugit.org/(.*)")

config = configparser.ConfigParser()
config.read(os.path.join(appdirs.user_config_dir(), "new_python_deb"))
base_dir: str = config["location"]["base_dir"]

# TODO: Use licensecheck to determine the license


def full_path(*path: str) -> str:
    """Add base dir to path."""
    return os.path.join(*((base_dir,) + path))


def get_package_data(package_name: str) -> dict[str, Any]:
    """Get pypi metadata for Python module."""
    url = f"https://pypi.python.org/pypi/{package_name}/json"
    r = requests.get(url)
    json_data: dict[str, Any] = r.json()
    return json_data


def get_github_url(data: dict[str, Any]) -> Optional[str]:
    """Find github URL in pypi metadata."""
    project_urls = data["info"].get("project_urls")
    if project_urls:
        urls: list[str] = project_urls.values()
        for url in urls:
            repo_name = github_repo_from_url(url)
            # skip URLs like this: https://github.com/sponsors/Zac-HD
            if repo_name and not repo_name.startswith("sponsors/"):
                return url

    homepage: str = data["info"].get("home_page")
    return homepage


def github_repo_from_url(url: str) -> Optional[str]:
    """Parse repo from GitHub URL."""
    m = re_github.match(url)
    return m.group(1) if m else None


def pypi_metadata(pypi_package_name: str) -> dict[str, Any]:
    """Load pypi metadata, use cache if available."""
    package_name = pypi_package_name.replace("_", "-")
    filename = f"{base_dir}/pypi/{package_name}.json"
    if os.path.exists(filename):
        data: dict[str, Any] = json.load(open(filename))

    else:
        data = get_package_data(pypi_package_name)
        with open(filename, "w") as out:
            json.dump(data, out, indent=2)

    return data


def check_for_bad_data(data: dict[str, Any]) -> None:
    """Exit with an error if there is a problem in the pypi metadata."""
    if "message" in data:
        print(data["message"])
        sys.exit(1)

    if "info" not in data:
        print(json.dumps(data, indent=2))
        sys.exit(1)


def get_rfc_date() -> str:
    """Get current timestamp in RFC 822 format."""
    return subprocess.run(["date", "-R"], capture_output=True, text=True).stdout[:-1]


class Package:
    """Debian package."""

    def __init__(
        self,
        pypi_package_name: str,
        version: Optional[str],
        url: Optional[str],
        homepage: Optional[str],
        add_python_prefix: bool = False,
    ) -> None:
        """Init."""
        self.pypi_package_name = pypi_package_name
        self.version = version
        self.url = url
        self.package_name = pypi_package_name.replace("_", "-").lower()
        self.upstream_package_name = self.package_name
        self.date = get_rfc_date()
        self.homepage = homepage
        self.pypi: Optional[dict[str, Any]] = None
        self.has_tests = False

        # do we include the python- prefix on the source package name
        self.add_python_prefix = add_python_prefix

    @property
    def package_dir(self) -> str:
        """Get location of package on filesystem."""
        return full_path(self.package_name, self.package_name)

    @property
    def level1_dir(self) -> str:
        """Get location of package on filesystem."""
        return full_path(self.package_name)

    def get_metadata(self) -> None:
        """Download metadata from pypi."""
        self.pypi = pypi_metadata(self.pypi_package_name)
        check_for_bad_data(self.pypi)

        if not self.homepage:
            self.homepage = get_github_url(self.pypi)
        assert self.homepage

        if self.version is None:
            self.version = self.pypi["info"]["version"]

        print(self.homepage)
        m = re_github.match(self.homepage)
        if not m:
            print("homepage is not github")
            print(f"https://pypi.python.org/pypi/{self.pypi_package_name}")
            sys.exit(1)

        self.repo_name = m.group(1)

        if (
            not self.pypi_package_name.startswith("python-")
            and "/python-" in self.repo_name
        ):
            self.add_python_prefix = True

        if self.add_python_prefix:
            print("""Adding python- prefix to source package name.""")
            self.package_name = "python-" + self.package_name

    @property
    def upstream(self) -> str:
        """Name and email of upstream author."""
        assert self.pypi
        return f"{self.pypi['info']['author']} <{self.pypi['info']['author_email']}>"

    @property
    def short_description(self) -> str:
        """Get short description from Package metadata."""
        assert self.pypi
        desc: str = self.pypi["info"]["summary"]
        return desc

    @property
    def orig_tar_gz_name(self) -> str:
        """Name of orig.tar.gz file."""
        return f"{self.package_name}_{self.version}.orig.tar.gz"

    @property
    def orig_tar_gz_path(self) -> str:
        """Path for orig.tar.gz file."""
        return full_path(self.package_name, self.orig_tar_gz_name)

    def get_tarball_url(self) -> Optional[str]:
        """Get tarball URL from GitHub for specific version."""
        assert self.version
        g = Github(config["github"]["access_token"])
        try:
            repo = g.get_repo(self.repo_name)
        except Exception:
            print(f"repo: [{self.repo_name}]")
            raise
        for tag in repo.get_tags():
            if tag.name not in (
                self.version,
                "v" + self.version,
                "release-" + self.version,
            ):
                continue
            return tag.tarball_url

        return None

    def download_tar(self) -> None:
        """Download the original tar file."""
        if os.path.exists(self.orig_tar_gz_path):
            return
        print(self.orig_tar_gz_path, "doesn't exist")
        assert self.version
        if not self.url:
            self.url = self.get_tarball_url()
        if not self.url:
            print(f"Can't find tarball for {self.version} on github")
            sys.exit(1)

        github_tarball_url = self.url

        r = requests.get(github_tarball_url)
        with open(self.orig_tar_gz_path, "wb") as tarball:
            tarball.write(r.content)

    def extract_tarball(self) -> None:
        """Extract tarball and rename dir."""
        if os.path.exists(self.package_dir):
            return
        with tarfile.open(self.orig_tar_gz_path, mode="r") as tf:
            top_dir = os.path.commonpath(tf.getnames())
            assert top_dir
            print(top_dir)
            full_top_dir = full_path(self.package_name, top_dir)
            if os.path.exists(full_top_dir):
                print(full_top_dir)
            assert not os.path.exists(full_top_dir)
            tf.extractall(path=self.level1_dir)
            os.rename(full_top_dir, self.package_dir)
        return

    def setup_git(self) -> None:
        """Git setup for package."""
        if os.path.exists(os.path.join(self.package_dir, ".git")):
            return

        assert self.version
        os.chdir(self.package_dir)
        subprocess.run(["git", "init"])
        subprocess.run(["git", "checkout", "-b", "upstream"])
        subprocess.run(["git", "add", "."])
        subprocess.run(["git", "commit", "-m", f"import {self.orig_tar_gz_name}"])
        subprocess.run(
            ["git", "tag", "-m", "tag upstream", "-s", f"upstream/{self.version}"]
        )
        subprocess.run(["pristine-tar", "commit", self.orig_tar_gz_path, "upstream"])
        subprocess.run(["git", "checkout", "-b", "debian/master"])

    @property
    def github_url(self) -> str:
        """Get github URL."""
        return "https://github.com/" + self.repo_name

    def debian_path(self, filename: str) -> str:
        """Path of a file or dir within the debian dir."""
        return os.path.join(self.debian_dir, filename)

    def drop_pytest_from_rules(self) -> None:
        """Remove the PYTEST line from debian/rules."""
        with in_place.InPlace(self.debian_path("rules")) as file:
            for line in file:
                if line != "export PYBUILD_TEST_PYTEST=1\n":
                    file.write(line)

    @property
    def debian_dir(self) -> str:
        """Path of the debian dir within the package."""
        return os.path.join(self.package_dir, "debian")

    def build_depends_list(self) -> list[str]:
        """List of package to add to Build-Depends."""
        setup_py = os.path.join(self.package_dir, "setup.py")

        deps = ["dh-sequence-python3", "python3-all"] + (
            ["python3-setuptools"]
            if os.path.exists(setup_py)
            else ["pybuild-plugin-pyproject", "python3-poetry-core"]
        )

        if self.has_tests:
            deps.append("python3-pytest <!nocheck>")

        return deps

    def build_depends_string(self) -> str:
        """Build-Depends string for debian/control."""
        return ",\n".join(" " * 15 + package for package in self.build_depends_list())

    def tests_in_package(self) -> bool:
        """Python package comes with tests."""
        for path, dirs, files in os.walk(self.package_dir):
            if path.startswith(self.debian_dir):
                continue
            if any("test" in i.lower() for i in dirs + files):
                print("tests found:", dirs + files)
                return True
        return False

    def render_template(self, path: str) -> None:
        """Replace placeholders in template."""
        assert self.version
        orig_content = open(path).read()
        content = orig_content.replace("UPSTREAM_PACKAGE", self.upstream_package_name)
        content = content.replace("PACKAGE", self.package_name)
        content = content.replace("EGG", self.upstream_package_name.replace("-", "_"))
        content = content.replace("SHORT", self.short_description or "NO DESCRIPTION")
        content = content.replace("GITHUB", self.github_url)
        content = content.replace("VERSION", self.version)
        content = content.replace("DATE", self.date)
        content = content.replace("UPSTREAM", self.upstream)
        content = content.replace("BUILD_DEPENDS", self.build_depends_string())
        if content != orig_content:
            open(path, "w").write(content)

    def delete_itp(self) -> None:
        """We don't want the ITP in the package."""
        debian_itp = self.debian_dir + "/itp"
        if os.path.exists(debian_itp):
            os.remove(debian_itp)

    @property
    def upstream_dir(self) -> str:
        """debian/upstream dir."""
        return os.path.join(self.debian_dir, "upstream")

    def render(self) -> None:
        """Render all the templates in the package."""
        if not os.path.exists(self.upstream_dir):
            os.mkdir(self.upstream_dir)
        dirs = [self.debian_dir, self.upstream_dir]

        for d in dirs:
            for f in os.scandir(d):
                if f.is_file():
                    self.render_template(f.path)

    def copy_templates(self) -> None:
        """Copy templates to the debian dir."""
        if os.path.exists(self.debian_dir):
            return
        shutil.copytree(full_path("template"), self.debian_dir)

    def add_readme_to_docs(self) -> None:
        """Add README.md or README.rst to debian/docs."""
        for ext in "md", "rst":
            filename = "README." + ext
            if not os.path.exists(os.path.join(self.package_dir, filename)):
                continue
            with open(os.path.join(self.debian_dir, "docs"), "w") as fh:
                fh.write(filename + "\n")
                break


@click.command()
@click.option("--version")
@click.option("--url")
@click.option("--homepage")
@click.option("--add-python-prefix", is_flag=True, default=False)
@click.argument("pypi_package_name")
def main(
    version: Optional[str],
    url: Optional[str],
    homepage: Optional[str],
    pypi_package_name: str,
    add_python_prefix: bool,
) -> None:
    """Download tarball and build Debian package."""
    p = Package(pypi_package_name, version, url, homepage, add_python_prefix)
    p.get_metadata()

    if not os.path.exists(p.level1_dir):
        os.mkdir(p.level1_dir)

    p.download_tar()

    print(p.orig_tar_gz_path, os.path.getsize(p.orig_tar_gz_path))

    p.extract_tarball()
    p.has_tests = p.tests_in_package()
    p.setup_git()
    p.copy_templates()
    p.delete_itp()
    p.render()
    if p.has_tests:
        p.drop_pytest_from_rules()

    p.add_readme_to_docs()
    subprocess.run(["wrap-and-sort"], cwd=p.package_dir)

    print(p.package_dir)


if __name__ == "__main__":
    main()
