#!/usr/bin/env python3
"""Metabranch CLI - A tool for managing git metabranches."""

from functools import wraps

import click
import git as gitpython
from rich.console import Console

from .exceptions import MetabranchError
from .render import (
    render_branch_list,
    render_progress,
    render_status,
    render_workflow_summary,
)
from .sdk import Clients, build_clients

console = Console()

# Global context settings for help options
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])


def _dedupe_branch_names(
    branches: tuple[str, ...],
) -> tuple[tuple[str, ...], tuple[str, ...]]:
    """Return stable deduped branch names plus repeated input names."""
    seen: set[str] = set()
    deduped: list[str] = []
    duplicates: list[str] = []
    for branch_name in branches:
        if branch_name in seen:
            if branch_name not in duplicates:
                duplicates.append(branch_name)
            continue
        seen.add(branch_name)
        deduped.append(branch_name)
    return tuple(deduped), tuple(duplicates)


def _warn_ignored_branches(action: str, reason: str, branches: tuple[str, ...]) -> None:
    """Print one warning for ignored branch names."""
    if not branches:
        return
    console.print(
        f"[yellow]Warning:[/] {reason} in {action} request were ignored: "
        + ", ".join(branches)
    )


def _normalize_branch_names(
    branches: tuple[str, ...],
    *,
    action: str,
) -> tuple[str, ...]:
    """Warn about repeated branch arguments and return a stable deduped tuple."""
    deduped, duplicates = _dedupe_branch_names(branches)
    _warn_ignored_branches(action, "duplicate branches", duplicates)
    return deduped


def _normalize_membership_branch_names(
    clients: Clients,
    branches: tuple[str, ...],
    *,
    action: str,
) -> tuple[str, ...]:
    """Warn about ignored add/remove branch names and return actionable ones."""
    deduped, duplicates = _dedupe_branch_names(branches)
    status = clients.repository.status()
    if not status.is_metabranch or status.metabranch is None:
        _warn_ignored_branches(action, "duplicate branches", duplicates)
        return deduped

    existing = set(status.metabranch.branches)
    if action == "add":
        ignored_state = {branch for branch in deduped if branch in existing}
        reason = "duplicate or already-present branches"
    else:
        ignored_state = {branch for branch in deduped if branch not in existing}
        reason = "duplicate or missing branches"

    ignored = tuple(
        dict.fromkeys(
            branch
            for branch in branches
            if branch in set(duplicates) or branch in ignored_state
        )
    )
    _warn_ignored_branches(action, reason, ignored)
    return tuple(branch for branch in deduped if branch not in ignored_state)


def inject_client(func):
    """Decorator to inject SDK clients as the first parameter."""

    @wraps(func)
    def wrapper(*args, **kwargs):
        # Get the client from the context
        ctx = click.get_current_context()
        clients = ctx.obj["clients"]
        try:
            return func(clients, *args, **kwargs)
        except MetabranchError as exc:
            message = str(exc) or (
                exc.__doc__.strip() if exc.__doc__ else "Metabranch command failed."
            )
            raise click.ClickException(message) from exc
        except gitpython.GitCommandError as exc:
            raise click.ClickException(
                _format_git_command_error(exc, clients)
            ) from exc

    return wrapper


def _format_git_command_error(
    exc: gitpython.GitCommandError,
    clients: Clients,
) -> str:
    """Render a Git command failure without leaking a traceback."""
    details: list[str] = []
    for stream in (getattr(exc, "stdout", None), getattr(exc, "stderr", None)):
        if isinstance(stream, str):
            text = stream.strip()
            if text and text not in details:
                details.append(text)

    message = "\n".join(details) if details else "Git command failed."
    journal = clients.repository.active_workflow()
    if journal is not None and journal.status == "blocked":
        message += (
            "\n\nResolve the Git conflict, then run `metabranch resume`, "
            "or run `metabranch abort` to roll back."
        )
    return message


@click.group(context_settings=CONTEXT_SETTINGS)
@click.pass_context
def cli(ctx):
    """Metabranch - A tool for managing git metabranches."""
    ctx.ensure_object(dict)
    clients = build_clients()
    if hasattr(clients, "_context"):
        clients._context.progress_reporter = (  # type: ignore[attr-defined]
            lambda message: render_progress(console, message)
        )
    ctx.obj["clients"] = clients


