import logging
import os
import subprocess
from dataclasses import dataclass, field
from datetime import datetime
from typing import Generator, List

from .shared import Mirror

LOG = logging.getLogger("mirror-tool")
COMMIT_LIMIT = int(os.getenv("MIRROR_TOOL_COMMIT_LIMIT") or "20")


class GitParseError(RuntimeError):
    pass


@dataclass
class Commit:
    """Contains info on a single commit."""

    revision: str
    """The commit's revision (SHA1)."""

    revision_abbrev: str
    """The commit's abbreviated revision."""

    author_name: str
    """The author's name."""

    author_email: str
    """The author's email."""

    author_email_local: str
    """The local part of the author's email (prior to the '@' character)."""

    author_datetime: datetime
    """The 'author' timestamp."""

    committer_name: str
    """The committer's name."""

    committer_email: str
    """The committer's email."""

    committer_email_local: str
    """The local part of the committer's email (prior to the '@' character)."""

    committer_datetime: datetime
    """The 'committer' timestamp."""

    subject: str
    """The subject line from the commit message."""

    body: str = ""
    """The body of the commit message."""

    url: str = ""
    """A URL at which this commit can possibly be accessed.

    This value is guessed based on the Git hosting service which appears to be in use
    for the mirrored repo. It might be wrong or missing in some cases.
    """

    @classmethod
    def from_log(cls, log: bytes) -> Generator["Commit", None, None]:
        # Parse a git log into Commit objects.
        # Log should be generated by command:
        # git log --pretty=format:%H%n%h%n%an%n%ae%n%al%n%at%n%cn%n%ce%n%cl%n%ct%n%s%n%b -z

        commit_logs = log.split(b"\x00")

        for commit_log in commit_logs:
            lines = commit_log.decode("utf-8").splitlines()
            if not lines:
                continue

            kwargs = {}

            kwargs["revision"] = lines.pop(0)
            kwargs["revision_abbrev"] = lines.pop(0)
            kwargs["author_name"] = lines.pop(0)
            kwargs["author_email"] = lines.pop(0)
            kwargs["author_email_local"] = lines.pop(0)
            kwargs["author_datetime"] = datetime.utcfromtimestamp(int(lines.pop(0)))
            kwargs["committer_name"] = lines.pop(0)
            kwargs["committer_email"] = lines.pop(0)
            kwargs["committer_email_local"] = lines.pop(0)
            kwargs["committer_datetime"] = datetime.utcfromtimestamp(int(lines.pop(0)))
            kwargs["subject"] = lines.pop(0)
            kwargs["body"] = "\n".join(lines)

            yield Commit(**kwargs)


@dataclass
class UpdateInfo:
    """Contains info on an update for a single mirror."""

    mirror: Mirror
    """The mirror associated with this update."""

    commits: List[Commit] = field(default_factory=list)
    """The commits pulled in by the update. The first entry is the tip of the update."""

    commit_count: int = 0
    """The number of commit(s) being pulled by the update."""

    commit_elided_count: int = 0
    """The number of commit(s) being pulled in, but omitted from UpdateInfo because
    there were too many commits.

    Equal to commit_count - len(commits).
    """

    changed: bool = False
    """True if there were any changes at all."""


def add_urls(mirror: Mirror, commits: List[Commit]):
    if not mirror.url.startswith("https://"):
        return

    if "github" in mirror.url:
        for commit in commits:
            commit.url = f"{mirror.url}/commit/{commit.revision}"
    elif "gitlab" in mirror.url:
        for commit in commits:
            commit.url = f"{mirror.url}/-/commit/{commit.revision}"


def get_update_info(
    rev_to: str,
    mirror: Mirror,
    rev_from: str = "HEAD",
    commit_limit: int = COMMIT_LIMIT,
) -> UpdateInfo:
    logs = subprocess.check_output(
        [
            "git",
            "log",
            "-z",
            "--pretty=format:%H%n%h%n%an%n%ae%n%al%n%at%n%cn%n%ce%n%cl%n%ct%n%s%n%b",
            f"{rev_from}..{rev_to}",
        ],
        text=False,
    )

    commits = list(Commit.from_log(logs))
    add_urls(mirror, commits)
    count = len(commits)
    elided = 0

    if len(commits) > commit_limit:
        commits = commits[:commit_limit]
        elided = count - len(commits)

    changed = True if count else False

    return UpdateInfo(
        mirror=mirror,
        changed=changed,
        commit_count=count,
        commit_elided_count=elided,
        commits=commits,
    )
