# Copyright (C) 2026 GoodData Corporation

"""Native rules export engine.

Generates self-contained rule files for AI tools (Claude, Cursor, Codex,
JetBrains, Junie) without requiring the AIDA MCP server at runtime.
"""

from __future__ import annotations

import contextlib
import hashlib
import json
import logging
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path

from aida_mcp.server.repo_onboarding_templates import (
    client_rule_file_relpaths,
    ensure_gitignore_contains,
)
from aida_mcp.tools.core.git_policy_config import GitPolicy, load_git_policy
from aida_mcp.tools.core.preferences_config import AidaPreferences, load_preferences
from aida_mcp.tools.rules.materialize import materialize_rules_dir
from aida_mcp.tools.rules.store import RuleStore
from aida_mcp.utils.aida_paths import get_repo_config_root

logger = logging.getLogger(__name__)

# Rule paths that need MCP-to-native transformation.
_WORKFLOW_ROUTER_PATH = "core/tools/workflow-router.mdc"
_VALIDATION_WORKFLOW_PATH = "core/tools/validation-workflow.mdc"

# Tool name → (output relative path, frontmatter renderer)
_TOOL_TARGETS: dict[str, str] = {
    "cursor": ".cursor/rules/aida.mdc",
    "claude": ".claude/CLAUDE.md",
    "codex": "AGENTS.md",
    "jetbrains": ".aiassistant/rules/aida.md",
    "junie": ".junie/guidelines.md",
}

ALL_TOOL_NAMES = tuple(_TOOL_TARGETS.keys())


@dataclass(frozen=True, slots=True)
class NativeExportResult:
    wrote_files: list[str]
    manifest_path: str


def export_native_rules(
    *,
    workspace_root: Path,
    tools: list[str] | None = None,
) -> NativeExportResult:
    """Generate self-contained native rule files for the specified tools."""
    target_tools = tools or list(ALL_TOOL_NAMES)

    # Load all inputs.
    preferences = load_preferences(workspace_root=workspace_root)
    git_policy = load_git_policy(workspace_root)
    rules_dir = materialize_rules_dir(workspace_root=workspace_root)
    store = RuleStore(rules_dir=rules_dir)
    store.load()

    # Build the unified content body (tool-agnostic).
    body = _build_native_body(
        store=store, preferences=preferences, git_policy=git_policy, workspace_root=workspace_root
    )

    # Write per-tool files.
    wrote: list[str] = []
    for tool in target_tools:
        relpath = _TOOL_TARGETS.get(tool)
        if not relpath:
            logger.warning("Unknown tool target: %s", tool)
            continue
        content = _render_for_tool(tool, body)
        out = workspace_root / relpath
        out.parent.mkdir(parents=True, exist_ok=True)
        out.write_text(content, encoding="utf-8")
        wrote.append(relpath)

    # Ensure outputs are gitignored.
    ws_gitignore = workspace_root / ".gitignore"
    for relpath in client_rule_file_relpaths():
        ensure_gitignore_contains(ws_gitignore, relpath)

    # Write staleness manifest.
    inputs_hash = compute_inputs_hash(workspace_root)
    manifest_path = _write_manifest(workspace_root, inputs_hash=inputs_hash, output_files=wrote)

    return NativeExportResult(wrote_files=wrote, manifest_path=str(manifest_path))


def compute_inputs_hash(workspace_root: Path) -> str:
    """Compute a hash of all inputs that affect native export output."""
    h = hashlib.sha256()

    # Hash rules content.
    rules_dir = materialize_rules_dir(workspace_root=workspace_root)
    store = RuleStore(rules_dir=rules_dir)
    store.load()
    for rule in sorted(store.get_all_rules(), key=lambda r: r.path):
        h.update(rule.path.encode())
        h.update(b"\0")
        h.update(rule.content.encode())
        h.update(b"\0")

    # Hash config files that affect output.
    config_root = get_repo_config_root(workspace_root)
    for config_name in (
        "preferences.yaml",
        "preferences.local.yaml",
        "git_policy.yaml",
        "git_policy.local.yaml",
        "rules_selection.yaml",
        "rules_selection.local.yaml",
    ):
        config_path = config_root / config_name
        if config_path.exists():
            with contextlib.suppress(OSError):
                h.update(config_path.read_bytes())
        h.update(b"\0")

    # Hash commit/PR templates.
    templates_dir = config_root / "templates"
    if templates_dir.is_dir():
        for tpl in sorted(templates_dir.iterdir()):
            if tpl.is_file():
                with contextlib.suppress(OSError):
                    h.update(tpl.read_bytes())
                h.update(b"\0")

    return h.hexdigest()


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _build_native_body(
    *,
    store: RuleStore,
    preferences: AidaPreferences,
    git_policy: GitPolicy,
    workspace_root: Path,
) -> str:
    """Build the tool-agnostic native rules body."""
    now = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
    parts: list[str] = [
        f"<!-- Generated by `aida-mcp rules export` on {now}. Do not edit manually. -->",
        "<!-- Re-run `aida-mcp rules export` to regenerate. -->",
        "",
    ]

    # Workflow section (replaces workflow-router.mdc).
    parts.append(_render_workflow_section(preferences, git_policy, workspace_root))

    # All other rules.
    for rule in sorted(store.get_all_rules(), key=lambda r: r.path):
        if rule.path in (_WORKFLOW_ROUTER_PATH, _VALIDATION_WORKFLOW_PATH):
            continue  # Already handled via the workflow section.
        parts.append("")
        parts.append(f"<!-- Rule: {rule.path} ({rule.origin}) -->")
        content = rule.content.strip()
        if content:
            parts.append(content)

    return "\n".join(parts)


