"""Helpers for `aida-mcp commit` command."""

from __future__ import annotations

import re
import subprocess
from dataclasses import dataclass
from pathlib import Path

from aida_mcp.tools.core.git_policy_config import CommitPolicy

_CO_AUTHOR_PREFIX_RE = re.compile(r"^co-authored-by:\s*", flags=re.IGNORECASE)
_CO_AUTHOR_VALUE_RE = re.compile(r"^.+ <[^<>\s@]+@[^<>\s@]+>$")
_FOOTER_LINE_RE = re.compile(r"^[A-Za-z][A-Za-z0-9-]*:\s+.+$")


@dataclass(frozen=True, slots=True)
class CommitMessageInput:
    commit_type: str
    scope: str | None
    title: str
    body: str | None
    risk: str
    jira: list[str]
    co_authored_by: list[str]
    ai_share: list[str]


def render_commit_message(data: CommitMessageInput, *, policy: CommitPolicy | None = None) -> str:
    active_policy = policy or CommitPolicy()
    title = data.title.strip()
    if not title:
        raise ValueError("Commit title must not be empty.")
    if "\n" in title:
        raise ValueError("Commit title must be a single line.")

    subject = _build_subject(data.commit_type, data.scope, title)
    if len(subject) > active_policy.subject_max_chars:
        raise ValueError(f"Commit title line is too long ({len(subject)} > {active_policy.subject_max_chars}).")

    risk = data.risk.strip().lower()
    if risk not in set(active_policy.risk_values):
        allowed = "|".join(active_policy.risk_values)
        raise ValueError(f"Unsupported risk value '{data.risk}'. Allowed: {allowed}.")

    lines: list[str] = [subject, ""]
    body = (data.body or "").strip()
    if body:
        lines.append(body)
        lines.append("")

    footer_lines: list[str] = []
    if data.ai_share:
        parsed_ai_share = _parse_ai_share_items(data.ai_share)
        footer_lines.append(_render_ai_share_line(parsed_ai_share))
    elif active_policy.require_ai_share:
        raise ValueError("AI share is required by git policy; pass --ai-share actor=percent entries.")

    normalized_co_authors: list[str] = [_normalize_co_author_value(entry) for entry in data.co_authored_by]
    for entry in normalized_co_authors:
        footer_lines.append(f"Co-authored-by: {entry}")
    if active_policy.require_co_authored_by and not normalized_co_authors:
        raise ValueError("Co-authored-by trailer is required by git policy; pass --co-authored-by.")

    jira_items = [item.strip() for item in data.jira if item.strip()]
    if jira_items:
        footer_lines.append(f"JIRA: {', '.join(jira_items)}")

    # Keep risk as the last footer line by policy.
    footer_lines.append(f"risk: {risk}")
    lines.extend(footer_lines)

    return "\n".join(lines).rstrip() + "\n"


def validate_commit_message(message: str, *, policy: CommitPolicy | None = None) -> list[str]:
    """Return policy violations for an existing commit message."""
    active_policy = policy or CommitPolicy()
    errors: list[str] = []
    lines = message.splitlines()
    if not lines:
        return ["Commit message is empty."]

    subject = lines[0].strip()
    if not subject:
        errors.append("Commit title must not be empty.")
        return errors
    if len(subject) > active_policy.subject_max_chars:
        errors.append(f"Commit title line is too long ({len(subject)} > {active_policy.subject_max_chars}).")

    footer_lines = _extract_trailing_footers(lines)
    footer_map: dict[str, list[str]] = {}
    for line in footer_lines:
        key, value = line.split(":", 1)
        footer_map.setdefault(key.strip().lower(), []).append(value.strip())

    risk_values = footer_map.get("risk", [])
    if not risk_values:
        errors.append("does not contain a valid risk trailer")
    else:
        risk = risk_values[-1].lower()
        if risk not in set(active_policy.risk_values):
            allowed = "|".join(active_policy.risk_values)
            errors.append(f"Unsupported risk value '{risk}'. Allowed: {allowed}.")
        if not footer_lines or not footer_lines[-1].lower().startswith("risk:"):
            errors.append("risk trailer must be the last footer line")
        errors.extend(_validate_footer_order(footer_lines))

    if active_policy.require_ai_share and "ai-code-share" not in footer_map:
        errors.append("AI-Code-Share trailer is required by git policy")

    if active_policy.require_co_authored_by and "co-authored-by" not in footer_map:
        errors.append("Co-authored-by trailer is required by git policy")

    return errors


def commit_with_message(
    *,
    workspace_root: Path,
    message: str,
    stage_all: bool,
) -> tuple[Path, list[str]]:
    repo_root = _resolve_git_root(workspace_root)
    if stage_all:
        _run_git(["add", "-A"], cwd=repo_root)

    staged_files = _staged_files(repo_root)
    if not staged_files:
        raise ValueError("No staged changes to commit. Stage files first or use --all.")

    _run_git(["commit", "-m", message], cwd=repo_root)
    return repo_root, staged_files


