# SPDX-License-Identifier: GPL-2.0-or-later
"""
repository helpers
"""
import os
import subprocess
import shutil
import json
from pathlib import Path
from typing import List, Optional, Type
from configparser import ConfigParser
from .repo import Repo, _logger
from .svn import SvnRepo


class GitRepo(Repo):
    """
    git repository helper implementation
    """

    vcs = "git"
    global_options = ["-c", "advice.detachedHead=false"]
    ignore_file_name = ".gitignore"

    @property
    def vcs_dir(self):
        return self.path / ".git"

    def add(self, file: Path):
        _logger.info("Adding %s", file)
        self.execute(f"add {file}".split())

    def remove(self, file: Path):
        _logger.info("Removing %s", file)
        self.execute(f"rm {file}".split())

    def checkout(self):
        cmd = ["clone"]
        cmd.extend(self.options)
        if self.tag is not None:
            cmd.extend(["-b", self.tag])
        elif self.branch is not None:
            cmd.extend(["-b", self.branch])
        if self.remote_path is not None:
            cmd.extend(["--filter=blob:none", "--no-checkout"])
        cmd.append(self.url)
        cmd.append(self.path)
        _logger.info("Cloning %s into %s", self.url, self.path)
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self.execute(cmd, cwd=self.path.parent)

        if self.remote_path is not None:
            self.execute(["sparse-checkout", "set", "--no-cone", self.remote_path])

        if self.target_commit is not None or self.remote_path is not None:
            cmd = ["checkout"]
            if self.target_commit is not None:
                cmd.append(self.target_commit)
            self.execute(cmd)

    def update(self):
        _logger.info("Updating %s", self.path)
        if self.__get_current_branch() is not None:
            self.execute("pull".split())

    def status(self):
        stat = ""
        cmd = ["status", "-s"]
        if self.__get_current_branch() is None:
            tag = (
                self.execute(
                    "describe --tags --exact-match".split(),
                    capture_output=True,
                    check=False,
                )
                .stdout.decode()
                .strip()
            )
            if tag != "":
                stat = f" tag: {tag}"
            else:
                stat = (
                    " @"
                    + self.execute("rev-parse HEAD".split(), capture_output=True)
                    .stdout.decode()
                    .strip()
                )
            stat += "\n"
        else:
            cmd.append("-b")

        stat += self.execute(cmd, capture_output=True).stdout.decode()

        dirty = ""
        if self.is_dirty():
            dirty = "[D] "

        _logger.info("%s: %s%s\n", self.path, dirty, stat.strip())

    def has_externals(self, relpath: str = ".", recursive: bool = True):
        return (self.path / relpath / ".gitmodules").exists() or (
            self.path / relpath / "git_externals.json"
        ).exists()

    def is_dirty(self, relpath: str = "."):
        return (
            # local modifications
            self.execute("status -s".split(), capture_output=True)
            .stdout.decode()
            .strip()
            != ""
            # unpushed commit
            or self.execute(
                "log --branches --not --remotes --no-walk --oneline".split(),
                capture_output=True,
            )
            .stdout.decode()
            .strip()
            != ""
            # local commit out of tree
            or self.execute(
                "describe --contains --all HEAD".split(),
                capture_output=True,
                check=False,
            ).returncode
            != 0
        )

    def __list_git_submodules(self, relpath: str = ".") -> List[Repo]:
        return_list: List[Repo] = []
        config = ConfigParser()
        config.read(f"{relpath}/.gitmodules")
        for section in config.sections():
            options = []
            if "path" not in config[section]:
                raise ValueError("Can't find path option in .gitmodule config")
            path = config[section]["path"].strip()

            if "url" not in config[section]:
                raise ValueError("Can't find url option in .gitmodule config")
            url = config[section]["url"].strip()

            if "branch" in config[section]:
                branch = config[section]["branch"].strip()
                if branch == ".":
                    branch = self.__get_current_branch()
                if branch is not None:
                    options.extend(["-b", branch])

            out = self.execute(
                f"-C {relpath} ls-tree -d HEAD {path}".split(),
                capture_output=True,
            )
            tree_props = out.stdout.decode().strip().split()
            if not tree_props[-1] == path:
                raise ValueError("Can't find commit sha1 with ls-tree")
            commit = tree_props[-2]
            return_list.append(
                GitRepo(
                    url,
                    self.path / relpath / path,
                    target_commit=commit,
                    options=options,
                )
            )
        return return_list

    def __list_git_externals(self, jdata, relpath: str = ".") -> List[Repo]:
        return_list: List[Repo] = []
        for url, cfg in json.loads(jdata.read()).items():
            target_commit = cfg.get("ref", None)
            tag = cfg.get("tag", None)
            branch = cfg.get("branch", None)
            options: List[str] = []

            if "targets" not in cfg:
                _logger.info("incomplete external, missing targets for repo: %s", url)
                continue

            # targets object contains actual external data
            for src, targets in cfg["targets"].items():
                remote_path = src
                if src in [".", "./"]:
                    remote_path = None
                for target in targets:
                    # here we generate single externals for each target directory
                    # in output repository
                    repoclass: Optional[Type[Repo]] = None
                    if cfg["vcs"] == "git":
                        repoclass = GitRepo
                        if src not in [".", "./"]:
                            _logger.warning(
                                "converting external '%s' with custom source path '%s', "
                                "check out project configs, something might need an update",
                                url,
                                src,
                            )
                    elif cfg["vcs"] == "svn":
                        repoclass = SvnRepo

                    if repoclass is None:
                        _logger.warning(
                            "unable to handle vcs '%s' for external: %s",
                            cfg["vcs"],
                            url,
                        )
                    else:
                        return_list.append(
                            repoclass(
                                url,
                                self.path / relpath / target,
                                target_commit=target_commit,
                                tag=tag,
                                branch=branch,
                                remote_path=remote_path,
                                options=options,
                            )
                        )

        return return_list

    def list_externals(self, relpath: str = ".") -> List[Repo]:
        return_list: List[Repo] = []
        if os.path.exists(f"{relpath}/.gitmodules"):
            try:
                return_list = self.__list_git_submodules(relpath)
            except (subprocess.CalledProcessError, ValueError) as error:
                _logger.error("unable to read submodule properties: %s", error)
        if os.path.exists(f"{relpath}/git_externals.json"):
            try:
                with open(  # pylint: disable=unspecified-encoding
                    f"{relpath}/git_externals.json", "r"
                ) as jdata:
                    return_list = self.__list_git_externals(jdata, relpath)
            except (subprocess.CalledProcessError, ValueError) as error:
                _logger.error("unable to read git_externals properties: %s", error)

        return return_list

    def list_files(self):
        _logger.info("Listing %s files", self.path)
        self.execute("ls-files")

    def list_folders(self, printout: bool = True):
        return self._return_local_folders(
            self.execute(
                "ls-tree -rd --name-only HEAD".split(), capture_output=True
            ).stdout.decode(),
            printout,
        )

    def rm_externals(self, relpath: str = "."):
        if os.path.exists(f"{relpath}/.gitmodules"):
            _logger.info("Delete git submodules from %s/%s", self.path, relpath)
            try:
                self.execute(f"-C {relpath} submodule deinit .".split())
                config = ConfigParser()
                config.read(f"{relpath}/.gitmodules")
                paths = []
                for section in config.sections():
                    if "path" not in config[section]:
                        raise ValueError("Can't find path option in .gitmodule config")
                    paths.append(config[section]["path"].strip())

                self.execute(f"-C {relpath} rm .gitmodules".split())
                for path in paths:
                    self.execute(f"-C {relpath} rm {path}".split())
                shutil.rmtree(f"{relpath}/.git/modules", ignore_errors=True)

            except (subprocess.CalledProcessError, ValueError) as error:
                _logger.info("unable to read submodule properties: %s", error)
        if os.path.exists(f"{relpath}/git_externals.json"):
            _logger.info("Delete git externals from %s/%s", self.path, relpath)
            with open(  # pylint: disable=unspecified-encoding
                f"{relpath}/git_externals.json", "r"
            ) as jdata:
                for cfg in json.loads(jdata.read()).values():
                    if "targets" not in cfg:
                        continue
                    # targets object contains actual external data
                    for targets in cfg["targets"].values():
                        for target in targets:
                            shutil.rmtree(f"{relpath}/{target}", ignore_errors=True)
            self.execute(f"-C {relpath} rm git_externals.json".split())
            shutil.rmtree(f"{relpath}/.git_externals", ignore_errors=True)

    def add_ignores(self, *patterns: Path):
        ignore_file_path = self.path / self.ignore_file_name

        ignores_content = []
        if Path(ignore_file_path).is_file():
            with open(  # pylint: disable=unspecified-encoding
                ignore_file_path, "r"
            ) as ignore_file:
                ignores_content = [i.strip() for i in ignore_file.read().splitlines()]

        for pattern in patterns:
            path = str(pattern.relative_to(self.path)).strip()
            if path not in ignores_content:
                ignores_content.append(path)
            else:
                _logger.info("Ignore rule %s already exists in .gitignore", path)

        with open(  # pylint: disable=unspecified-encoding
            ignore_file_path, "w"
        ) as ignore_file:
            ignore_file.write("\n".join(ignores_content))

        self.add(ignore_file_path)

    def del_ignores(self, *patterns: Path):
        ignore_file_path = self.path / self.ignore_file_name

        ignorecont = []
        if Path(ignore_file_path).is_file():
            with open(  # pylint: disable=unspecified-encoding
                ignore_file_path, "r"
            ) as ignore_file:
                ignorecont = [i.strip() for i in ignore_file.read().splitlines()]

        for pattern in patterns:
            pattern = pattern.relative_to(self.path)
            try:
                del ignorecont[ignorecont.index(str(pattern))]
                _logger.info("Delete ignore rule %s from .gitignore", pattern)
            except ValueError:
                _logger.info("Ignore rule %s does not exist in .gitignore", pattern)

        with open(  # pylint: disable=unspecified-encoding
            ignore_file_path, "w"
        ) as ignore_file:
            ignore_file.write("\n".join(ignorecont))

    def commit(self, message: str, files: List[Path]):
        raise NotImplementedError()

    def __get_current_branch(self):
        branch = (
            self.execute(
                "rev-parse --abbrev-ref --symbolic-full-name HEAD".split(),
                capture_output=True,
            )
            .stdout.decode()
            .strip()
        )
        if branch == "HEAD":
            return None
        return branch

    @staticmethod
    def check_remote(url):
        cmd = f"git ls-remote {url}".split()
        # pylint: disable=subprocess-run-check
        return subprocess.run(cmd, capture_output=True).returncode == 0
