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

from __future__ import annotations

import shutil
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import cast

import orjson

from aida_mcp.server import mcp_install
from aida_mcp.utils.aida_paths import get_repo_config_root
from aida_mcp.utils.repo_readiness import check_repo_readiness

# The env var that enables vendor/example embedded rules.
_VENDOR_RULES_ENV_KEY = "AIDA_ENABLE_EMBEDDED_VENDOR_RULES"


def _read_existing_vendor_rules_env(mcp_json_path: Path) -> str | None:
    """Read existing AIDA_ENABLE_EMBEDDED_VENDOR_RULES value from an mcp.json file.

    Returns the value if present, None otherwise.
    """
    if not mcp_json_path.exists():
        return None
    try:
        loaded: object = orjson.loads(mcp_json_path.read_text(encoding="utf-8"))
        if not isinstance(loaded, dict):
            return None
        data = cast(dict[str, object], loaded)

        servers_raw: object = data.get("mcpServers")
        if not isinstance(servers_raw, dict):
            return None
        servers = cast(dict[str, object], servers_raw)

        aida_raw: object = servers.get("aida")
        if not isinstance(aida_raw, dict):
            return None
        aida = cast(dict[str, object], aida_raw)

        env_raw: object = aida.get("env")
        if not isinstance(env_raw, dict):
            return None
        env = cast(dict[str, object], env_raw)

        value = env.get(_VENDOR_RULES_ENV_KEY)
        if isinstance(value, str):
            return value
    except Exception:
        pass
    return None


def _now_stamp() -> str:
    return datetime.now().strftime("%Y%m%d-%H%M%S")


def _read_template_text(filename: str) -> str:
    try:
        from importlib.resources import files

        return files("aida_mcp.onboarding_templates").joinpath(filename).read_text(encoding="utf-8")
    except Exception as exc:
        # Keep init robust even if packaging/resource lookup changes.
        return (
            f"# AIDA onboarding template missing\n\nFailed to load aida_mcp.onboarding_templates:{filename} ({exc})\n"
        )


def _render_cursor_rule() -> str:
    header = "---\ndescription: MCP-powered development workflow (AIDA)\nalwaysApply: true\n---\n\n"
    body = _read_template_text("workflow_body.md")
    return header + body


def _render_claude_rule() -> str:
    # Claude Code loads `./.claude/CLAUDE.md` automatically as project memory.
    header = "---\napply: always\n---\n\n"
    body = _read_template_text("workflow_body.md")
    return header + body


def _write_text_if_missing(path: Path, content: str, *, force: bool) -> bool:
    """Write a file if missing; if force=True, overwrite.

    Returns:
        True if file was written/overwritten, False otherwise.
    """
    if path.exists() and not force:
        return False
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")
    return True


def _placeholder_change_domains_yaml() -> str:
    return (
        "# AIDA change domain classification\n"
        "#\n"
        "# Define how changed paths map to validation domains.\n"
        "# See `.aida/validation_policy.yaml` for mapping domains to pipelines.\n"
        "domains: []\n"
    )


def _placeholder_validation_policy_yaml() -> str:
    return (
        "# AIDA validation policy\n"
        "#\n"
        "# This file wires domains -> pipelines -> steps (command_id + processor_id).\n"
        "# Initially empty (validate may no-op until you configure routes).\n"
        "validation_policy:\n"
        "  codegen: []\n"
        "  routes: []\n"
        "  pipelines: {}\n"
    )


def _placeholder_validation_registry_yaml() -> str:
    return (
        "# AIDA validation registry\n"
        "#\n"
        "# Define command_id and processor_id specifications referenced by validation_policy.yaml.\n"
        "registry:\n"
        "  includes: []\n"
        "  commands: {}\n"
        "  processors: {}\n"
    )


def _placeholder_git_policy_yaml() -> str:
    return (
        "# AIDA git policy\n"
        "#\n"
        "# Configure commit and git workflow policy used by AIDA tools.\n"
        "git_policy:\n"
        "  commit:\n"
        "    # Max allowed characters for the full commit subject line.\n"
        "    subject_max_chars: 70\n"
        "    # Allowed values for required `risk:` footer.\n"
        "    risk_values: [nonprod, low, high]\n"
        "    # Require AI contribution metadata (`AI-Code-Share`) in each commit.\n"
        "    require_ai_share: false\n"
        "    # Require `Co-authored-by:` footer in each commit.\n"
        "    require_co_authored_by: false\n"
        "    # Allow `--ai-share-auto <actor>` (deterministic actor=100).\n"
        "    enable_ai_share_auto: true\n"
        "  workflow:\n"
        "    # Prefer `git push --force-with-lease` over `--force` for unpublished branches.\n"
        "    prefer_force_with_lease: true\n"
        "    # Allow autosquash/rebase only for unpublished (not yet shared) commits.\n"
        "    autosquash_unpublished_only: true\n"
    )


