# Copyright (C) 2026 GoodData Corporation

"""Commit-plan resolver used by CLI and MCP tools."""

from __future__ import annotations

import re
import shlex
from dataclasses import dataclass
from pathlib import Path

from aida_mcp.server.commit_cli import CommitMessageInput, render_commit_message, should_stage_all_changes
from aida_mcp.tools.core.git_policy_config import load_git_policy

_FOOTER_LINE_RE = re.compile(r"^[A-Za-z][A-Za-z0-9-]*:\s+.+$")
_NON_INTERACTIVE_GIT_ENV = "GIT_EDITOR=true GIT_MERGE_AUTOEDIT=no"


@dataclass(frozen=True, slots=True)
class CommitCommandPlan:
    command: str


@dataclass(frozen=True, slots=True)
class CommitCommandParts:
    subject: str
    message_lines: list[str]
    trailers: list[str]


def resolve_commit_plan(
    *,
    workspace_root: Path,
    commit_type: str,
    scope: str | None,
    title: str | None,
    body: str | None,
    risk: str | None,
    tickets: list[str],
    co_authored_by: list[str],
    fixup_of: str | None = None,
) -> CommitCommandPlan:
    if fixup_of:
        _validate_fixup_inputs(
            title=title,
            risk=risk,
            tickets=tickets,
            co_authored_by=co_authored_by,
        )
        return CommitCommandPlan(
            command=_build_raw_git_fixup_command(
                fixup_of=fixup_of,
                body=body,
                stage_all=should_stage_all_changes(workspace_root),
            )
        )

    policy = load_git_policy(workspace_root)
    message = render_commit_message(
        CommitMessageInput(
            commit_type=commit_type,
            scope=scope,
            title=(title or "").strip(),
            body=(body or "").strip() or None,
            risk=risk,
            tickets=tickets,
            co_authored_by=co_authored_by,
        ),
        policy=policy.commit,
        workspace_root=workspace_root,
    )
    parts = _message_to_git_parts(message)
    return CommitCommandPlan(
        command=_build_raw_git_commit_command(
            parts=parts,
            stage_all=should_stage_all_changes(workspace_root),
        )
    )


def _validate_fixup_inputs(
    *,
    title: str | None,
    risk: str | None,
    tickets: list[str],
    co_authored_by: list[str],
) -> None:
    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(ticket.strip() for ticket in tickets):
        raise ValueError("ticket is not supported with fixup_of.")
    if any(entry.strip() for entry in co_authored_by):
        raise ValueError("co_authored_by is not supported with fixup_of.")


def _message_to_git_parts(message: str) -> CommitCommandParts:
    lines = message.rstrip("\n").split("\n")
    if not lines or not lines[0].strip():
        raise ValueError("Commit message must include a subject.")

    subject = lines[0]
    trailer_start = _find_trailer_start(lines)
    if trailer_start == -1:
        body_lines = lines[1:]
        trailer_lines: list[str] = []
    else:
        body_lines = lines[1:trailer_start]
        trailer_lines = [line for line in lines[trailer_start:] if line.strip()]

    return CommitCommandParts(
        subject=subject,
        message_lines=[line for line in body_lines if line.strip()],
        trailers=[_footer_line_to_trailer(line) for line in trailer_lines],
    )


def _find_trailer_start(lines: list[str]) -> int:
    idx = len(lines) - 1
    while idx >= 0 and not lines[idx].strip():
        idx -= 1
    if idx < 0:
        return -1

    footer_lines_reversed: list[str] = []
    while idx >= 0:
        line = lines[idx].strip()
        if not line:
            break
        if not _FOOTER_LINE_RE.fullmatch(line):
            break
        footer_lines_reversed.append(line)
        idx -= 1

    if not footer_lines_reversed:
        return -1
    return idx + 1


def _build_raw_git_commit_command(*, parts: CommitCommandParts, stage_all: bool) -> str:
    lines: list[str] = []
    if stage_all:
        lines.append("git add -A && \\")
    lines.append(f"{_NON_INTERACTIVE_GIT_ENV} \\")
    lines.append("git commit \\")
    args = [("-m", parts.subject)]
    args.extend(("-m", line) for line in parts.message_lines)
    args.extend(("--trailer", trailer) for trailer in parts.trailers)
    for idx, (flag, value) in enumerate(args):
        suffix = " \\" if idx < len(args) - 1 else ""
        lines.append(f"  {flag} {shlex.quote(value)}{suffix}")
    return "\n".join(lines)


def _build_raw_git_fixup_command(*, fixup_of: str, body: str | None, stage_all: bool) -> str:
    lines: list[str] = []
    if stage_all:
        lines.append("git add -A && \\")
    lines.append(f"{_NON_INTERACTIVE_GIT_ENV} \\")
    lines.append("git commit \\")
    lines.append(f"  --fixup={shlex.quote(fixup_of.strip())}\\")
    body_lines = _body_to_git_lines(body)
    for idx, line in enumerate(body_lines):
        suffix = " \\" if idx < len(body_lines) - 1 else ""
        lines.append(f"  -m {shlex.quote(line)}{suffix}")
    return "\n".join(lines)


def _body_to_git_lines(body: str | None) -> list[str]:
    body_text = (body or "").strip("\n").strip()
    if not body_text:
        return []
    return [line for line in body_text.splitlines() if line.strip()]


def _footer_line_to_trailer(line: str) -> str:
    key, sep, value = line.partition(":")
    if not sep or not value.strip():
        raise ValueError(f"Invalid trailer line '{line}'.")
    return f"{key.strip()}={value.strip()}"