def _render_workflow_section(
    preferences: AidaPreferences,
    git_policy: GitPolicy,
    workspace_root: Path,
) -> str:
    """Render the native workflow section replacing MCP workflow-router."""
    lines: list[str] = ["# Workflow", ""]

    # Validation subsection.
    lines.append("## Validation")
    lines.append("")
    if preferences.validation.pre_commit != "disabled":
        line = "- Before committing, run: `aida-mcp validate --scope pre_commit`"
        if preferences.validation.pre_commit == "ask":
            line += " (ask the user for confirmation before running)"
        lines.append(line)
    if preferences.validation.pre_push != "disabled":
        line = "- Before push/PR, run: `aida-mcp validate --scope pre_push`"
        if preferences.validation.pre_push == "ask":
            line += " (ask the user for confirmation before running)"
        lines.append(line)
    if preferences.validation.pre_commit == "disabled" and preferences.validation.pre_push == "disabled":
        lines.append("- Validation is disabled by preferences.")
    lines.append("")

    # Commits subsection.
    lines.append("## Commits")
    lines.append("")
    lines.append("- Do not commit, push, or create PRs unless the user explicitly asks.")
    if preferences.commit.mode != "disabled":
        cp = git_policy.commit
        lines.append(f"- Subject line max: {cp.subject_max_chars} characters.")
        lines.append("- Commit message format: `{type}({scope}): {title}`")
        if cp.risk_enabled:
            lines.append(f"- Include `{cp.risk_prefix}:` trailer with one of: {', '.join(cp.risk_values)}.")
        if cp.ticket_enabled:
            lines.append(f"- Include `{cp.ticket_prefix}:` trailer when a ticket ID is known.")
        if cp.require_co_authored_by:
            lines.append("- Include `Co-authored-by:` trailer.")

        # Show template if available.
        template_text = _load_commit_template(workspace_root, git_policy)
        if template_text:
            lines.append("")
            lines.append("Commit message template:")
            lines.append("```")
            lines.append(template_text.strip())
            lines.append("```")

        if preferences.commit.mode == "ask":
            lines.append("")
            lines.append(
                "- Show the constructed `git commit` command to the user and ask for confirmation before executing."
            )
    else:
        lines.append("- Commits are disabled by preferences.")
    lines.append("")

    # PR subsection.
    lines.append("## Pull Requests")
    lines.append("")
    if preferences.pr.mode != "disabled":
        lines.append("- Use `gh pr create` to create pull requests.")

        pr_title_tpl = _load_pr_title_template(workspace_root, git_policy)
        pr_body_tpl = _load_pr_body_template(workspace_root, git_policy)
        if pr_title_tpl:
            lines.append("")
            lines.append("PR title template:")
            lines.append("```")
            lines.append(pr_title_tpl.strip())
            lines.append("```")
        if pr_body_tpl:
            lines.append("")
            lines.append("PR body template:")
            lines.append("```")
            lines.append(pr_body_tpl.strip())
            lines.append("```")

        if preferences.pr.mode == "ask":
            lines.append("")
            lines.append(
                "- Show the constructed `gh pr create` command to the user and ask for confirmation before executing."
            )
    else:
        lines.append("- PR creation is disabled by preferences.")
    lines.append("")

    return "\n".join(lines)


def _load_commit_template(workspace_root: Path, git_policy: GitPolicy) -> str | None:
    if not git_policy.commit.template_file:
        return None
    config_root = get_repo_config_root(workspace_root)
    path = config_root / git_policy.commit.template_file
    try:
        return path.read_text(encoding="utf-8")
    except OSError:
        return None


def _load_pr_title_template(workspace_root: Path, git_policy: GitPolicy) -> str | None:
    if not git_policy.pr.title_template_file:
        return None
    config_root = get_repo_config_root(workspace_root)
    path = config_root / git_policy.pr.title_template_file
    try:
        return path.read_text(encoding="utf-8")
    except OSError:
        return None


def _load_pr_body_template(workspace_root: Path, git_policy: GitPolicy) -> str | None:
    if not git_policy.pr.body_template_file:
        return None
    config_root = get_repo_config_root(workspace_root)
    path = config_root / git_policy.pr.body_template_file
    try:
        return path.read_text(encoding="utf-8")
    except OSError:
        return None


def _render_for_tool(tool: str, body: str) -> str:
    """Add tool-specific frontmatter to the native body."""
    if tool == "cursor":
        header = "---\ndescription: AIDA development rules (native mode)\nalwaysApply: true\n---\n\n"
        return header + body
    if tool == "claude":
        header = "---\napply: always\n---\n\n"
        return header + body
    return body


def _write_manifest(workspace_root: Path, *, inputs_hash: str, output_files: list[str]) -> Path:
    config_root = get_repo_config_root(workspace_root)
    manifest_path = config_root / "native-export-manifest.json"
    manifest = {
        "version": 1,
        "exported_at": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "inputs_hash": inputs_hash,
        "output_files": output_files,
    }
    manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
    return manifest_path