def _build_subject(commit_type: str, scope: str | None, title: str) -> str:
    ctype = commit_type.strip()
    if not ctype:
        raise ValueError("Commit type must not be empty.")
    if any(ch.isspace() for ch in ctype):
        raise ValueError("Commit type must not contain spaces.")
    scope_normalized = (scope or "").strip()
    if scope_normalized:
        return f"{ctype}({scope_normalized}): {title}"
    return f"{ctype}: {title}"


def _normalize_co_author_value(raw_value: str) -> str:
    value = raw_value.strip()
    if not value:
        raise ValueError("Empty co-author value.")
    value = _CO_AUTHOR_PREFIX_RE.sub("", value).strip()
    if not _CO_AUTHOR_VALUE_RE.fullmatch(value):
        raise ValueError(f"Invalid co-author format '{raw_value}'. Expected: Name <email>.")
    return value


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

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


def _validate_footer_order(footer_lines: list[str]) -> list[str]:
    errors: list[str] = []
    if not footer_lines:
        return errors

    keys = [line.split(":", 1)[0].strip().lower() for line in footer_lines]
    try:
        risk_index = len(keys) - 1 - keys[::-1].index("risk")
    except ValueError:
        return errors

    # JIRA trailers (if present) must be the block immediately before risk.
    jira_block_start = risk_index
    if "jira" in keys:
        idx = risk_index - 1
        if idx < 0 or keys[idx] != "jira":
            errors.append("JIRA trailer must be immediately before risk trailer")
        else:
            while idx >= 0 and keys[idx] == "jira":
                idx -= 1
            jira_block_start = idx + 1
        if any(key == "jira" for key in keys[:jira_block_start]):
            errors.append("JIRA trailers must form a single block before risk trailer")

    # Co-authored-by trailers (if present) must be the block immediately before JIRA/risk.
    if "co-authored-by" in keys:
        idx = jira_block_start - 1
        if idx < 0 or keys[idx] != "co-authored-by":
            errors.append("Co-authored-by trailer must be immediately before JIRA/risk trailers")
        else:
            while idx >= 0 and keys[idx] == "co-authored-by":
                idx -= 1
        if any(key == "co-authored-by" for key in keys[: idx + 1]):
            errors.append("Co-authored-by trailers must form a single block before JIRA/risk trailers")

    return errors


def _render_ai_share_line(parsed: list[tuple[str, int]]) -> str:
    parts = [f"{actor}={percent}%" for actor, percent in parsed]
    return f"AI-Code-Share: {', '.join(parts)}"


def _parse_ai_share_items(items: list[str]) -> list[tuple[str, int]]:
    parsed: list[tuple[str, int]] = []
    total = 0
    for raw in items:
        actor, percent = _parse_ai_share_item(raw)
        parsed.append((actor, percent))
        total += percent
    if total != 100:
        raise ValueError(f"AI share total must be 100, got {total}.")
    return parsed


def _parse_ai_share_item(raw_item: str) -> tuple[str, int]:
    item = raw_item.strip()
    if "=" not in item:
        raise ValueError(f"Invalid AI share '{raw_item}'. Expected actor=percent.")
    actor, percent_raw = item.split("=", 1)
    actor = actor.strip()
    percent_raw = percent_raw.strip()
    if not actor:
        raise ValueError(f"Invalid AI share '{raw_item}': actor must not be empty.")
    try:
        percent = int(percent_raw)
    except ValueError as exc:
        raise ValueError(f"Invalid AI share '{raw_item}': percent must be an integer.") from exc
    if percent < 0 or percent > 100:
        raise ValueError(f"Invalid AI share '{raw_item}': percent must be 0..100.")
    return actor, percent


def _resolve_git_root(path: Path) -> Path:
    completed = _run_git(["rev-parse", "--show-toplevel"], cwd=path)
    return Path(completed.stdout.strip()).resolve()


def _staged_files(repo_root: Path) -> list[str]:
    completed = _run_git(["diff", "--cached", "--name-only"], cwd=repo_root)
    return [line.strip() for line in completed.stdout.splitlines() if line.strip()]


def _run_git(argv: list[str], *, cwd: Path) -> subprocess.CompletedProcess[str]:
    cmd = ["git", *argv]
    completed = subprocess.run(cmd, cwd=cwd, text=True, capture_output=True, check=False)
    if completed.returncode != 0:
        stderr = completed.stderr.strip()
        stdout = completed.stdout.strip()
        details = stderr or stdout or f"git {' '.join(argv)} failed"
        raise RuntimeError(details)
    return completed