@cli.command()
@click.option("-b", "--branch", "create", is_flag=True, help="Create a new metabranch")
@click.argument("name")
@click.argument("names", nargs=-1)
@inject_client
def checkout(clients: Clients, create: bool, name: str, names: tuple):
    """Create a metabranch or checkout an existing branch."""
    if create:
        clients.repository.create(
            name,
            *_normalize_branch_names(names, action="checkout"),
        )
    else:
        if names:
            raise click.UsageError(
                "Branches cannot be provided when switching metabranch"
            )
        clients.repository.checkout(name)


@cli.command()
@click.argument("name", required=False)
@click.argument("names", nargs=-1)
@inject_client
def branch(
    clients: Clients,
    name: str | None,
    names: tuple[str, ...],
):
    """List metabranches or create one without checking it out."""
    if name is None:
        render_branch_list(console, clients.repository.list())
        return
    clients.repository.create(
        name,
        *_normalize_branch_names(names, action="branch"),
        checkout=False,
    )


@cli.command()
@click.argument("branches", nargs=-1)
@inject_client
def add(clients: Clients, branches: tuple[str, ...]):
    """Add one or more contained branches to the current metabranch."""
    if not branches:
        raise click.UsageError("Provide at least one branch to add.")
    clients.branches.add_branches(
        *_normalize_membership_branch_names(clients, branches, action="add")
    )


@cli.command()
@click.argument("branches", nargs=-1)
@inject_client
def remove(clients: Clients, branches: tuple[str, ...]):
    """Remove one or more contained branches from the current metabranch."""
    if not branches:
        raise click.UsageError("Provide at least one branch to remove.")
    clients.branches.remove_branches(
        *_normalize_membership_branch_names(clients, branches, action="remove")
    )


@cli.command()
@inject_client
def status(clients: Clients):
    """Show current branch status."""
    render_status(console, clients.repository.status())


@cli.command(
    context_settings={
        **CONTEXT_SETTINGS,
        "ignore_unknown_options": True,
        "allow_extra_args": True,
    }
)
@click.argument("commit_args", nargs=-1, type=click.UNPROCESSED)
@inject_client
def commit(clients: Clients, commit_args: tuple[str, ...]):
    """Run git commit on the metabranch, then reconcile it immediately."""
    clients.workflows.commit(*commit_args)


@cli.command()
@click.argument("base", required=False)
@click.option(
    "-r",
    "--recursive",
    is_flag=True,
    help="Also update contained branches, not just the metabranch.",
)
@click.option(
    "--pull",
    "pull_branches",
    is_flag=True,
    help="Pull contained branches from remotes before rebuilding.",
)
@click.option("-i", "--interactive", is_flag=True, help="Interactive rebase")
@inject_client
def rebase(
    clients: Clients,
    base: str | None,
    recursive: bool,
    pull_branches: bool,
    interactive: bool,
):
    """Rebuild the current metabranch, optionally rebasing contained branches."""
    clients.workflows.rebase(
        base=base,
        interactive=interactive,
        recursive=recursive,
        pull=pull_branches,
    )


@cli.command()
@click.argument("base", required=False)
@click.option(
    "-r",
    "--recursive",
    is_flag=True,
    help="Also pull contained branches, not just the metabranch.",
)
@inject_client
def pull(clients: Clients, base: str | None, recursive: bool):
    """Pull contained branches from remotes and rebuild the metabranch."""
    clients.workflows.pull(base=base, recursive=recursive)


@cli.command()
@click.option(
    "-r",
    "--recursive",
    is_flag=True,
    help="Also push contained branches, not just the metabranch.",
)
@click.option("--force", is_flag=True, help="Force-push the selected branches.")
@inject_client
def push(clients: Clients, recursive: bool, force: bool):
    """Push the current metabranch and its branches."""
    clients.workflows.push(force=force, recursive=recursive)


@cli.command()
@inject_client
def resume(clients: Clients):
    """Resume the active metabranch workflow."""
    journal = clients.repository.active_workflow()
    if journal is not None:
        render_workflow_summary(console, journal, verb="Resuming")
    clients.workflows.resume()


@cli.command()
@inject_client
def abort(clients: Clients):
    """Abort the active metabranch workflow."""
    journal = clients.repository.active_workflow()
    if journal is not None:
        render_workflow_summary(console, journal, verb="Aborting")
    clients.workflows.abort()


if __name__ == "__main__":
    cli()
