# Copyright (C) 2026 GoodData Corporation

"""CLI handlers for commit and alias commands."""

from __future__ import annotations

import argparse
import asyncio
import json
import shutil
import sys
from pathlib import Path
from typing import cast

from aida_mcp.server import mcp_install
from aida_mcp.server.commit_cli import CommitMessageInput, commit_with_fixup, commit_with_message, render_commit_message
from aida_mcp.server.commit_plan import CommitCommandPlan, resolve_commit_plan
from aida_mcp.server.commit_policy_checks import check_commit_policy_range
from aida_mcp.tools.core.git_policy_config import load_git_policy
from aida_mcp.tools.core.template_loader import (
    build_commit_template_variables,
    build_pr_template_variables,
    render_commit_template_preview,
    render_pr_text,
    validate_pr_text_against_template,
)
from aida_mcp.tools.validate.engine import execute_plan
from aida_mcp.utils.git import list_changed_files
from aida_mcp.utils.repo_readiness import check_repo_readiness


def commit_cmd(args: argparse.Namespace) -> None:
    workspace_root = Path(args.workspace_root).expanduser().resolve() if args.workspace_root else Path.cwd().resolve()
    body_value = _read_body_from_args(args)
    fixup_of = str(args.fixup_of).strip() if getattr(args, "fixup_of", None) else None

    if not fixup_of and not getattr(args, "title", None):
        raise ValueError("title is required unless --fixup-of is provided.")

    if bool(getattr(args, "plan", False)):
        plan = resolve_commit_plan(
            workspace_root=workspace_root,
            commit_type=str(args.type),
            scope=str(args.scope) if args.scope else None,
            title=str(args.title) if args.title is not None else None,
            body=body_value or None,
            risk=str(args.risk) if args.risk is not None else None,
            tickets=[str(x) for x in (args.ticket or [])],
            co_authored_by=[str(x) for x in (args.co_authored_by or [])],
            fixup_of=fixup_of,
        )
        _print_commit_plan(plan=plan, as_json=bool(getattr(args, "json", False)))
        return

    if fixup_of and bool(args.dry_run):
        plan = resolve_commit_plan(
            workspace_root=workspace_root,
            commit_type=str(args.type),
            scope=str(args.scope) if args.scope else None,
            title=None,
            body=body_value or None,
            risk=None,
            tickets=[],
            co_authored_by=[],
            fixup_of=fixup_of,
        )
        print("[aida-mcp][commit] dry-run: would commit with command:")
        print(plan.command)
        return

    git_policy = load_git_policy(workspace_root)

    message = None
    if not fixup_of:
        message = render_commit_message(
            CommitMessageInput(
                commit_type=str(args.type),
                scope=str(args.scope) if args.scope else None,
                title=str(args.title),
                body=body_value or None,
                risk=str(args.risk) if args.risk is not None else None,
                tickets=[str(x) for x in (args.ticket or [])],
                co_authored_by=[str(x) for x in (args.co_authored_by or [])],
            ),
            policy=git_policy.commit,
            workspace_root=workspace_root,
        )

    if bool(args.dry_run) and message is not None:
        print("[aida-mcp][commit] dry-run: would commit with message:")
        print("---")
        print(message.rstrip())
        print("---")
        return

    validation = _run_pre_commit_validation(workspace_root=workspace_root)
    if not bool(validation.get("success", False)):
        print("[aida-mcp][commit] pre-commit validation failed")
        validation_text = str(validation.get("text", "")).strip()
        if validation_text:
            print(f"[aida-mcp][commit] details: {validation_text}")
        raise SystemExit(1)
    if bool(validation.get("did_autofix", False)):
        modified = validation.get("modified_files")
        print("[aida-mcp][commit] pre-commit validation changed files")
        if isinstance(modified, list):
            modified_paths = [str(item) for item in modified if isinstance(item, str)]
            for item in modified_paths:
                print(f"  - {item}")
        print("[aida-mcp][commit] continuing with automatic staging of current changes")
    print("[aida-mcp][commit] pre-commit validation passed")

    if fixup_of:
        repo_root, staged_files = commit_with_fixup(
            workspace_root=workspace_root,
            fixup_of=fixup_of,
            body=body_value or None,
        )
    else:
        repo_root, staged_files = commit_with_message(
            workspace_root=workspace_root,
            message=cast(str, message),
        )
    print(f"[aida-mcp][commit] committed in {repo_root}")
    print("[aida-mcp][commit] files:")
    for file_path in staged_files:
        print(f"  - {file_path}")


