from typing import List, Optional

from e2b.exceptions import InvalidArgumentException
from e2b.sandbox._git.auth import strip_credentials, with_credentials
from e2b.sandbox._git.parse import derive_repo_dir_from_url
from e2b.sandbox._git.types import ClonePlan


def shell_escape(value: str) -> str:
    """
    Escape a string for safe use in a shell command.

    :param value: Value to escape
    :return: Shell-escaped string
    """
    return "'" + value.replace("'", "'\"'\"'") + "'"


def build_git_command(args: List[str], repo_path: Optional[str] = None) -> str:
    """
    Build a shell-safe git command string.

    :param args: Git command arguments
    :param repo_path: Repository path for `git -C`, if provided
    :return: Shell-safe git command
    """
    parts = ["git"]
    if repo_path:
        parts.extend(["-C", repo_path])
    parts.extend(args)
    return " ".join(shell_escape(part) for part in parts)


def build_push_args(
    remote_name: Optional[str],
    *,
    remote: Optional[str],
    branch: Optional[str],
    set_upstream: bool,
) -> List[str]:
    """
    Build arguments for a git push command.

    :param remote_name: Resolved remote name, if any
    :param remote: Remote name override
    :param branch: Branch name to push
    :param set_upstream: Whether to set upstream tracking
    :return: List of git push arguments
    """
    args = ["push"]
    target_remote = remote_name or remote
    if set_upstream and target_remote:
        args.append("--set-upstream")
    if target_remote:
        args.append(target_remote)
    if branch:
        args.append(branch)
    return args


def build_pull_args(
    remote: Optional[str],
    branch: Optional[str],
    remote_name: Optional[str] = None,
) -> List[str]:
    """
    Build arguments for a git pull command.

    :param remote: Remote name override
    :param branch: Branch name to pull
    :param remote_name: Resolved remote name, if any
    :return: List of git pull arguments
    """
    args = ["pull"]
    target_remote = remote_name or remote
    if target_remote:
        args.append(target_remote)
    if branch:
        args.append(branch)
    return args


def build_remote_add_args(name: str, url: str, fetch: bool) -> List[str]:
    """
    Build arguments for a git remote add command.

    :param name: Remote name
    :param url: Remote URL
    :param fetch: Whether to fetch after adding the remote
    :return: List of git remote add arguments
    """
    if not name or not url:
        raise InvalidArgumentException(
            "Both remote name and URL are required to add a git remote."
        )

    args = ["remote", "add"]
    if fetch:
        args.append("-f")
    args.extend([name, url])
    return args


def build_remote_add_shell_command(
    args: List[str],
    path: str,
    name: str,
    url: str,
    fetch: bool,
) -> str:
    """
    Build a shell command that adds or updates a remote and optionally fetches.

    :param args: Base git remote add args
    :param path: Repository path
    :param name: Remote name
    :param url: Remote URL
    :param fetch: Whether to fetch after adding the remote
    :return: Shell command string
    """
    add_cmd = build_git_command(args, path)
    set_url_cmd = build_git_command(build_remote_set_url_args(name, url), path)
    cmd = f"{add_cmd} || {set_url_cmd}"
    if fetch:
        fetch_cmd = build_git_command(["fetch", name], path)
        cmd = f"({cmd}) && {fetch_cmd}"
    return cmd


def build_remote_get_url_args(name: str) -> List[str]:
    """
    Build arguments for a git remote get-url command.
    """
    return ["remote", "get-url", name]


def build_remote_set_url_args(name: str, url: str) -> List[str]:
    """
    Build arguments for a git remote set-url command.
    """
    return ["remote", "set-url", name, url]


def build_remote_get_command(path: str, name: str) -> str:
    """
    Build a shell command that returns the remote URL or empty output.

    :param path: Repository path
    :param name: Remote name
    :return: Shell command string
    """
    if not name:
        raise InvalidArgumentException("Remote name is required.")

    return f"{build_git_command(build_remote_get_url_args(name), path)} || true"


def build_credential_approve_command(
    username: str,
    password: str,
    host: str,
    protocol: str,
) -> str:
    """
    Build a git credential approve command for the given credentials.
    """
    target_host = host.strip() or "github.com"
    target_protocol = protocol.strip() or "https"
    credential_input = "\n".join(
        [
            f"protocol={target_protocol}",
            f"host={target_host}",
            f"username={username}",
            f"password={password}",
            "",
            "",
        ]
    )
    return (
        f"printf %s {shell_escape(credential_input)} | "
        f"{build_git_command(['credential', 'approve'])}"
    )


