import json
import os
import subprocess
import sys
import urllib.parse
from pathlib import Path

from .. import conf as osc_conf
from .. import oscerr
from ..util import yaml as osc_yaml


class GitStore:

    @classmethod
    def is_project_dir(cls, path):
        try:
            store = cls(path)
        except oscerr.NoWorkingCopy:
            return False
        return store.is_project

    @classmethod
    def is_package_dir(cls, path):
        try:
            store = cls(path)
        except oscerr.NoWorkingCopy:
            return False
        return store.is_package

    @staticmethod
    def get_build_project(git_repo_url: str):
        """
        Get the project we use for building from _ObsPrj git repo.
        The _ObsPrj is located under the same owner as the repo with the package.
        They share the same branch.
        """
        import tempfile

        from osc import gitea_api

        # parse the git_repo_url (which usually corresponds with the url of the 'origin' remote of the local git repo)
        scheme, netloc, path, params, query, fragment = gitea_api.Git.urlparse(git_repo_url)

        # scheme + host
        gitea_host = urllib.parse.urlunparse((scheme, netloc, "", None, None, None))

        # OBS and Gitea usernames are identical
        # XXX: we're using the configured apiurl; it would be great to have a mapping from -G/--gitea-login to -A/--apiurl so we don't have to provide -A on the command-line
        apiurl = osc_conf.config["apiurl"]
        gitea_user = osc_conf.get_apiurl_usr(apiurl)

        # remove trailing ".git" from path
        if path.endswith(".git"):
            path = path[:-4]

        gitea_owner, gitea_repo = path.strip("/").split("/")[-2:]

        # replace gitea_repo with _ObsPrj
        gitea_repo = "_ObsPrj"

        # XXX: we assume that the _ObsPrj project has the same branch as the package
        gitea_branch = fragment

        gitea_conf = gitea_api.Config()
        try:
            gitea_login = gitea_conf.get_login_by_url_user(url=gitea_host, user=gitea_user)
        except gitea_api.Login.DoesNotExist:
            # matching login entry doesn't exist in git-obs config
            return None

        gitea_conn = gitea_api.Connection(gitea_login)

        with tempfile.TemporaryDirectory(prefix="osc_devel_project_git") as tmp_dir:
            try:
                gitea_api.Repo.clone(gitea_conn, gitea_owner, gitea_repo, branch=gitea_branch, quiet=True, directory=tmp_dir)
                return GitStore(tmp_dir, check=False).project
            except gitea_api.GiteaException:
                # "_ObsPrj" repo doesn't exist
                return None
            except subprocess.CalledProcessError:
                # branch doesn't exist
                return None
            except FileNotFoundError:
                # "project.build" file doesn't exist
                return None

    def get_project_obs_scm_store(self):
        from ..obs_scm import Store

        if not self.is_package:
            return None

        try:
            store = Store(os.path.join(self.abspath, ".."))
            store.assert_is_project()
            return store
        except oscerr.NoWorkingCopy:
            return None

    def get_project_git_scm_store(self):
        path = self.abspath
        while path:
            if path == "/":
                # no git repo found
                return None

            path, _ = os.path.split(path)

            if os.path.exists(os.path.join(path, ".git")):
                break

        config_path = os.path.join(path, "_config")
        pbuild_path = os.path.join(path, "_pbuild")
        subdirs_path = os.path.join(path, "_subdirs")
        manifest_path = os.path.join(path, "_manifest")

        # we always stop at the top-most directory that contains .git subdir
        if not (os.path.isfile(config_path) or os.path.isfile(pbuild_path)):
            # it's not a project, stop traversing and return
            return None

        if os.path.isfile(manifest_path):
            if os.path.isfile(subdirs_path):
                print("WARNING: Ignoring '_subdirs' file, using data from '_manifest'", file=sys.stderr)

            # the _manifest file contains a list of project subdirs that contain packages and list of dirs which are packages
            with open(manifest_path, "r") as f:
                data = osc_yaml.yaml_load(f)

            # ``packages`` is a list of directories which are packages
            packages = data.get("packages", [])
            packages_abspath = [os.path.abspath(os.path.join(path, i)) for i in packages]

            # ``subdirectories`` is a list of directories, which have subdirectories which are packages
            subdirs = data.get("subdirectories", [])
            subdirs_abspath = [os.path.abspath(os.path.join(path, i)) for i in subdirs]

            common_paths = set(packages) & set(subdirs)
            if common_paths:
                print(f"WARNING: _manifest contains conflicting entries between 'packages' and 'subdirectories': {sorted(common_paths)}", file=sys.stderr)

            if self.abspath in packages_abspath:
                # we're in a path defined in 'packages' -> it's a package
                pass
            elif self.abspath in subdirs_abspath:
                # paths listed in ``subdirectories`` are never packages, their subdirs are
                return None
            elif os.path.abspath(os.path.join(self.abspath, "..")) not in subdirs_abspath:
                # we're outside paths specified in 'subdirectories' -> not a package
                return None
            else:
                # we're in a subdir of a directory listed in 'subdirectories' -> it's a package
                pass

        elif os.path.isfile(subdirs_path):
            # the _subdirs file contains a list of project subdirs that contain packages
            with open(subdirs_path, "r") as f:
                data = osc_yaml.yaml_load(f)

                # ``subdirs`` is a list of directories, which have subdirectories which are packages
                subdirs = data.get("subdirs", [])

                # if set to "include", then all top-level directories are packages in addition to ``subdirs``
                toplevel = data.get("toplevel", "")

            if toplevel == "include":
                subdirs.append(".")

            subdirs_abspath = [os.path.abspath(os.path.join(path, subdir)) for subdir in subdirs]

            # paths listed in ``subdirs`` are never packages, their subdirs are
            if self.abspath in subdirs_abspath:
                return None

            # we're outside paths specified in subdirs -> not a package
            if os.path.abspath(os.path.join(self.abspath, "..")) not in subdirs_abspath:
                return None
        else:
            # no _subdirs file and self.abspath is not directly under the project dir -> not a valid package
            if path != os.path.abspath(os.path.join(self.abspath, "..")):
                return None

        return GitStore(path)

    def __init__(self, path, check=True):
        self.path = path
        self.abspath = os.path.abspath(self.path)

        self._apiurl = None
        self._package = None
        self._project = None

        self.is_project = False
        self.is_package = False

        if os.path.exists(os.path.join(self.abspath, ".git")):
            # NOTE: we have only one store in project-git for all packages
            config_path = os.path.join(self.abspath, "_config")
            pbuild_path = os.path.join(self.abspath, "_pbuild")
            if os.path.isfile(config_path) or os.path.isfile(pbuild_path):
                # there's .git and _config/_pbuild in the working directory -> it's a project
                self.is_project = True
            else:
                # there's .git and no _config/_pbuild in the working directory -> it's a package
                self.is_package = True

        self.project_store = None

        if self.project_store is None:
            self.project_store = self.get_project_obs_scm_store()

        if self.project_store is None:
            self.project_store = self.get_project_git_scm_store()

        if self.project_store:
            self.is_package = True

        if check and not any([self.is_project, self.is_package]):
            msg = f"Directory '{self.path}' is not a Git SCM working copy"
            raise oscerr.NoWorkingCopy(msg)

        if check and not self.scmurl:
            msg = f"Directory '{self.path}' is a Git SCM repo that lacks the 'origin' remote"
            raise oscerr.NoWorkingCopy(msg)

        # TODO: decide if we need explicit 'git lfs pull' or not
        # self._run_git(["lfs", "pull"])

    def assert_is_project(self):
        if not self.is_project:
            msg = f"Directory '{self.path}' is not a Git SCM working copy of a project"
            raise oscerr.NoWorkingCopy(msg)

    def assert_is_package(self):
        if not self.is_package:
            msg = f"Directory '{self.path}' is not a Git SCM working copy of a package"
            raise oscerr.NoWorkingCopy(msg)

    def _run_git(self, args):
        return subprocess.check_output(["git"] + args, encoding="utf-8", cwd=self.abspath).strip()

    @property
    def apiurl(self):
        from ..obs_scm import Store

        if not self._apiurl:
            if not self._apiurl and self.project_store:
                # get apiurl from .osc that is at the same level as project .git
                try:
                    store = Store(self.project_store.abspath)
                    store.assert_is_project()
                    self._apiurl = store.apiurl
                except oscerr.NoWorkingCopy:
                    pass

            if not self._apiurl and self.is_package and self.project_store:
                # read apiurl from parent directory that contains a project with .osc metadata
                self._apiurl = self.project_store.apiurl

            if not self._apiurl:
                # HACK: use the currently configured apiurl
                self._apiurl = osc_conf.config["apiurl"]

        return self._apiurl

    @apiurl.setter
    def apiurl(self, value):
        self._apiurl = value

    @property
    def project(self):
        from ..obs_scm import Store

        if not self._project:
            if self.is_package:
                # handle _project in a package

                if not self._project and self.project_store:
                    # get project from .osc that is at the same level as project .git
                    try:
                        store = Store(self.project_store.abspath)
                        store.assert_is_project()
                        self._project = store.project
                    except oscerr.NoWorkingCopy:
                        pass

                if not self._project and self.project_store:
                    # read project from detected project store
                    self._project = self.project_store.project

                if not self._project:
                    # read project from Gitea (identical owner, repo: _ObsPrj, file: project.build)
                    origin = self._run_git(["remote", "get-url", self.current_remote])
                    self._project = self.get_build_project(origin)

            else:
                # handle _project in a project

                if not self._project:
                    # read project from "project.build" file
                    path = os.path.join(self.abspath, "project.build")
                    if os.path.exists(path):
                        with open(path, "r", encoding="utf-8") as f:
                            self._project = f.readline().strip()

            if not self._project:
                # HACK: assume openSUSE:Factory project if project metadata is missing
                self._project = "openSUSE:Factory"

        return self._project

    @project.setter
    def project(self, value):
        self._project = value

    @property
    def package(self):
        if self._package is None:
            origin = self._run_git(["remote", "get-url", self.current_remote])
            self._package = Path(urllib.parse.urlsplit(origin).path).stem
        return self._package

    @package.setter
    def package(self, value):
        self._package = value

    def _get_option(self, name):
        try:
            result = self._run_git(["config", "--local", "--get", f"osc.{name}"])
        except subprocess.CalledProcessError:
            result = None
        return result

    def _check_type(self, name, value, expected_type):
        if not isinstance(value, expected_type):
            raise TypeError(f"The option '{name}' should be {expected_type.__name__}, not {type(value).__name__}")

    def _set_option(self, name, value):
        self._run_git(["config", "--local", f"osc.{name}", value])

    def _unset_option(self, name):
        try:
            self._run_git(["config", "--local", "--unset", f"osc.{name}"])
        except subprocess.CalledProcessError:
            pass

    def _get_dict_option(self, name):
        result = self._get_option(name)
        if result is None:
            return None
        result = json.loads(result)
        self._check_type(name, result, dict)
        return result

    def _set_dict_option(self, name, value):
        if value is None:
            self._unset_option(name)
            return
        self._check_type(name, value, dict)
        value = json.dumps(value)
        self._set_option(name, value)

    def _get_list_option(self, name):
        result = self._get_option(name)
        if result is None:
            return None
        result = json.loads(result)
        self._check_type(name, result, list)
        return result

    def _set_list_option(self, name, value):
        if value is None:
            self._unset_option(name)
            return
        self._check_type(name, value, list)
        value = json.dumps(value)
        self._set_option(name, value)

    @property
    def last_buildroot(self):
        self.assert_is_package()
        result = self._get_dict_option("last-buildroot")
        if result is not None:
            result = (result["repo"], result["arch"], result["vm_type"])
        return result

    @last_buildroot.setter
    def last_buildroot(self, value):
        self.assert_is_package()
        if len(value) != 3:
            raise ValueError("A tuple with exactly 3 items is expected: (repo, arch, vm_type)")
        value = {
            "repo": value[0],
            "arch": value[1],
            "vm_type": value[2],
        }
        self._set_dict_option("last-buildroot", value)

    @property
    def build_repositories(self):
        from ..core import Repo

        self.assert_is_package()
        repos = self._get_list_option("build-repositories")
        if repos is None:
            return None
        return [Repo(**i) for i in repos]

    @build_repositories.setter
    def build_repositories(self, value):
        from ..core import Repo

        self.assert_is_package()
        repos = []
        if value is not None:
            for i in value:
                if not isinstance(i, Repo):
                    raise ValueError(f"The value is not an instance of 'Repo': {i}")
                repos.append(i.dict())
        self._set_list_option("build-repositories", repos)

    @property
    def scmurl(self):
        try:
            return self._run_git(["remote", "get-url", self.current_remote])
        except subprocess.CalledProcessError:
            return None

    @property
    def current_remote(self):
        result = None
        try:
            result = self._run_git(["rev-parse", "--abbrev-ref", "@{u}"])
            if result:
                result = result.split("/")[0]
        except subprocess.CalledProcessError:
            pass

        if result:
            return result
        return "origin"