def check_commit_policy_cmd(args: argparse.Namespace) -> None:
    workspace_root = Path(args.workspace_root).expanduser().resolve() if args.workspace_root else Path.cwd().resolve()
    result = check_commit_policy_range(
        workspace_root=workspace_root, from_ref=str(args.from_ref), to_ref=str(args.to_ref)
    )
    if bool(args.json):
        import json

        print(json.dumps(result, indent=2, sort_keys=True))
    else:
        if result["success"]:
            print(f"[aida-mcp][check-commit-policy] OK ({result['checked_commits']} commit(s) checked)")
        else:
            print(f"[aida-mcp][check-commit-policy] FAILED ({result['checked_commits']} commit(s) checked)")
            violations_obj = result.get("violations", [])
            violations = violations_obj if isinstance(violations_obj, list) else []
            for item_obj in violations:
                if not isinstance(item_obj, dict):
                    continue
                item = cast(dict[str, object], item_obj)
                print(f"  - {item.get('sha')}")
                errors_obj = item.get("errors", [])
                errors = errors_obj if isinstance(errors_obj, list) else []
                for err in errors:
                    print(f"    * {err}")
    if not result["success"]:
        raise SystemExit(1)


def check_pr_policy_cmd(args: argparse.Namespace) -> None:
    workspace_root = Path(args.workspace_root).expanduser().resolve() if args.workspace_root else Path.cwd().resolve()
    git_policy = load_git_policy(workspace_root)
    body = str(args.body)
    if args.body_file:
        body = Path(args.body_file).read_text(encoding="utf-8")

    errors = validate_pr_text_against_template(
        workspace_root=workspace_root,
        policy=git_policy.pr,
        commit_policy=git_policy.commit,
        title=str(args.title),
        body=body,
    )
    result = {"success": not errors, "errors": errors}
    if bool(args.json):
        import json

        print(json.dumps(result, indent=2, sort_keys=True))
    else:
        if result["success"]:
            print("[aida-mcp][check-pr-policy] OK")
        else:
            print("[aida-mcp][check-pr-policy] FAILED")
            for error in errors:
                print(f"  * {error}")
    if not result["success"]:
        raise SystemExit(1)


def preview_commit_template_cmd(args: argparse.Namespace) -> None:
    workspace_root = Path(args.workspace_root).expanduser().resolve() if args.workspace_root else Path.cwd().resolve()
    git_policy = load_git_policy(workspace_root)
    variables = build_commit_template_variables(
        commit_type=str(args.type),
        scope=str(args.scope or ""),
        title=str(args.title),
        body=_read_body_from_args(args),
        risk=str(args.risk),
        ticket_items=[str(x) for x in (args.ticket or [])],
        co_authored_by_items=[str(x) for x in (args.co_authored_by or [])],
    )
    preview = render_commit_template_preview(
        workspace_root=workspace_root, policy=git_policy.commit, variables=variables
    )
    if bool(args.json):
        import json

        print(json.dumps(preview, indent=2, sort_keys=True))
        return
    print("[aida-mcp][preview-commit-template] subject:")
    print(preview["subject"])
    print("[aida-mcp][preview-commit-template] body:")
    print("---")
    print(preview["body"])
    print("---")
    print("[aida-mcp][preview-commit-template] full:")
    print("---")
    print(preview["preview"])
    print("---")


def preview_pr_template_cmd(args: argparse.Namespace) -> None:
    workspace_root = Path(args.workspace_root).expanduser().resolve() if args.workspace_root else Path.cwd().resolve()
    git_policy = load_git_policy(workspace_root)
    variables = build_pr_template_variables(
        title=str(args.title),
        ticket=str(args.ticket or ""),
        risk=str(args.risk),
        summary=str(args.summary),
        test_plan=str(args.test_plan),
        commit_type=str(args.type),
        scope=str(args.scope or ""),
        co_authored_by=str(args.co_authored_by or ""),
        ticket_enabled=git_policy.commit.ticket_enabled,
        ticket_prefix=git_policy.commit.ticket_prefix,
        risk_enabled=git_policy.commit.risk_enabled,
        risk_prefix=git_policy.commit.risk_prefix,
    )
    preview = render_pr_text(workspace_root=workspace_root, policy=git_policy.pr, variables=variables)
    if bool(args.json):
        import json

        print(json.dumps(preview, indent=2, sort_keys=True))
        return
    print("[aida-mcp][preview-pr-template] title:")
    print(preview["title"])
    print("[aida-mcp][preview-pr-template] body:")
    print("---")
    print(preview["body"])
    print("---")