@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, vendor_rules: bool = False) -> InitResult:
    wrote: list[str] = []
    skipped: list[str] = []

    config_root = get_repo_config_root(workspace_root)
    config_root_existed = config_root.exists()

    # Repo-owned config skeleton
    config_root.mkdir(parents=True, exist_ok=True)
    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()),
    ]
    for path, content in targets:
        # Safety: never overwrite existing repo-owned `.aida/*.yaml` configs. `--force` applies to
        # client files (Cursor/Claude) only.
        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)))

    # Project-scoped MCP config (Cursor + Claude Code)
    #
    # Keep the generated config portable: prefer resolving `aida-mcp` from PATH rather than pinning
    # to the current machine's absolute install location.
    portable_command = Path("aida-mcp")

    def _build_extra_env_for_mcp(mcp_path: Path) -> dict[str, str] | None:
        """Build extra_env for an mcp.json file, preserving existing vendor rules when --force is used."""
        env: dict[str, str] = {}
        if vendor_rules:
            # User explicitly requested vendor rules via --vendor-rules flag.
            env[_VENDOR_RULES_ENV_KEY] = "true"
        elif force and mcp_path.exists():
            # Tactical workaround: when --force overwrites an existing mcp.json without --vendor-rules,
            # preserve any existing AIDA_ENABLE_EMBEDDED_VENDOR_RULES value.
            existing_value = _read_existing_vendor_rules_env(mcp_path)
            if existing_value is not None:
                env[_VENDOR_RULES_ENV_KEY] = existing_value
        return env or None

    cursor_mcp = workspace_root / ".cursor" / "mcp.json"
    if cursor_mcp.exists() and not force:
        skipped.append(str(cursor_mcp.relative_to(workspace_root)))
    else:
        mcp_install.write_mcp_json_config(
            dest=cursor_mcp, command_path=portable_command, extra_env=_build_extra_env_for_mcp(cursor_mcp)
        )
        wrote.append(str(cursor_mcp.relative_to(workspace_root)))

    claude_project_mcp = workspace_root / ".mcp.json"
    if claude_project_mcp.exists() and not force:
        skipped.append(str(claude_project_mcp.relative_to(workspace_root)))
    else:
        mcp_install.write_mcp_json_config(
            dest=claude_project_mcp,
            command_path=portable_command,
            extra_env=_build_extra_env_for_mcp(claude_project_mcp),
        )
        wrote.append(str(claude_project_mcp.relative_to(workspace_root)))

    # Client instruction stubs (single always-on file per tool)
    cursor_rule = workspace_root / ".cursor" / "rules" / "aida.mdc"
    if _write_text_if_missing(cursor_rule, _render_cursor_rule(), force=force):
        wrote.append(str(cursor_rule.relative_to(workspace_root)))
    else:
        skipped.append(str(cursor_rule.relative_to(workspace_root)))

    claude_rule = workspace_root / ".claude" / "CLAUDE.md"
    if _write_text_if_missing(claude_rule, _render_claude_rule(), force=force):
        wrote.append(str(claude_rule.relative_to(workspace_root)))
    else:
        skipped.append(str(claude_rule.relative_to(workspace_root)))

    return InitResult(wrote_files=wrote, skipped_files=skipped)


def doctor_repo(*, workspace_root: Path) -> 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")

    # Client instruction hints (non-blocking)
    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]


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] = []

    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)
        # copy2 follows symlinks by default; this keeps backups readable even for symlinked inputs.
        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()

    # 1) Backup and replace client instruction files with thin stubs
    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)
    # Intentionally: we do not manage other tool instruction files (Copilot, etc.) in this phase.

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

    # Write fresh stubs (idempotent)
    if dry_run:
        print("[aida-mcp][migrate][dry-run] would write AIDA instruction stubs and project MCP configs")
    else:
        init_res = init_repo(workspace_root=workspace_root, command_path=command_path, force=force)
        wrote.extend(init_res.wrote_files)

    return MigrateResult(backups_dir=backups_dir, backed_up=backed_up, removed=removed, wrote=wrote)
