# SPDX-License-Identifier: GPL-2.0-or-later
"""
repository helpers
"""
import logging
import os
import subprocess
from pathlib import Path
from typing import Union, List, Optional
from abc import ABC, abstractmethod

_logger = logging.getLogger(__name__)


def shortest_path(path: Path) -> Path:
    """
    return the shortest representation of input path:
    - a relative path if it is inside current directory
    - absolute path otherwise
    """
    try:
        return path.resolve().relative_to(Path(".").resolve())
    except ValueError:
        return path.resolve()


class Repo(ABC):
    """
    base abstract class for repository helper implementations, defines the
    required public methods
    """

    def __init__(  # pylint: disable=too-many-arguments
        self,
        url: Optional[str],
        path: Path,
        target_commit: Optional[str] = None,
        tag: Optional[str] = None,
        branch: Optional[str] = None,
        remote_path: Optional[str] = None,
        options: Optional[List[str]] = None,
    ):
        self.url = url
        self.path = path
        self.target_commit = target_commit
        self.tag = tag
        self.branch = branch
        self.remote_path = remote_path
        self.options = []
        if isinstance(options, list):
            self.options = options

    @property
    @abstractmethod
    def vcs(self) -> str:
        """
        return vcs type string
        """

    global_options: List[str] = []
    """
    optional list of cmdline options to be used for each command
    """

    @property
    @abstractmethod
    def vcs_dir(self) -> Path:
        """
        return resolved vcs private directory
        """

    def __str__(self):
        res = f"{shortest_path(self.path)} ("
        if self.remote_path is not None:
            res += f"{self.remote_path} "
        res += f"{self.vcs}: {self.url} -"
        if self.tag is not None:
            res += f" T'{self.tag}'"
        elif self.branch is not None:
            res += f" b'{self.branch}'"
        if self.target_commit is not None:
            res += f" r{self.target_commit}"
        res += f" {self.options})"
        return res

    def __repr__(self):
        return str(self)

    def __eq__(self, other):
        return (
            self.url == other.url
            and self.path == other.path
            and self.target_commit == other.target_commit
            and self.tag == other.tag
            and self.branch == other.branch
            and self.remote_path == other.remote_path
            and self.options == other.options
        )

    @abstractmethod
    def add(self, file: Path):
        """
        Add a file/directory to current local repo,
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def remove(self, file: Path):
        """
        Remove a file/directory from current local repo,
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def checkout(self):
        """
        perform a fresh checkout of the remote repository into local path,
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def update(self):
        """
        update the local copy of the repository pulling remote changes
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def status(self):
        """
        print out the local repository status message
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def has_externals(self, relpath: str = ".", recursive: bool = True):
        """
        return True if local repository has implementation defined externals
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def is_dirty(self, relpath: str = "."):
        """
        return True if repository has local modification
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def list_externals(self, relpath: str = "."):
        """
        return a list of external subprojects defined in this repository as a list of Repo
        instances
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def list_files(self):
        """
        print out the list of files handled by this repository prefixed by the repo path
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def list_folders(self, printout: bool = True):
        """
        return list of folder commited in this repository
        if printout is true also print to console
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def rm_externals(self, relpath: str = "."):
        """
        remove external subprojects defined in this repository
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def reset_externals(self, relpath: str = "."):
        """
        reset externals to remote original state
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def add_ignores(self, *patterns: Path):
        """
        add ignore patterns to local repository ignores list
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def del_ignores(self, *patterns: Path):
        """
        delete ignore patterns from local repository ignores list
        MUST be implemented by actual Repo helper implementation
        """

    @abstractmethod
    def commit(self, message: str, files: List[Path]):
        """
        commit files with defined message
        MUST be implemented by actual Repo helper implementation
        """

    def execute(self, command_and_args: Union[str, List[str]], **subprocess_kwargs):
        """
        execute a command in the local repository path
        """
        if isinstance(command_and_args, str):
            command_and_args = [command_and_args]
        cmd = [self.vcs]
        cmd.extend(self.global_options)
        cmd.extend([str(arg) for arg in command_and_args])
        if "cwd" not in subprocess_kwargs:
            subprocess_kwargs["cwd"] = self.path
        if "check" not in subprocess_kwargs:
            subprocess_kwargs["check"] = True
        _logger.debug(
            "executing command: '%s', (env: %s)", " ".join(cmd), subprocess_kwargs
        )
        # pylint: disable=subprocess-run-check
        return subprocess.run(cmd, **subprocess_kwargs)

    def exists(self):
        """
        return True if local repository exists
        """
        return self.path.exists()

    def _return_local_folders(self, output_list: str, printout: bool):
        return_list = []
        for line in output_list.split("\n"):
            if os.path.isdir(self.path / line):
                return_list.append(str(self.path / line))

        if printout:
            _logger.info("folder list: \n%s", "\n".join(return_list))
        return return_list

    @staticmethod
    @abstractmethod
    def check_remote(url: str):
        """
        return True if remote url is a valid repository,
        MUST be implemented by actual Repo helper implementation
        """