def build_has_upstream_args() -> List[str]:
    """
    Build arguments for a git upstream check command.
    """
    return ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]


def build_status_args() -> List[str]:
    """
    Build arguments for a git status command.
    """
    return ["status", "--porcelain=1", "-b"]


def build_branches_args() -> List[str]:
    """
    Build arguments for a git branch listing command.
    """
    return ["branch", "--format=%(refname:short)\t%(HEAD)"]


def build_create_branch_args(branch: str) -> List[str]:
    """
    Build arguments for a git checkout -b command.
    """
    return ["checkout", "-b", branch]


def build_checkout_branch_args(branch: str) -> List[str]:
    """
    Build arguments for a git checkout command.
    """
    return ["checkout", branch]


def build_delete_branch_args(branch: str, force: bool) -> List[str]:
    """
    Build arguments for a git branch delete command.
    """
    return ["branch", "-D" if force else "-d", branch]


def build_add_args(files: Optional[List[str]], all: bool) -> List[str]:
    """
    Build arguments for a git add command.
    """
    args = ["add"]
    if not files:
        args.append("-A" if all else ".")
    else:
        args.append("--")
        args.extend(files)
    return args


def build_commit_args(
    message: str,
    author_name: Optional[str],
    author_email: Optional[str],
    allow_empty: bool,
) -> List[str]:
    """
    Build arguments for a git commit command.
    """
    args = ["commit", "-m", message]
    if allow_empty:
        args.append("--allow-empty")
    author_args: List[str] = []
    if author_name:
        author_args.extend(["-c", f"user.name={author_name}"])
    if author_email:
        author_args.extend(["-c", f"user.email={author_email}"])
    if author_args:
        args = author_args + args
    return args


def build_reset_args(
    mode: Optional[str],
    target: Optional[str],
    paths: Optional[List[str]],
) -> List[str]:
    """
    Build arguments for a git reset command.
    """
    allowed_modes = ["soft", "mixed", "hard", "merge", "keep"]
    if mode and mode not in allowed_modes:
        raise InvalidArgumentException(
            f"Reset mode must be one of {', '.join(allowed_modes)}."
        )

    args = ["reset"]
    if mode:
        args.append(f"--{mode}")
    if target:
        args.append(target)
    if paths:
        args.append("--")
        args.extend(paths)
    return args


def build_restore_args(
    paths: List[str],
    staged: Optional[bool],
    worktree: Optional[bool],
    source: Optional[str],
) -> List[str]:
    """
    Build arguments for a git restore command.
    """
    if not paths:
        raise InvalidArgumentException("At least one path is required.")

    resolved_staged = staged
    resolved_worktree = worktree
    if staged is None and worktree is None:
        resolved_worktree = True
    elif staged is True and worktree is None:
        resolved_worktree = False
    elif staged is None and worktree is not None:
        resolved_staged = False

    if resolved_staged is False and resolved_worktree is False:
        raise InvalidArgumentException(
            "At least one of staged or worktree must be true."
        )

    args = ["restore"]
    if resolved_worktree:
        args.append("--worktree")
    if resolved_staged:
        args.append("--staged")
    if source:
        args.extend(["--source", source])
    args.append("--")
    args.extend(paths)
    return args


def build_clone_plan(
    url: str,
    path: Optional[str],
    branch: Optional[str],
    depth: Optional[int],
    auth_username: Optional[str],
    auth_password: Optional[str],
    dangerously_store_credentials: bool,
) -> ClonePlan:
    """
    Build clone arguments and metadata for post-clone credential stripping.
    """
    clone_url = (
        with_credentials(url, auth_username, auth_password)
        if auth_username and auth_password
        else url
    )
    sanitized_url = strip_credentials(clone_url)
    should_strip = not dangerously_store_credentials and sanitized_url != clone_url
    repo_path = path if not should_strip else path or derive_repo_dir_from_url(url)
    if should_strip and not repo_path:
        raise InvalidArgumentException(
            "A destination path is required when using credentials without storing them."
        )

    args = ["clone", clone_url]
    if branch:
        args.extend(["--branch", branch, "--single-branch"])
    if depth:
        args.extend(["--depth", str(depth)])
    if path:
        args.append(path)

    return ClonePlan(
        args=args,
        repo_path=repo_path,
        sanitized_url=sanitized_url if should_strip else None,
        should_strip=should_strip,
    )
