# Copyright (C) 2026 GoodData Corporation

"""Repository onboarding commands: init / doctor / migrate."""

from __future__ import annotations

import shutil
from dataclasses import dataclass
from pathlib import Path

from aida_mcp.server import mcp_install
from aida_mcp.server.repo_onboarding_detection import (
    codex_toml_has_aida_server,
    mcp_json_has_aida_server,
)
from aida_mcp.server.repo_onboarding_history import infer_template_hints
from aida_mcp.server.repo_onboarding_templates import (
    now_stamp,
    placeholder_change_domains_yaml,
    placeholder_commit_message_template,
    placeholder_git_policy_yaml,
    placeholder_pr_body_template,
    placeholder_pr_title_template,
    placeholder_rules_selection_yaml,
    placeholder_validation_policy_yaml,
    placeholder_validation_registry_yaml,
    reconcile_aida_gitignore,
    render_agents_rule,
    render_claude_rule,
    render_cursor_rule,
    render_jetbrains_ai_rule,
    render_junie_guidelines,
    write_text_if_missing,
)
from aida_mcp.utils.aida_paths import get_repo_config_root
from aida_mcp.utils.repo_config_audit import audit_repo_configs
from aida_mcp.utils.repo_config_auto_fix import auto_fix_repo_configs
from aida_mcp.utils.repo_config_reconcile import reconcile_repo_configs
from aida_mcp.utils.repo_config_versions import collect_repo_config_statuses, migrate_repo_config_versions
from aida_mcp.utils.repo_readiness import check_repo_readiness


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


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


def init_repo(
    *,
    workspace_root: Path,
    command_path: Path,
    force: bool,
    http_url: str | None = None,
) -> InitResult:
    wrote: list[str] = []
    skipped: list[str] = []

    config_root = get_repo_config_root(workspace_root)
    config_root_existed = config_root.exists()
    config_root.mkdir(parents=True, exist_ok=True)
    detected_profiles = _detect_embedded_profiles(workspace_root)
    template_hints = infer_template_hints(workspace_root)
    targets: list[tuple[Path, str]] = [
        (config_root / "change_domains.yaml", placeholder_change_domains_yaml()),
        (config_root / "validation_policy.yaml", placeholder_validation_policy_yaml()),
        (config_root / "validation_registry.yaml", placeholder_validation_registry_yaml()),
        (config_root / "git_policy.yaml", placeholder_git_policy_yaml()),
        (
            config_root / "templates" / "commit-message.txt",
            placeholder_commit_message_template(hints=template_hints),
        ),
        (config_root / "templates" / "pr-title.txt", placeholder_pr_title_template(hints=template_hints)),
        (config_root / "templates" / "pr-body.md", placeholder_pr_body_template(hints=template_hints)),
        (
            config_root / "rules_selection.yaml",
            placeholder_rules_selection_yaml(embedded_include_paths=detected_profiles),
        ),
    ]
    for path, content in targets:
        effective_force = False if config_root_existed else force
        if write_text_if_missing(path, content, force=effective_force):
            wrote.append(str(path.relative_to(workspace_root)))
        else:
            skipped.append(str(path.relative_to(workspace_root)))

    gitignore_path = config_root / ".gitignore"
    gitignore_updates = reconcile_aida_gitignore(gitignore_path, dry_run=False)
    if gitignore_updates:
        wrote.append(str(gitignore_path.relative_to(workspace_root)))
    else:
        skipped.append(str(gitignore_path.relative_to(workspace_root)))

    portable_command = Path("aida-mcp")

    cursor_mcp = workspace_root / ".cursor" / "mcp.json"
    if cursor_mcp.exists() and not force and mcp_json_has_aida_server(cursor_mcp):
        skipped.append(str(cursor_mcp.relative_to(workspace_root)))
    else:
        mcp_install.install_for_cursor_project(
            project_root=workspace_root,
            command_path=portable_command,
            http_url=http_url,
        )
        wrote.append(str(cursor_mcp.relative_to(workspace_root)))

    claude_project_mcp = workspace_root / ".mcp.json"
    if claude_project_mcp.exists() and not force and mcp_json_has_aida_server(claude_project_mcp):
        skipped.append(str(claude_project_mcp.relative_to(workspace_root)))
    else:
        mcp_install.install_for_claude_project(
            project_root=workspace_root,
            command_path=portable_command,
            http_url=http_url,
        )
        wrote.append(str(claude_project_mcp.relative_to(workspace_root)))

    codex_project_config = workspace_root / ".codex" / "config.toml"
    if codex_project_config.exists() and not force and codex_toml_has_aida_server(codex_project_config):
        skipped.append(str(codex_project_config.relative_to(workspace_root)))
    else:
        mcp_install.install_for_codex_project(
            project_root=workspace_root,
            command_path=portable_command,
            http_url=http_url,
        )
        wrote.append(str(codex_project_config.relative_to(workspace_root)))

    rules_result = update_rules(workspace_root=workspace_root, force=force)
    wrote.extend(rules_result.wrote_files)
    skipped.extend(rules_result.skipped_files)

    return InitResult(wrote_files=wrote, skipped_files=skipped)


