# Copyright (C) 2026 GoodData Corporation

"""Commit execution MCP tool."""

from __future__ import annotations

import subprocess
from pathlib import Path
from typing import TYPE_CHECKING, Annotated

from pydantic import Field

from aida_mcp.server.app_instance import Context
from aida_mcp.server.commit_cli import (
    CommitMessageInput,
    commit_with_fixup,
    commit_with_message,
    render_commit_message,
)
from aida_mcp.tools.core.git_policy_config import CommitPolicy, load_git_policy
from aida_mcp.utils.workspace import get_workspace_root

if TYPE_CHECKING:
    from mcp.server.fastmcp import FastMCP


def _normalize_str_list(value: str | list[str] | None) -> list[str]:
    if value is None:
        return []
    if isinstance(value, str):
        return [value]
    return value


def _get_head_info(repo_root: Path) -> tuple[str, str]:
    """Return (short_sha, subject) of HEAD."""
    sha = subprocess.check_output(
        ["git", "rev-parse", "--short", "HEAD"],
        cwd=repo_root,
        text=True,
    ).strip()
    subject = subprocess.check_output(
        ["git", "log", "-1", "--format=%s"],
        cwd=repo_root,
        text=True,
    ).strip()
    return sha, subject


def register(mcp: FastMCP) -> None:
    """Register commit execution tools."""
    mcp.tool()(commit_command)


async def commit_command(
    commit_type: Annotated[str, Field(description="Commit type prefix, for example feat or fix.")] = "chore",
    title: Annotated[str | None, Field(description="Commit title text (subject suffix).")] = None,
    risk: Annotated[
        str | None,
        Field(description="Policy-controlled risk footer value; may be required by .aida/git_policy.yaml."),
    ] = None,
    scope: Annotated[str | None, Field(description="Optional commit scope.")] = None,
    body: Annotated[str | None, Field(description="Optional commit body text.")] = None,
    fixup_of: Annotated[
        str | None, Field(description="Optional commit SHA/ref to use with git commit --fixup.")
    ] = None,
    ticket: Annotated[
        str | list[str] | None,
        Field(description='Optional ticket ID or list of IDs, for example ["DX-340"].'),
    ] = None,
    co_authored_by: Annotated[
        str | list[str] | None,
        Field(
            description=(
                "Policy-controlled co-author value or list of values in `Name <email>` form; "
                "may be required by .aida/git_policy.yaml, "
                'for example ["OpenAI Codex <codex@openai.com>"].'
            )
        ),
    ] = None,
    *,
    ctx: Context,
) -> dict[str, object]:
    """Execute a policy-compliant git commit in the workspace."""
    resolved_root = await get_workspace_root(ctx)
    policy = load_git_policy(resolved_root).commit
    tickets = _normalize_str_list(ticket)
    co_authors = _normalize_str_list(co_authored_by)

    needs_more_input = _build_commit_needs_more_input(
        policy=policy,
        risk=risk,
        co_authored_by=co_authors,
        fixup_of=fixup_of,
    )
    if needs_more_input is not None:
        return needs_more_input

    try:
        if fixup_of:
            # Validate fixup inputs inline (title/risk/tickets/co_authored_by not supported with fixup)
            if title and title.strip():
                raise ValueError(
                    "title is not supported with fixup_of; git will derive the fixup subject from the target."
                )
            if risk and risk.strip():
                raise ValueError("risk is not supported with fixup_of.")
            if any(t.strip() for t in tickets):
                raise ValueError("ticket is not supported with fixup_of.")
            if any(e.strip() for e in co_authors):
                raise ValueError("co_authored_by is not supported with fixup_of.")

            repo_root, staged_files = commit_with_fixup(
                workspace_root=resolved_root,
                fixup_of=fixup_of,
                body=body,
            )
        else:
            msg_input = CommitMessageInput(
                commit_type=commit_type,
                scope=scope,
                title=title or "",
                body=body,
                risk=risk,
                tickets=tickets,
                co_authored_by=co_authors,
            )
            message = render_commit_message(msg_input, workspace_root=resolved_root)
            repo_root, staged_files = commit_with_message(
                workspace_root=resolved_root,
                message=message,
            )

        sha, subject = _get_head_info(repo_root)
        return {
            "ok": True,
            "status": "committed",
            "sha": sha,
            "subject": subject,
            "files_changed": staged_files,
            "instruction": "Commit created. Do not run any git commit commands.",
        }
    except (ValueError, RuntimeError) as exc:
        return {
            "ok": False,
            "status": "error",
            "message": str(exc),
            "instruction": "The command failed. Do not retry without addressing the issue.",
        }


def _build_commit_needs_more_input(
    *,
    policy: CommitPolicy,
    risk: str | None,
    co_authored_by: list[str],
    fixup_of: str | None,
) -> dict[str, object] | None:
    if fixup_of:
        return None

    missing_fields: list[str] = []
    invalid_fields: list[str] = []
    field_requirements: dict[str, object] = {}

    normalized_risk = (risk or "").strip().lower()
    if policy.risk_enabled:
        field_requirements["risk"] = {
            "required_by_policy": True,
            "allowed_values": list(policy.risk_values),
            "trailer": policy.risk_prefix,
            "inference_hint": "Infer the best-fit risk from the current changes and retry.",
        }
        if not normalized_risk:
            missing_fields.append("risk")
        elif normalized_risk not in set(policy.risk_values):
            invalid_fields.append("risk")

    if policy.require_co_authored_by:
        field_requirements["co_authored_by"] = {
            "required_by_policy": True,
            "format": "Name <email>",
            "trailer": "Co-authored-by",
            "inference_hint": "Use the active assistant identity for this conversation and retry.",
        }
        if not any(item.strip() for item in co_authored_by):
            missing_fields.append("co_authored_by")

    if not missing_fields and not invalid_fields:
        return None

    message_parts: list[str] = []
    if missing_fields:
        message_parts.append(
            "Repository commit policy requires additional fields: " + ", ".join(sorted(missing_fields)) + "."
        )
    if "risk" in invalid_fields:
        allowed = ", ".join(policy.risk_values)
        message_parts.append(f"Risk value must be one of: {allowed}.")

    return {
        "ok": False,
        "status": "needs_more_input",
        "tool": "commit_command",
        "message": " ".join(message_parts),
        "missing_fields": missing_fields,
        "invalid_fields": invalid_fields,
        "field_requirements": field_requirements,
        "retry_instruction": (
            "Infer policy-controlled values from the current changes and retry commit_command. "
            "Do not ask the user for these fields unless inference is impossible."
        ),
    }
