# Copyright (C) 2026 GoodData Corporation

"""Template loading/rendering helpers for repository policies."""

from __future__ import annotations

import re
from collections.abc import Iterable
from pathlib import Path
from string import Formatter

from aida_mcp.tools.core.git_policy_config import CommitPolicy, PrPolicy
from aida_mcp.utils.aida_paths import get_repo_config_root

_DEFAULT_PR_TITLE_TEMPLATE = "{type}{repository_part}: {title}"
_DEFAULT_PR_BODY_TEMPLATE = "## Summary\n{summary}\n\n## Test plan\n{test_plan}\n\n{co_authored_by}\n{ticket}\n{risk}"


def render_commit_message_template(
    *, workspace_root: Path, policy: CommitPolicy, variables: dict[str, str]
) -> str | None:
    template_file = policy.template_file
    if not template_file:
        return None
    text = _load_template_text(workspace_root=workspace_root, template_file=template_file)
    return _normalize_commit_template_render(
        _render_template(text, _commit_variables_with_policy(variables, policy)),
        policy=policy,
    )


def render_pr_text(
    *,
    workspace_root: Path,
    policy: PrPolicy,
    variables: dict[str, str],
) -> dict[str, str]:
    rendered_title = _render_pr_part(
        workspace_root=workspace_root,
        policy=policy,
        target="title",
        variables=variables,
        fallback_template=_DEFAULT_PR_TITLE_TEMPLATE,
    )
    rendered_body = _render_pr_part(
        workspace_root=workspace_root,
        policy=policy,
        target="body",
        variables=variables,
        fallback_template=_DEFAULT_PR_BODY_TEMPLATE,
    )
    return {"title": rendered_title, "body": rendered_body}


def validate_pr_text_against_template(
    *,
    workspace_root: Path,
    policy: PrPolicy,
    commit_policy: CommitPolicy,
    title: str,
    body: str,
) -> list[str]:
    title_template = _load_pr_template_or_default(
        workspace_root=workspace_root,
        policy=policy,
        target="title",
        fallback_template=_DEFAULT_PR_TITLE_TEMPLATE,
    )
    body_template = _load_pr_template_or_default(
        workspace_root=workspace_root,
        policy=policy,
        target="body",
        fallback_template=_DEFAULT_PR_BODY_TEMPLATE,
    )

    errors: list[str] = []
    title_has_unresolved = _contains_unfilled_placeholders(title_template, title)
    body_has_unresolved = _contains_unfilled_placeholders(body_template, body)
    if title_has_unresolved:
        errors.append("PR title contains unresolved template placeholders.")
    if body_has_unresolved:
        errors.append("PR body contains unresolved template placeholders.")

    parsed_title = None if title_has_unresolved else _extract_template_values(title_template, title, label="PR title")
    if not title_has_unresolved and parsed_title is None:
        detail = _template_mismatch_detail(
            template_text=title_template,
            rendered_text=title,
            label="PR title",
        )
        if detail:
            errors.append(f"PR title does not match configured template: {detail}")
        else:
            errors.append("PR title does not match configured template.")
    parsed_body = None if body_has_unresolved else _extract_template_values(body_template, body, label="PR body")
    if not body_has_unresolved and parsed_body is None:
        detail = _template_mismatch_detail(
            template_text=body_template,
            rendered_text=body,
            label="PR body",
        )
        if detail:
            errors.append(f"PR body does not match configured template: {detail}")
        else:
            errors.append("PR body does not match configured template.")
    _ = commit_policy
    _ = parsed_title
    _ = parsed_body
    return errors


def render_commit_template_preview(
    *, workspace_root: Path, policy: CommitPolicy, variables: dict[str, str]
) -> dict[str, str]:
    preview = render_commit_message_template(workspace_root=workspace_root, policy=policy, variables=variables) or ""
    if not preview:
        preview = _render_template("{type}{repository_part}: {title}\n\n{body}", variables)
    parsed = _split_commit_message(preview)
    return {
        "subject": parsed["subject"],
        "body": parsed["body"],
        "preview": preview,
    }