def update_rules(*, workspace_root: Path, force: bool = True) -> UpdateRulesResult:
    """Write client rule/instruction files from onboarding templates.

    By default overwrites existing files (force=True) since the canonical
    source is the packaged template, not the repo copy.
    """
    wrote: list[str] = []
    skipped: list[str] = []

    targets: list[tuple[Path, str]] = [
        (workspace_root / ".cursor" / "rules" / "aida.mdc", render_cursor_rule()),
        (workspace_root / ".claude" / "CLAUDE.md", render_claude_rule()),
        (workspace_root / "AGENTS.md", render_agents_rule()),
        (workspace_root / ".aiassistant" / "rules" / "aida.md", render_jetbrains_ai_rule()),
        (workspace_root / ".junie" / "guidelines.md", render_junie_guidelines()),
    ]
    for path, content in targets:
        if write_text_if_missing(path, content, force=force):
            wrote.append(str(path.relative_to(workspace_root)))
        else:
            skipped.append(str(path.relative_to(workspace_root)))

    return UpdateRulesResult(wrote_files=wrote, skipped_files=skipped)


def _detect_embedded_profiles(workspace_root: Path) -> list[str]:
    profile_paths: set[str] = set()

    python_markers = ("pyproject.toml", "requirements.txt", "requirements-dev.txt", "setup.py")
    if any((workspace_root / marker).exists() for marker in python_markers):
        profile_paths.add("profiles/languages/python/**")

    gradle_markers = ("build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts")
    has_gradle = any((workspace_root / marker).exists() for marker in gradle_markers)
    if has_gradle:
        profile_paths.add("profiles/build/gradle/**")

    kotlin_markers = ("build.gradle.kts", "settings.gradle.kts")
    if any((workspace_root / marker).exists() for marker in kotlin_markers):
        profile_paths.add("profiles/languages/kotlin/**")

    spring_markers = ("build.gradle", "build.gradle.kts", "pom.xml")
    for marker in spring_markers:
        marker_path = workspace_root / marker
        if not marker_path.exists():
            continue
        try:
            text = marker_path.read_text(encoding="utf-8")
        except OSError:
            continue
        if "spring-boot" in text.lower():
            profile_paths.add("profiles/frameworks/spring-boot/**")
            break

    return sorted(profile_paths)


def doctor_repo(*, workspace_root: Path, auto_fix: bool = False) -> int:
    readiness = check_repo_readiness(workspace_root=workspace_root)
    ok = True
    print(f"[aida-mcp][doctor] workspace_root: {workspace_root}")
    print(f"[aida-mcp][doctor] config_root: {readiness.config_root}")
    if not readiness.config_root_exists:
        ok = False
        print("[aida-mcp][doctor][error] Repo is not initialized for AIDA (.aida missing).")
        print("[aida-mcp][doctor][next] Run: aida-mcp init")
    else:
        print("[aida-mcp][doctor] .aida/ exists")
    if readiness.missing_validate_files:
        ok = False
        missing = ", ".join(readiness.missing_validate_files)
        print(f"[aida-mcp][doctor][error] Missing required validate config file(s): {missing}")
        print("[aida-mcp][doctor][next] Run: aida-mcp init (or create the files manually)")
    else:
        print("[aida-mcp][doctor] validate config files present")
    outdated_statuses = [s for s in collect_repo_config_statuses(workspace_root=workspace_root) if s.outdated]
    if outdated_statuses:
        outdated = ", ".join(
            f"{status.name} (found {status.version or 'unversioned'}, latest {status.latest_version})"
            for status in outdated_statuses
        )
        print(f"[aida-mcp][doctor][warn] Outdated AIDA config version(s): {outdated}")
        print("[aida-mcp][doctor][next] Run: aida-mcp migrate")
    else:
        print("[aida-mcp][doctor] repo config versions are current")
    audit_findings = audit_repo_configs(workspace_root=workspace_root)
    if audit_findings:
        print("[aida-mcp][doctor][warn] Repo config audit findings:")
        for finding in audit_findings:
            print(f"[aida-mcp][doctor][warn] {finding.file}: {finding.message}")
    else:
        print("[aida-mcp][doctor] repo config audit is clean")
    gitignore_updates = reconcile_aida_gitignore(
        get_repo_config_root(workspace_root) / ".gitignore",
        dry_run=True,
    )
    if gitignore_updates:
        print("[aida-mcp][doctor][warn] .aida/.gitignore is missing local override entries:")
        for update in gitignore_updates:
            print(f"[aida-mcp][doctor][warn] {update}")
    if auto_fix:
        fixes = auto_fix_repo_configs(workspace_root=workspace_root, dry_run=False)
        fixes.extend(reconcile_repo_configs(workspace_root=workspace_root, dry_run=False))
        fixes.extend(reconcile_aida_gitignore(get_repo_config_root(workspace_root) / ".gitignore", dry_run=False))
        if fixes:
            print("[aida-mcp][doctor][fix] Applied safe config fixes:")
            for fix in fixes:
                print(f"[aida-mcp][doctor][fix] {fix}")
            remaining_findings = audit_repo_configs(workspace_root=workspace_root)
            if remaining_findings:
                print("[aida-mcp][doctor][warn] Remaining repo config audit findings after auto-fix:")
                for finding in remaining_findings:
                    print(f"[aida-mcp][doctor][warn] {finding.file}: {finding.message}")
            else:
                print("[aida-mcp][doctor] repo config audit is clean after auto-fix")
        else:
            print("[aida-mcp][doctor][fix] No safe config fixes available.")
    cursor_rules_dir = workspace_root / ".cursor" / "rules"
    if cursor_rules_dir.exists():
        legacy = [p for p in cursor_rules_dir.rglob("*.mdc") if p.name != "aida.mdc"]
        if legacy:
            print(
                f"[aida-mcp][doctor][warn] Found existing Cursor rule files under {cursor_rules_dir} ({len(legacy)} files)."
            )
            print("[aida-mcp][doctor][warn] If they conflict with AIDA, consider migrating them (aida-mcp migrate).")
    dot_claude = workspace_root / ".claude" / "CLAUDE.md"
    root_claude = workspace_root / "CLAUDE.md"
    if dot_claude.exists() and root_claude.exists():
        print(
            "[aida-mcp][doctor][warn] Both `CLAUDE.md` and `.claude/CLAUDE.md` exist; Claude may pick up conflicting guidance."
        )
    return 0 if ok else 2