def alias_status_cmd(args: argparse.Namespace) -> None:
    name = str(args.name or "aida")
    resolved_on_path = shutil.which(name)

    command_path = mcp_install.resolve_self_command_path(sys.argv[0])
    scripts_dir = command_path.parent
    alias_path = scripts_dir / name

    print(f"[aida-mcp][alias] name: {name}")
    print(f"[aida-mcp][alias] aida-mcp: {command_path}")
    print(f"[aida-mcp][alias] scripts_dir: {scripts_dir}")
    print(f"[aida-mcp][alias] alias_path: {alias_path}")

    if resolved_on_path:
        print(f"[aida-mcp][alias] {name} resolves on PATH: {resolved_on_path}")
    else:
        print(f"[aida-mcp][alias] {name} not found on PATH")

    if alias_path.exists():
        print("[aida-mcp][alias] alias file exists next to aida-mcp")
    else:
        print("[aida-mcp][alias] alias file NOT present next to aida-mcp")


def alias_install_cmd(args: argparse.Namespace) -> None:
    name = str(args.name or "aida")
    force = bool(getattr(args, "force", False))

    resolved_on_path = shutil.which(name)
    if resolved_on_path and not force:
        print(f"[aida-mcp][alias][error] Refusing to install alias '{name}': command already exists on PATH.")
        print(f"[aida-mcp][alias][error] Found: {resolved_on_path}")
        print("[aida-mcp][alias][error] Keep using 'aida-mcp', or rerun with --force if you understand the risk.")
        raise SystemExit(2)

    command_path = mcp_install.resolve_self_command_path(sys.argv[0])
    scripts_dir = command_path.parent
    alias_path = scripts_dir / name

    wrapper = (
        "#!/usr/bin/env sh\n"
        "# Generated by `aida-mcp alias install`.\n"
        "# This wrapper lives next to the installed `aida-mcp` script.\n"
        "set -e\n"
        'exec "$(dirname "$0")/aida-mcp" "$@"\n'
    )
    alias_path.write_text(wrapper, encoding="utf-8")
    alias_path.chmod(0o755)
    print(f"[aida-mcp][alias] Installed alias '{name}' at {alias_path}")
    if resolved_on_path:
        print(
            f"[aida-mcp][alias] Note: '{name}' already existed on PATH ({resolved_on_path}); PATH precedence decides which runs."
        )


def alias_uninstall_cmd(args: argparse.Namespace) -> None:
    name = str(args.name or "aida")

    command_path = mcp_install.resolve_self_command_path(sys.argv[0])
    scripts_dir = command_path.parent
    alias_path = scripts_dir / name

    if not alias_path.exists():
        print(f"[aida-mcp][alias] Alias '{name}' is not installed at {alias_path}")
        return

    try:
        current = alias_path.read_text(encoding="utf-8")
    except Exception:
        current = ""

    marker = "# Generated by `aida-mcp alias install`."
    if marker not in current:
        print(
            f"[aida-mcp][alias][error] Refusing to remove '{alias_path}': file does not look like an aida-mcp alias wrapper."
        )
        print("[aida-mcp][alias][error] Remove it manually if you are sure.")
        raise SystemExit(2)

    alias_path.unlink()
    print(f"[aida-mcp][alias] Removed alias '{name}' at {alias_path}")


def _run_pre_commit_validation(*, workspace_root: Path) -> dict[str, object]:
    readiness = check_repo_readiness(workspace_root=workspace_root)
    if not readiness.validate_ready:
        missing: list[str] = []
        if not readiness.config_root_exists:
            missing.append(f"missing config dir: {readiness.config_root}")
        else:
            missing.extend(
                [f"missing file: {readiness.config_root}/{name}" for name in readiness.missing_validate_files]
            )
        details = "; ".join(missing) if missing else "unknown repo readiness issue"
        raise RuntimeError(f"Repository is not initialized for AIDA validation. {details}. Run `aida-mcp init` first.")

    changed_paths = list_changed_files(workspace_root, include_untracked=True)
    return asyncio.run(
        execute_plan(
            workspace_root=workspace_root,
            scope="pre_commit",
            changed_paths=changed_paths,
            dry_run=False,
            explain=False,
            ctx=None,
        )
    )


def _read_body_from_args(args: argparse.Namespace) -> str:
    body_value = ""
    if args.body is not None:
        body_value = str(args.body)
    elif args.body_file:
        body_value = Path(args.body_file).read_text(encoding="utf-8")
    elif bool(getattr(args, "body_stdin", False)):
        body_value = sys.stdin.read()
    elif args.body_line:
        body_value = "\n".join([str(line) for line in args.body_line])
    return body_value


def _print_commit_plan(*, plan: CommitCommandPlan, as_json: bool) -> None:
    if as_json:
        print(json.dumps({"command": plan.command}, indent=2, sort_keys=True))
        return
    print(plan.command)
