from __future__ import annotations

import logging
import os
from contextlib import suppress
from datetime import date
from pathlib import Path

from .. import _config as _config_mod
from .. import _types as _t
from .._run_cmd import CompletedProcess as _CompletedProcess
from .._scm_version import ScmVersion
from ._git import GitWorkdir
from ._hg import HgWorkdir, run_hg
from ._scm_workdir import get_latest_file_mtime

log = logging.getLogger(__name__)

_FAKE_GIT_DESCRIBE_ERROR = _CompletedProcess(
    "fake git describe output for hg",
    1,
    "<>hg git failed to describe",
)


class GitWorkdirHgClient(GitWorkdir, HgWorkdir):
    @classmethod
    def from_potential_worktree(
        cls, wd: _t.PathT, config: _config_mod.Configuration | None = None
    ) -> GitWorkdirHgClient | None:
        hg_cmd = config.env.hg_command if config is not None else None
        timeout = config.env.subprocess_timeout if config is not None else None
        res = run_hg(
            ["root"], cwd=wd, hg_command=hg_cmd, timeout=timeout
        ).parse_success(parse=Path)
        if res is None:
            return None
        result = cls(res)
        result._config = config
        return result

    def is_dirty(self) -> bool:
        res = self.run_hg(["id", "-T", "{if(dirty, 1, 0)}"], check=True)
        return bool(int(res.stdout))

    def get_branch(self) -> str | None:
        res = self.run_hg(["id", "-T", "{bookmarks}"])
        if res.returncode:
            log.info("branch err %s", res)
            return None
        return res.stdout

    def get_head_date(self) -> date | None:
        return self.run_hg(["log", "-r", ".", "-T", "{shortdate(date)}"]).parse_success(
            parse=date.fromisoformat, error_msg="head date err"
        )

    def get_dirty_tag_date(self) -> date | None:
        """Get the latest modification time of changed files in the working directory.

        Returns the date of the most recently modified file that has changes,
        or None if no files are changed or if an error occurs.
        """
        if not self.is_dirty():
            return None

        try:
            # Get list of changed files using hg status
            status_res = self.run_hg(["status", "-m", "-a", "-r"])
            if status_res.returncode != 0:
                return None

            changed_files = []
            for line in status_res.stdout.strip().split("\n"):
                if line and len(line) > 2:
                    # Format is "M filename" or "A filename" etc.
                    filepath = line[2:]  # Skip status char and space
                    changed_files.append(filepath)

            return get_latest_file_mtime(changed_files, self.path)

        except Exception as e:
            log.debug("Failed to get dirty tag date: %s", e)

        return None

    def get_scm_version(self) -> ScmVersion | None:
        """Obtain version metadata from this hg-git hybrid."""
        from ._git import _git_parse_inner

        return _git_parse_inner(self.config, self)

    def list_tracked_files(self, path: Path | str = "") -> list[str]:
        """List files tracked via hg in an hg-git setup."""
        from .._file_finders import scm_find_files
        from .._file_finders._hg import _hg_ls_files_and_dirs

        base = str(path) if path else str(self.project_root)
        hg_files, hg_dirs = _hg_ls_files_and_dirs(
            str(self.path),
            hg_command=self._hg_command,
            timeout=self._subprocess_timeout,
        )
        return scm_find_files(base, hg_files, hg_dirs)

    def is_file_tracked(self, path: Path) -> bool:
        res = self.run_hg(["files", str(path)])
        return res.returncode == 0

    def is_shallow(self) -> bool:
        return False

    def fetch_shallow(self) -> None:
        pass

    def get_hg_node(self) -> str | None:
        res = self.run_hg(["log", "-r", ".", "-T", "{node}"])
        if res.returncode:
            return None
        else:
            return res.stdout

    def _hg2git(self, hg_node: str) -> str | None:
        with suppress(FileNotFoundError):
            with open(os.path.join(self.path, ".hg/git-mapfile")) as map_items:
                for item in map_items:
                    if hg_node in item:
                        git_node, hg_node = item.split()
                        return git_node
        return None

    def node(self) -> str | None:
        hg_node = self.get_hg_node()
        if hg_node is None:
            return None

        git_node = self._hg2git(hg_node)

        if git_node is None:
            # trying again after hg -> git
            self.run_hg(["gexport"])
            git_node = self._hg2git(hg_node)

            if git_node is None:
                log.debug("Cannot get git node so we use hg node %s", hg_node)

                if hg_node == "0" * len(hg_node):
                    # mimic Git behavior
                    return None

                return hg_node

        return git_node

    def count_all_nodes(self) -> int:
        res = self.run_hg(["log", "-r", "ancestors(.)", "-T", "."])
        return len(res.stdout)

    def default_describe(self) -> _CompletedProcess:
        """
        Tentative to reproduce the output of

        `git describe --dirty --tags --long --match *[0-9]*`

        """
        res = self.run_hg(
            [
                "log",
                "-r",
                "(reverse(ancestors(.)) and tag(r're:v?[0-9].*'))",
                "-T",
                "{tags}{if(tags, ' ', '')}",
            ],
        )
        if res.returncode:
            return _FAKE_GIT_DESCRIBE_ERROR
        hg_tags: list[str] = res.stdout.split()

        if not hg_tags:
            return _FAKE_GIT_DESCRIBE_ERROR

        try:
            with self.path.joinpath(".hg/git-tags").open() as fp:
                git_tags: dict[str, str] = dict(line.split()[::-1] for line in fp)
        except FileNotFoundError:
            return _FAKE_GIT_DESCRIBE_ERROR

        tag: str
        for hg_tag in hg_tags:
            if hg_tag in git_tags:
                tag = hg_tag
                break
        else:
            logging.warning("tag not found hg=%s git=%s", hg_tags, git_tags)
            return _FAKE_GIT_DESCRIBE_ERROR

        res = self.run_hg(["log", "-r", f"'{tag}'::.", "-T", "."])
        if res.returncode:
            return _FAKE_GIT_DESCRIBE_ERROR
        distance = len(res.stdout) - 1

        node = self.node()
        assert node is not None
        desc = f"{tag}-{distance}-g{node}"

        if self.is_dirty():
            desc += "-dirty"
        log.debug("faked describe %r", desc)
        return _CompletedProcess(
            ["setuptools-scm", "faked", "describe"],
            returncode=0,
            stdout=desc,
            stderr="",
        )