@dataclass(frozen=True, slots=True)
class MigrateResult:
    backups_dir: Path
    backed_up: list[str]
    removed: list[str]
    wrote: list[str]
    config_updates: list[str]


def migrate_repo(
    *,
    workspace_root: Path,
    command_path: Path,
    client_only: bool,
    dry_run: bool,
    force: bool,
) -> MigrateResult:
    stamp = now_stamp()
    config_root = get_repo_config_root(workspace_root)
    backups_dir = config_root / "migrate-backups" / stamp
    backed_up: list[str] = []
    removed: list[str] = []
    wrote: list[str] = []
    config_updates: list[str] = []

    def _backup_one(path: Path) -> None:
        rel = str(path.relative_to(workspace_root))
        dest = backups_dir / rel
        backed_up.append(rel)
        if dry_run:
            return
        dest.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(path, dest)

    def _remove_one(path: Path) -> None:
        rel = str(path.relative_to(workspace_root))
        removed.append(rel)
        if dry_run:
            return
        path.unlink()

    candidates: list[Path] = []
    cursor_rules_dir = workspace_root / ".cursor" / "rules"
    if cursor_rules_dir.exists():
        candidates.extend([p for p in cursor_rules_dir.rglob("*.mdc") if p.is_file()])
    dot_claude = workspace_root / ".claude" / "CLAUDE.md"
    if dot_claude.exists():
        candidates.append(dot_claude)
    root_claude = workspace_root / "CLAUDE.md"
    if root_claude.exists():
        candidates.append(root_claude)
    root_agents = workspace_root / "AGENTS.md"
    if root_agents.exists():
        candidates.append(root_agents)
    jetbrains_ai = workspace_root / ".aiassistant" / "rules" / "aida.md"
    if jetbrains_ai.exists():
        candidates.append(jetbrains_ai)
    junie_guidelines = workspace_root / ".junie" / "guidelines.md"
    if junie_guidelines.exists():
        candidates.append(junie_guidelines)

    for p in sorted(set(candidates)):
        _backup_one(p)
        _remove_one(p)

    if dry_run:
        print("[aida-mcp][migrate][dry-run] would write AIDA instruction stubs and project MCP configs")
    else:
        wrote.extend(migrate_repo_config_versions(workspace_root=workspace_root, dry_run=False))
        config_updates.extend(reconcile_repo_configs(workspace_root=workspace_root, dry_run=False))
        gitignore_updates = reconcile_aida_gitignore(config_root / ".gitignore", dry_run=False)
        config_updates.extend(gitignore_updates)
        init_res = init_repo(
            workspace_root=workspace_root,
            command_path=command_path,
            force=force,
        )
        wrote.extend(init_res.wrote_files)
    if dry_run:
        wrote.extend(migrate_repo_config_versions(workspace_root=workspace_root, dry_run=True))
        config_updates.extend(reconcile_repo_configs(workspace_root=workspace_root, dry_run=True))
        config_updates.extend(reconcile_aida_gitignore(config_root / ".gitignore", dry_run=True))
    return MigrateResult(
        backups_dir=backups_dir,
        backed_up=backed_up,
        removed=removed,
        wrote=wrote,
        config_updates=config_updates,
    )
