import re
from typing import List
from typing import Optional
from typing import Tuple

from .connection import Connection
from .connection import GiteaHTTPResponse


class PullRequest:
    @classmethod
    def cmp(cls, entry: dict):
        if "base" in entry:
            # a proper pull request
            return entry["base"]["repo"]["full_name"], entry["number"]
        else:
            # an issue without pull request details
            return entry["repository"]["full_name"], entry["number"]

    @classmethod
    def split_id(cls, pr_id: str) -> Tuple[str, str, str]:
        """
        Split <owner>/<repo>#<number> into individual components and return them in a tuple.
        """
        match = re.match(r"^([^/]+)/([^/]+)#([0-9]+)$", pr_id)
        if not match:
            raise ValueError(f"Invalid pull request id: {pr_id}")
        return match.groups()

    @classmethod
    def to_human_readable_string(cls, entry: dict):
        from osc.output import KeyValueTable
        from . import User

        def yes_no(value):
            return "yes" if value else "no"

        if "base" in entry:
            # a proper pull request
            entry_id = f"{entry['base']['repo']['full_name']}#{entry['number']}"
            is_pull_request = True
        else:
            # an issue without pull request details
            entry_id = f"{entry['repository']['full_name']}#{entry['number']}"
            is_pull_request = False

        # HACK: search API returns issues, the URL needs to be transformed to a pull request URL
        entry_url = entry["url"]
        entry_url = re.sub(r"^(.*)/api/v1/repos/(.+/.+)/issues/([0-9]+)$", r"\1/\2/pulls/\3", entry_url)

        table = KeyValueTable()
        table.add("ID", entry_id, color="bold")
        table.add("URL", f"{entry_url}")
        table.add("Title", f"{entry['title']}")
        table.add("State", entry["state"])
        if is_pull_request:
            table.add("Draft", yes_no(entry["draft"]))
            table.add("Merged", yes_no(entry["merged"]))
            table.add("Allow edit", yes_no(entry["allow_maintainer_edit"]))
        table.add("Author", f"{User.to_login_full_name_email_string(entry['user'])}")
        if is_pull_request:
            table.add("Source", f"{entry['head']['repo']['full_name']}, branch: {entry['head']['ref']}, commit: {entry['head']['sha']}")
        table.add("Description", entry["body"])

        return str(table)

    @classmethod
    def list_to_human_readable_string(cls, entries: List, sort: bool = False):
        if sort:
            entries = sorted(entries, key=cls.cmp)
        result = []
        for entry in entries:
            result.append(cls.to_human_readable_string(entry))
        return "\n\n".join(result)

    @classmethod
    def create(
        cls,
        conn: Connection,
        *,
        target_owner: str,
        target_repo: str,
        target_branch: str,
        source_owner: str,
        source_branch: str,
        title: str,
        description: Optional[str] = None,
    ) -> GiteaHTTPResponse:
        """
        Create a pull request to ``owner``/``repo`` to the ``base`` branch.
        The pull request comes from a fork. The fork repo name is determined from gitea database.

        :param conn: Gitea ``Connection`` instance.
        :param target_owner: Owner of the target repo.
        :param target_repo: Name of the target repo.
        :param target_branch: Name of the target branch in the target repo.
        :param source_owner: Owner of the source (forked) repo.
        :param source_branch: Name of the source branch in the source (forked) repo.
        :param title: Pull request title.
        :param description: Pull request description.
        """
        url = conn.makeurl("repos", target_owner, target_repo, "pulls")
        data = {
            "base": target_branch,
            "head": f"{source_owner}:{source_branch}",
            "title": title,
            "body": description,
        }
        return conn.request("POST", url, json_data=data)

    @classmethod
    def get(
        cls,
        conn: Connection,
        owner: str,
        repo: str,
        number: int,
    ) -> GiteaHTTPResponse:
        """
        Get a pull request.

        :param conn: Gitea ``Connection`` instance.
        :param owner: Owner of the repo.
        :param repo: Name of the repo.
        :param number: Number of the pull request in the repo.
        """
        url = conn.makeurl("repos", owner, repo, "pulls", str(number))
        return conn.request("GET", url)

    @classmethod
    def set(
        cls,
        conn: Connection,
        owner: str,
        repo: str,
        number: int,
        *,
        title: Optional[str] = None,
        description: Optional[str] = None,
        allow_maintainer_edit: Optional[bool] = None,
    ) -> GiteaHTTPResponse:
        """
        Change a pull request.

        :param conn: Gitea ``Connection`` instance.
        :param owner: Owner of the repo.
        :param repo: Name of the repo.
        :param number: Number of the pull request in the repo.
        :param title: Change pull request title.
        :param description: Change pull request description.
        :param allow_maintainer_edit: Change whether users with write access to the base branch can also push to the pull request's head branch.
        """
        json_data = {
            "title": title,
            "description": description,
            "allow_maintainer_edit": allow_maintainer_edit,
        }
        url = conn.makeurl("repos", owner, repo, "pulls", str(number))
        return conn.request("PATCH", url, json_data=json_data)

    @classmethod
    def list(
        cls,
        conn: Connection,
        owner: str,
        repo: str,
        *,
        state: Optional[str] = "open",
    ) -> GiteaHTTPResponse:
        """
        List pull requests in a repo.

        :param conn: Gitea ``Connection`` instance.
        :param owner: Owner of the repo.
        :param repo: Name of the repo.
        :param state: Filter by state: open, closed, all. Defaults to open.
        """
        if state == "all":
            state = None

        q = {
            "state": state,
        }
        url = conn.makeurl("repos", owner, repo, "pulls", query=q)
        return conn.request("GET", url)

    @classmethod
    def search(
        cls,
        conn: Connection,
        *,
        state: str = "open",
        title: Optional[str] = None,
        owner: Optional[str] = None,
        labels: Optional[List[str]] = None,
        assigned: bool = False,
        created: bool = False,
        mentioned: bool = False,
        review_requested: bool = False,
    ) -> GiteaHTTPResponse:
        """
        Search pull requests.
        :param conn: Gitea ``Connection`` instance.
        :param state: Filter by state: open, closed. Defaults to open.
        :param title: Filter by substring in title.
        :param owner: Filter by owner of the repository associated with the pull requests.
        :param labels: Filter by associated labels. Non existent labels are discarded.
        :param assigned: Filter pull requests assigned to you.
        :param created: Filter pull requests created by you.
        :param mentioned: Filter pull requests mentioning you.
        :param review_requested: Filter pull requests requesting your review.
        """
        q = {
            "type": "pulls",
            "state": state,
            "q": title,
            "owner": owner,
            "labels": ",".join(labels) if labels else None,
            "assigned": assigned,
            "created": created,
            "mentioned": mentioned,
            "review_requested": review_requested,
        }
        url = conn.makeurl("repos", "issues", "search", query=q)
        return conn.request("GET", url)

    @classmethod
    def get_patch(
        cls,
        conn: Connection,
        owner: str,
        repo: str,
        number: str,
    ) -> GiteaHTTPResponse:
        """
        Get a patch associated with a pull request.

        :param conn: Gitea ``Connection`` instance.
        :param owner: Owner of the repo.
        :param repo: Name of the repo.
        :param number: Number of the pull request in the repo.
        """
        url = conn.makeurl("repos", owner, repo, "pulls", f"{number}.patch")
        return conn.request("GET", url)
