# -*- coding: utf8 -*-
# Copyright (c) 2019 Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

import logging
import os
import typing as t
from pathlib import Path

import databind.core.dataclasses as dataclasses
import docspec
from nr.util.git import Git, NoCurrentBranchError

from pydoc_markdown.interfaces import Context, SourceLinker

logger = logging.getLogger(__name__)


@dataclasses.dataclass
class BaseGitSourceLinker(SourceLinker):
    """
    This is the base source linker for code in a Git repository.
    """

    #: The root directory, relative to the context directory. The context directory
    #: is usually the directory that contains Pydoc-Markdown configuration file.
    #: If not set, the project root will be determined from the Git repository in
    #: the context directory.
    root: t.Optional[str] = None

    #: Use the branch name instead of the current SHA to generate URLs.
    use_branch: bool = False

    def __post_init__(self) -> None:
        self._project_root: t.Optional[str] = None
        self._branch: t.Optional[str] = None
        self._sha: t.Optional[str] = None

    def get_context_vars(self) -> t.Dict[str, str]:
        return {}

    def get_url_template(self) -> str:
        raise NotImplementedError

    # SourceLinker

    def get_source_url(self, obj: docspec.ApiObject) -> t.Optional[str]:
        """
        Compute the URL in the GitHub/Gitea #repo for the API object *obj*.
        """

        assert self._project_root is not None

        if not obj.location or not obj.location.filename:
            return None

        # Compute the path relative to the project root.
        try:
            rel_path = Path(obj.location.filename).relative_to(self._project_root)
        except ValueError:
            logger.debug("Ignored API object %s, path points outside of project root.", obj.name)
            return None

        context_vars = self.get_context_vars()
        context_vars["path"] = str(rel_path)
        context_vars["sha"] = (self._branch if self.use_branch else self._sha) or "?"
        context_vars["lineno"] = str(obj.location.lineno)

        url = self.get_url_template().format(**context_vars)

        logger.debug("Calculated URL for API object %s is %s", obj.name, url)
        return url

    # PluginBase

    def init(self, context: Context) -> None:
        """
        This is called before the rendering process. The source linker computes the project
        root and Git SHA at this stage before #get_source_url() is called.
        """

        git = Git(context.directory)

        if self.root:
            self._project_root = os.path.join(context.directory, self.root)
        else:
            project_root = git.get_toplevel()
            if not project_root:
                raise RuntimeError(f'Path "%s" is not in a Git repository', context.directory)
            self._project_root = project_root

        self._sha = git.rev_parse("HEAD")
        self._branch = None

        if self.use_branch:
            try:
                self._branch = git.get_current_branch_name()
                if "(" in self._branch:
                    raise NoCurrentBranchError  # For older versions of nr.util which returned the detached HEAD branch
            except NoCurrentBranchError:
                logger.warning("Repository is not currently on a branch, falling back to SHA")
                self.use_branch = False

        logger.debug("project_root = %r", self._project_root)
        logger.debug("sha = %r", self._sha)
        logger.debug("branch = %r", self._branch)


@dataclasses.dataclass
class BaseGitServiceSourceLinker(BaseGitSourceLinker):

    #: The repository name, formatted as `owner/repo`.
    repo: str

    #: The host name of the Git service.
    host: str

    def get_context_vars(self) -> t.Dict[str, str]:
        return {"repo": self.repo, "host": self.host}


@dataclasses.dataclass
class GitSourceLinker(BaseGitSourceLinker):
    """
    This source linker allows you to define your own URL template to generate a permalink for
    an API object.
    """

    #: The URL template as a Python format string. Available variables are "path", "sha" and "lineno".
    url_template: str

    def get_url_template(self) -> str:
        return self.url_template


@dataclasses.dataclass
class GiteaSourceLinker(BaseGitServiceSourceLinker):
    """
    Source linker for Git repositories hosted on gitea.com or any self-hosted Gitea instance.
    """

    host: str = "gitea.com"

    def get_url_template(self) -> str:
        return "https://{host}/{repo}/src/commit/{sha}/{path}#L{lineno}"


@dataclasses.dataclass
class GithubSourceLinker(BaseGitServiceSourceLinker):
    """
    Source linker for Git repositories hosted on github.com or any self-hosted GitHub instance.
    """

    host: str = "github.com"

    def get_url_template(self) -> str:
        return "https://{host}/{repo}/blob/{sha}/{path}#L{lineno}"


@dataclasses.dataclass
class GitlabSourceLinker(BaseGitServiceSourceLinker):
    """
    Source linker for Git repositories hosted on gitlab.com or any self-hosted Gitlab instance.
    """

    host: str = "gitlab.com"

    def get_url_template(self) -> str:
        return "https://{host}/{repo}/-/blob/{sha}/{path}#L{lineno}"


@dataclasses.dataclass
class BitbucketSourceLinker(BaseGitServiceSourceLinker):
    """
    Source linker for Git repositories hosted on bitbucket.org or any self-hosted Bitbucket instance.
    """

    host: str = "bitbucket.org"

    def get_url_template(self) -> str:
        return "https://{host}/{repo}/src/{sha}/{path}#lines-{lineno}"