def build_commit_template_variables(
    *,
    commit_type: str,
    scope: str | None,
    title: str,
    body: str,
    risk: str,
    ticket_items: Iterable[str],
    co_authored_by_items: Iterable[str],
) -> dict[str, str]:
    scope_value = (scope or "").strip()
    ticket_list = [item.strip() for item in ticket_items if item.strip()]
    co_author_list = [item.strip() for item in co_authored_by_items if item.strip()]
    ticket_values = ", ".join(ticket_list)
    co_authored_values = ", ".join(co_author_list)
    co_authored_block = "\n".join([f"Co-authored-by: {item}" for item in co_author_list])
    risk_value = risk.strip().lower()
    repository_part = f"({scope_value})" if scope_value else ""
    return {
        "type": commit_type.strip(),
        "scope": scope_value,
        "repository_part": repository_part,
        "scope_suffix": repository_part,
        "title": title.strip(),
        "subject": _render_template(
            "{type}{repository_part}: {title}",
            {
                "type": commit_type.strip(),
                "repository_part": repository_part,
                "title": title.strip(),
            },
        ),
        "body": body.strip(),
        "risk_value": risk_value,
        "risk": risk_value,
        "ticket_values": ticket_values,
        "ticket": ticket_values,
        "co_authored_by_values": co_authored_values,
        "co_authored_by": co_authored_block,
    }


def build_pr_template_variables(
    *,
    title: str,
    ticket: str,
    risk: str,
    summary: str,
    test_plan: str,
    commit_type: str,
    scope: str,
    co_authored_by: str,
    ticket_enabled: bool = True,
    ticket_prefix: str = "JIRA",
    risk_enabled: bool = True,
    risk_prefix: str = "risk",
) -> dict[str, str]:
    ticket_clean = ticket.strip()
    scope_clean = scope.strip()
    repository_part = f"({scope_clean})" if scope_clean else ""
    risk_value = risk.strip().lower()
    co_authored_clean = co_authored_by.strip()
    if co_authored_clean and not co_authored_clean.lower().startswith("co-authored-by:"):
        co_authored_clean = f"Co-authored-by: {co_authored_clean}"
    return {
        "title": title.strip(),
        "ticket_id": ticket_clean,
        "ticket_ref_prefix": f"[{ticket_clean}] " if ticket_clean else "",
        "ticket": f"{ticket_prefix}: {ticket_clean}" if ticket_enabled and ticket_clean else "",
        "ticket_values": ticket_clean,
        "risk": f"{risk_prefix}: {risk_value}" if risk_enabled and risk_value else "",
        "risk_value": risk_value,
        "summary": summary.strip(),
        "test_plan": test_plan.strip(),
        "type": commit_type.strip(),
        "scope": scope_clean,
        "repository_part": repository_part,
        "co_authored_by": co_authored_clean,
    }


def _load_template_text(*, workspace_root: Path, template_file: str) -> str:
    config_root = get_repo_config_root(workspace_root)
    template_path = (config_root / template_file).resolve()
    if not template_path.is_file():
        raise ValueError(f"Template file not found: {template_path}")
    if config_root.resolve() not in template_path.parents:
        raise ValueError(f"Template path must stay within {config_root}: {template_file}")
    return template_path.read_text(encoding="utf-8")


class _SafeTemplateVars(dict[str, str]):
    def __missing__(self, key: str) -> str:
        raise ValueError(f"Unknown template variable '{{{key}}}'.")


def _render_template(template_text: str, variables: dict[str, str]) -> str:
    normalized = _SafeTemplateVars(dict(variables))
    try:
        rendered = template_text.format_map(normalized).strip()
    except ValueError:
        raise
    except Exception as exc:  # pragma: no cover - defensive
        raise ValueError(f"Invalid template: {exc}") from exc
    return rendered


