from abc import ABC, abstractmethod
from collections.abc import Iterable
import glob
from pathlib import Path
import re
from typing import Self

import git
from git.remote import PushInfoList
from loguru import logger
from pydantic import DirectoryPath, Field

from .fp import lfilter, lmap, negate

from .exceptions import GitStatusError
from .pydantic import BaseModelRW
from .timestamps import make_timestamp
from .typing import ensure_tuple

# class PathsManager:
#     @abstractmethod
#     def setup(self) -> None: ...

#     @classmethod
#     @abstractmethod
#     def auto(cls, root_dir: Path): ...

#     @classmethod
#     @abstractmethod
#     def read(cls, config_file_path: Path) -> Self: ...

#     @abstractmethod
#     def write(self, config_file_path: Path) -> None: ...


def glob_extension(extension: str = "*", path: Path | None = None, ignore: re.Pattern[str] | str | None = None) -> list[Path]:
    """Search a directory recursively for all files ending with 'extension' and not matching 'ignore'.
    
        Defaults to the current directory.
    """
    if not path:
        path = Path.cwd()
    expression = f"**/*.{extension}"
    result: list[str] = glob.glob(expression, recursive=True)
    if ignore:
        _ignore = re.compile(ignore) if isinstance(ignore, str) else ignore
        result = lfilter(negate(_ignore.search), result)
    return lmap(Path, result)


class PathsManager(BaseModelRW, ABC):
    git_root: DirectoryPath | None = Field(default=None)
    # @property
    # @abstractmethod
    # def git_root(self) -> Path | None:
    #     raise NotImplementedError

    @property
    def repo(self) -> git.Repo | None:
        if not self.git_root:
            return None
        dot_git = self.git_root / ".git"
        if not dot_git.exists():
            return git.Repo.init(self.git_root)
        return git.Repo(self.git_root, search_parent_directories=True)

    @classmethod
    @abstractmethod
    def create(cls, *args, **kwargs) -> Self:
        """
        Create directories first if they do not already exist.
        """
        ...

    @classmethod
    @abstractmethod
    def auto(cls, *args, **kwargs) -> Self:
        """
        Create directories and files in the default location if they do not already exist.
        """
        ...

    @classmethod
    @abstractmethod
    def read(cls, config_file_path: Path) -> Self: ...

    @abstractmethod
    def write(self, config_file_path: Path) -> None: ...

    @staticmethod
    def ensure_file_exists(
        p: Path, content_if_empty: str = "", root_must_exist: bool = True
    ) -> Path:
        PathsManager.ensure_parents_exist(p, root_must_exist)
        if not p.exists():
            p.touch()
            if content_if_empty:
                p.write_text(content_if_empty)
        return p

    @staticmethod
    def ensure_directory_exists(p: Path, root_must_exist: bool = True) -> Path:
        PathsManager.ensure_parents_exist(p, root_must_exist)
        if not p.exists():
            p.mkdir()
        return p

    @staticmethod
    def ensure_parents_exist(p: Path, root_must_exist: bool = True) -> None:
        if not (root := Path(p.root)).exists():
            if root_must_exist:
                raise OSError(f"Root path does not exist: {root}")
            root.mkdir()
        for parent in reversed(p.parents):
            if not parent.exists():
                parent.mkdir()

    def assert_clean(self) -> None:
        if not self.is_clean:
            raise GitStatusError(f"Git repo at {self.git_root} must be clean.")

    @property
    def is_clean(self) -> bool:
        if not self.repo:
            return False
        return not self.repo.is_dirty()

    def commit_data(
        self,
        path_or_paths: Path | Iterable[Path] = tuple(),
        *,
        message: str | None = None,
        push: bool = True,
        remote_name: str = "origin",
        remote_url: str | None = None,
    ) -> tuple[git.Commit | None, PushInfoList | None]:
        def make_default_message() -> str:
            return f"automatic commit by '{self.__class__.__name__}' at {make_timestamp()}"

        repo = self.repo
        if not repo:
            return None, None

        for path in ensure_tuple(path_or_paths or self.git_root):
            repo.git.add(str(path))
        commit = repo.index.commit(message or make_default_message())

        push_info: PushInfoList | None = None
        if push:
            try:
                if remote_url:
                    remote = repo.create_remote(name=remote_name, url=remote_url)
                else:
                    remote = repo.remote(name=remote_name)
                push_info = remote.push()
            except Exception as e:
                logger.info(f"Push error: {e}")

        return commit, push_info