def _commit_variables_with_policy(variables: dict[str, str], policy: CommitPolicy) -> dict[str, str]:
    resolved = dict(variables)
    ticket_values = resolved.get("ticket_values", "").strip()
    risk_value = resolved.get("risk_value", resolved.get("risk", "")).strip().lower()
    resolved["ticket"] = f"{policy.ticket_prefix}: {ticket_values}" if policy.ticket_enabled and ticket_values else ""
    resolved["risk"] = f"{policy.risk_prefix}: {risk_value}" if policy.risk_enabled and risk_value else ""
    return resolved


_EMPTY_TRAILER_LABEL_RE = re.compile(r"^[A-Za-z][A-Za-z0-9-]*:\s*$")


def _normalize_commit_template_render(rendered_text: str, *, policy: CommitPolicy) -> str:
    lines = rendered_text.splitlines()
    normalized_lines = [line for line in lines if not _EMPTY_TRAILER_LABEL_RE.fullmatch(line.strip())]
    collapsed_lines = _collapse_blank_lines_inside_footer_block(normalized_lines, policy=policy)
    return "\n".join(collapsed_lines).strip()


def _collapse_blank_lines_inside_footer_block(lines: list[str], *, policy: CommitPolicy) -> list[str]:
    last = len(lines) - 1
    while last >= 0 and not lines[last].strip():
        last -= 1
    if last < 0:
        return []
    if not _looks_like_policy_footer(lines[last].strip(), policy=policy):
        return lines

    footer_lines_reversed: list[str] = []
    idx = last
    while idx >= 0:
        line = lines[idx].strip()
        if not line:
            idx -= 1
            continue
        if not _looks_like_policy_footer(line, policy=policy):
            break
        footer_lines_reversed.append(line)
        idx -= 1

    footer_lines = list(reversed(footer_lines_reversed))
    if not footer_lines:
        return lines

    prefix_lines = lines[: idx + 1]
    while prefix_lines and not prefix_lines[-1].strip():
        prefix_lines.pop()
    if not prefix_lines:
        return footer_lines
    return [*prefix_lines, "", *footer_lines]


def _contains_unfilled_placeholders(template_text: str, rendered_text: str) -> bool:
    for key in _extract_template_keys(template_text):
        token = f"{{{key}}}"
        if token in rendered_text:
            return True
    return False


def _extract_template_values(template_text: str, rendered_text: str, *, label: str) -> dict[str, str] | None:
    normalized_template = template_text.strip()
    normalized_rendered = rendered_text.strip()
    if not normalized_template:
        return {} if not normalized_rendered else None

    formatter = Formatter()
    parts: list[str] = ["^"]
    group_to_field: list[tuple[str, str]] = []
    for literal_text, field_name, _format_spec, _conversion in formatter.parse(normalized_template):
        if literal_text:
            parts.append(re.escape(literal_text))
        if field_name is not None:
            group_name = f"field_{len(group_to_field)}"
            parts.append(f"(?P<{group_name}>{_placeholder_pattern(field_name)})")
            group_to_field.append((group_name, field_name))
    parts.append("$")
    pattern = re.compile("".join(parts), re.DOTALL)
    match = pattern.fullmatch(normalized_rendered)
    if match is None:
        return None

    extracted: dict[str, str] = {}
    for group_name, field_name in group_to_field:
        value = match.group(group_name)
        existing = extracted.get(field_name, "")
        if existing and value and existing != value:
            raise ValueError(f"{label} contains conflicting values for template field {{{field_name}}}.")
        if value:
            extracted[field_name] = value
    return extracted


def _placeholder_pattern(field_name: str) -> str:
    if field_name == "ticket_ref_prefix":
        return r"(?:\[[^\]\n]+\]\s*)?"
    if field_name in {"ticket", "risk", "ticket_id"}:
        return r"[^\n]*"
    return r"[\s\S]*?"


def _load_pr_template_or_default(
    *,
    workspace_root: Path,
    policy: PrPolicy,
    target: str,
    fallback_template: str,
) -> str:
    template_file = policy.title_template_file if target == "title" else policy.body_template_file
    if not template_file:
        return fallback_template
    return _load_template_text(workspace_root=workspace_root, template_file=template_file)


def _render_pr_part(
    *,
    workspace_root: Path,
    policy: PrPolicy,
    target: str,
    variables: dict[str, str],
    fallback_template: str,
) -> str:
    template_text = _load_pr_template_or_default(
        workspace_root=workspace_root,
        policy=policy,
        target=target,
        fallback_template=fallback_template,
    )
    return _render_template(template_text, variables)


def _extract_template_keys(template_text: str) -> set[str]:
    formatter = Formatter()
    keys: set[str] = set()
    for _, field_name, _, _ in formatter.parse(template_text):
        if field_name:
            keys.add(field_name)
    return keys


def _template_mismatch_detail(*, template_text: str, rendered_text: str, label: str) -> str | None:
    normalized_template = template_text.replace("\r\n", "\n").replace("\r", "\n").strip("\n")
    normalized_rendered = rendered_text.replace("\r\n", "\n").replace("\r", "\n").strip("\n")

    heading_lines = [line.strip() for line in normalized_template.splitlines() if line.strip().startswith("## ")]
    missing_headings = [heading for heading in heading_lines if heading not in normalized_rendered]
    if missing_headings:
        headings = ", ".join([f"`{heading}`" for heading in missing_headings])
        return f"{label} is missing required section heading(s): {headings}."

    for heading in heading_lines:
        expected_block = f"\n\n{heading}\n"
        if expected_block in normalized_template:
            has_single_newline = f"\n{heading}\n" in normalized_rendered
            has_expected_blank = expected_block in normalized_rendered
            if has_single_newline and not has_expected_blank:
                return f"{label} is missing a blank line before `{heading}`."

    template_lines = normalized_template.splitlines()
    rendered_lines = normalized_rendered.splitlines()
    shared = min(len(template_lines), len(rendered_lines))
    for idx in range(shared):
        template_line = template_lines[idx]
        rendered_line = rendered_lines[idx]
        if _line_contains_placeholders(template_line):
            continue
        if template_line != rendered_line:
            line_no = idx + 1
            return f"{label} first mismatch at line {line_no}: expected `{template_line}` but found `{rendered_line}`."

    if len(template_lines) != len(rendered_lines):
        return f"{label} has {len(rendered_lines)} line(s), but template expects {len(template_lines)} line(s)."
    return None


def _line_contains_placeholders(line: str) -> bool:
    formatter = Formatter()
    return any(field_name is not None for _literal, field_name, _format_spec, _conversion in formatter.parse(line))


def _split_commit_message(message: str) -> dict[str, str]:
    lines = message.rstrip("\n").split("\n")
    subject = lines[0].strip() if lines else ""
    if len(lines) <= 1:
        return {"subject": subject, "body": ""}

    trailer_start = len(lines)
    idx = len(lines) - 1
    while idx >= 0 and not lines[idx].strip():
        idx -= 1
    while idx >= 0 and _looks_like_footer(lines[idx].strip()):
        trailer_start = idx
        idx -= 1

    body_lines = lines[1:trailer_start]
    return {"subject": subject, "body": "\n".join(body_lines).strip()}


def _looks_like_footer(line: str) -> bool:
    if ":" not in line:
        return False
    key, value = line.split(":", 1)
    return bool(key and key[0].isalpha() and value.strip())


def _looks_like_policy_footer(line: str, *, policy: CommitPolicy) -> bool:
    if not _looks_like_footer(line):
        return False
    key, _ = line.split(":", 1)
    normalized_key = key.strip().lower()
    allowed = {"co-authored-by"}
    if policy.ticket_enabled:
        allowed.add(policy.ticket_prefix.lower())
    if policy.risk_enabled:
        allowed.add(policy.risk_prefix.lower())
    return normalized_key in allowed
