"""Milco CLI – argparse with subcommands."""

from __future__ import annotations

import argparse
import json
import pathlib
import sys

from milco.core.run_context import RunContext, _make_run_id
from milco.core.artifacts import ensure_all_artifacts_exist
from milco.core.manifest import build_manifest, write_manifest, now_iso
from milco.core.run_index import build_index_record, append_index_record
from milco.core.history import (
    load_index_records,
    filter_records,
    format_table,
    format_json,
)
from milco.core.show import load_manifest, list_run_ids, format_run_detail
from milco.audit.auditor import run_audit
from milco.fix.fixer import run_fix
from milco.gate.gates import run_gates
from milco.tools.git_tools import get_repo_info, is_repo_dirty


MILCO_TOML_TEMPLATE = """\
[llm]
provider = "ollama"
model = "llama3"
# base_url = "http://localhost:11434"

# Uncomment for OpenAI-compatible API:
# [llm]
# provider = "openai"
# model = "gpt-4o"
#
# [llm.openai]
# api_key_env = "OPENAI_API_KEY"
"""

CONTRACT_TEMPLATE = """\
# Task Contract

## Task Type

{task_type}

## Goal

_Describe the objective of this task._

## Scope

_What will be changed or created._

## Out of scope

_What will NOT be touched._

## Success Criteria

_How do we know the task succeeded._

## Constraints

_Limits: no LLM calls, Windows-only, etc._

## Approvals required

CONFIRM APPLY

## Notes

_Any extra context._
"""


def _refusal_what_next(reason: str, **kwargs: str) -> list[str]:
    """Return 'What to do next' lines for a REFUSED run. reason: confirm_apply | unknown_task | llm_required."""
    if reason == "confirm_apply":
        return [
            "What to do next:",
            "  Add CONFIRM APPLY under '## Approvals required' in your contract, then re-run with --apply.",
        ]
    if reason == "unknown_task":
        lines = [
            "What to do next:",
            "  Run 'milco --list-tasks' to see available task types.",
        ]
        suggestion = kwargs.get("suggestion")
        if suggestion:
            lines.insert(1, f"  Did you mean '{suggestion}'?")
        return lines
    if reason == "llm_required":
        return [
            "What to do next:",
            "  Create milco.toml with an [llm] section, or run 'milco init' to generate one.",
        ]
    return []


def _cmd_init(args: argparse.Namespace) -> int:
    """Create starter files in current directory."""
    cwd = pathlib.Path.cwd()
    task_type = args.task_type

    contract_path = cwd / "TASK_CONTRACT.md"
    if contract_path.exists():
        print("Skipped: TASK_CONTRACT.md already exists")
    else:
        contract_path.write_text(
            CONTRACT_TEMPLATE.format(task_type=task_type),
            encoding="utf-8",
        )
        print(f"Created: TASK_CONTRACT.md (task type: {task_type})")

    toml_path = cwd / "milco.toml"
    if toml_path.exists():
        print("Skipped: milco.toml already exists")
    else:
        toml_path.write_text(MILCO_TOML_TEMPLATE, encoding="utf-8")
        print("Created: milco.toml")

    return 0


def _add_global_flags(parser: argparse.ArgumentParser) -> None:
    parser.add_argument(
        "--apply",
        action="store_true",
        help="Enable apply mode (requires CONFIRM APPLY in contract).",
    )
    parser.add_argument(
        "--run-id", dest="run_id", default=None, help="Reuse an existing run ID."
    )
    parser.add_argument("--contract", default=None, help="Path to TASK_CONTRACT.md.")


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="milco",
        description="Milco AI – deterministic, human-in-the-loop repo automation.",
    )
    _add_global_flags(parser)
    parser.add_argument(
        "--list-tasks", action="store_true", help="List available task types and exit."
    )
    parser.add_argument(
        "--list-templates",
        action="store_true",
        help="List scaffold template names and exit.",
    )
    parser.add_argument(
        "--version",
        action="store_true",
        help="Show version and exit.",
    )

    sub = parser.add_subparsers(dest="command")

    for name, hlp in [
        ("audit", "Audit the repo: scan files, validate contract, produce evidence."),
        ("fix", "Generate patches.diff for map.json."),
        ("gate", "Run gates and produce gate_report.json."),
        ("run", "Full pipeline: audit -> fix -> gate."),
    ]:
        sp = sub.add_parser(name, help=hlp)
        _add_global_flags(sp)

    hist = sub.add_parser(
        "history", help="Show run history from index.jsonl (read-only)."
    )
    hist.add_argument(
        "--last", type=int, default=20, help="Show last N runs (default 20)."
    )
    hist.add_argument(
        "--failed", action="store_true", help="Show only failed runs (exit_code == 1)."
    )
    hist.add_argument(
        "--json", dest="json_output", action="store_true", help="Output as JSON lines."
    )

    show = sub.add_parser("show", help="Show details of a specific run (read-only).")
    show.add_argument(
        "run_id",
        nargs="?",
        default=None,
        help="Run ID to inspect. Omit to list available runs.",
    )
    show.add_argument(
        "--json",
        dest="json_output",
        action="store_true",
        help="Output raw manifest as JSON.",
    )

    init_sp = sub.add_parser(
        "init", help="Create starter TASK_CONTRACT.md and milco.toml."
    )
    init_sp.add_argument(
        "--task-type",
        dest="task_type",
        default="map-gen",
        help="Pre-fill Task Type section (default: map-gen).",
    )

    return parser


def _make_ctx(args: argparse.Namespace) -> RunContext:
    repo_root = pathlib.Path.cwd()
    task_type = None
    llm = None

    if args.contract:
        from milco.core.contract import load_contract, parse_task_type

        try:
            text = load_contract(args.contract)
            task_type = parse_task_type(text)
        except FileNotFoundError:
            pass

    import milco.tasks.map_gen  # noqa: F401
    import milco.tasks.scaffold_task  # noqa: F401

    try:
        import milco.tasks.doc_gen  # noqa: F401
    except ImportError:
        pass
    try:
        import milco.tasks.format_task  # noqa: F401
    except ImportError:
        pass
    try:
        import milco.tasks.lint_task  # noqa: F401
    except ImportError:
        pass

    if task_type:
        from milco.tasks.registry import get_task

        task_cls = get_task(task_type)
        if task_cls and getattr(task_cls, "needs_llm", False):
            from milco.llm.config import resolve_provider

            try:
                llm = resolve_provider(repo_root)
            except ValueError:
                pass

    return RunContext(
        repo_root=repo_root,
        run_id=args.run_id or _make_run_id(),
        apply_mode=args.apply,
        contract_path=args.contract,
        task_type=task_type,
        llm=llm,
    )


def _finalize_manifest(
    ctx: RunContext,
    command: str,
    started: str,
    repo_info: dict,
    decision: str,
    exit_code: int,
    notes: list[str] | None = None,
) -> None:
    """Build and write manifest.json at end of any command."""
    if decision == "REFUSED":
        repo_info["is_dirty_after"] = repo_info.get("is_dirty_before")
    else:
        repo_info["is_dirty_after"] = is_repo_dirty(ctx.repo_root)
    manifest = build_manifest(
        ctx,
        command=command,
        started_at=started,
        finished_at=now_iso(),
        repo_info=repo_info,
        decision=decision,
        exit_code=exit_code,
        notes=notes or [],
    )
    write_manifest(ctx, manifest)
    record = build_index_record(manifest, ctx.run_dir)
    append_index_record(ctx.repo_root / "runs", record)


def _cmd_audit(ctx: RunContext) -> int:
    started = now_iso()
    repo_info = get_repo_info(ctx.repo_root)
    result = run_audit(ctx)
    exit_code = 0 if result["success"] else 1
    decision = "PASS" if result["success"] else "FAIL"
    print(f"[audit] run_id={ctx.run_id} success={result['success']}")
    if result["errors"]:
        for e in result["errors"]:
            print(f"  ERROR: {e}")
    _finalize_manifest(
        ctx,
        "audit",
        started,
        repo_info,
        decision,
        exit_code,
        notes=result.get("errors"),
    )
    print(f"  Artifacts -> {ctx.run_dir}")
    return exit_code


def _cmd_fix(ctx: RunContext) -> int:
    started = now_iso()
    repo_info = get_repo_info(ctx.repo_root)
    result = run_fix(ctx)
    exit_code = 0 if result.get("success", False) else 1
    decision = "PASS" if result.get("success", False) else "FAIL"
    print(f"[fix] run_id={ctx.run_id} applied={result['applied']}")
    if result["errors"]:
        for e in result["errors"]:
            print(f"  ERROR: {e}")
    _finalize_manifest(
        ctx, "fix", started, repo_info, decision, exit_code, notes=result.get("errors")
    )
    print(f"  Artifacts -> {ctx.run_dir}")
    return exit_code


def _cmd_gate(ctx: RunContext) -> int:
    started = now_iso()
    repo_info = get_repo_info(ctx.repo_root)
    report = run_gates(ctx)
    overall = report["overall_status"]
    exit_code = report["exit_code"]
    print(f"[gate] run_id={ctx.run_id} overall_status={overall}")
    for g in report["gates"]:
        print(f"  {g['name']}: {g['status']}")
    _finalize_manifest(ctx, "gate", started, repo_info, overall, exit_code)
    print(f"  Artifacts -> {ctx.run_dir}")
    return exit_code


def _cmd_run(ctx: RunContext) -> int:
    """Full pipeline: audit -> fix -> gate with REFUSED handling."""
    started = now_iso()
    repo_info = get_repo_info(ctx.repo_root)
    print(f"[run] Starting full pipeline, run_id={ctx.run_id}")

    # Detect REFUSED: --apply requested but CONFIRM APPLY missing
    refused = ctx.apply_mode and not ctx.has_confirm_apply()

    if refused:
        from milco.core.artifacts import write_artifact as _wa

        ctx.ensure_run_dir()
        summary_lines = [
            "DECISION: REFUSED\n",
            "\nApply requested but CONFIRM APPLY not found in contract.\n",
            "\n" + "\n".join(_refusal_what_next("confirm_apply")) + "\n",
        ]
        _wa(ctx, "summary.md", "".join(summary_lines))
        ensure_all_artifacts_exist(ctx)
        _finalize_manifest(
            ctx,
            "run",
            started,
            repo_info,
            "REFUSED",
            1,
            notes=["Apply requested but CONFIRM APPLY missing in contract."],
        )
        print("  DECISION: REFUSED (CONFIRM APPLY missing)")
        for line in _refusal_what_next("confirm_apply"):
            print(f"  {line}")
        print(f"  Artifacts -> {ctx.run_dir}")
        return 1

    # Unknown task type
    if ctx.task_type:
        import milco.tasks.map_gen  # noqa: F401
        from milco.tasks.registry import get_task

        task_cls = get_task(ctx.task_type)
        if task_cls is None:
            import difflib
            from milco.core.artifacts import write_artifact as _wa
            from milco.tasks.registry import list_tasks as _lt
            import milco.tasks.map_gen  # noqa: F401

            try:
                import milco.tasks.doc_gen  # noqa: F401
            except ImportError:
                pass
            try:
                import milco.tasks.format_task  # noqa: F401
            except ImportError:
                pass
            try:
                import milco.tasks.lint_task  # noqa: F401
            except ImportError:
                pass
            import milco.tasks.scaffold_task  # noqa: F401

            ctx.ensure_run_dir()
            available = _lt()
            close = difflib.get_close_matches(ctx.task_type, available, n=1, cutoff=0.5)
            summary_lines = [
                f"DECISION: REFUSED\n\nUnknown task type: {ctx.task_type}\n",
                "\n"
                + "\n".join(
                    _refusal_what_next(
                        "unknown_task", suggestion=close[0] if close else ""
                    )
                )
                + "\n",
            ]
            _wa(ctx, "summary.md", "".join(summary_lines))
            ensure_all_artifacts_exist(ctx)
            _finalize_manifest(
                ctx,
                "run",
                started,
                repo_info,
                "REFUSED",
                1,
                notes=[f"Unknown task type: {ctx.task_type}"],
            )
            print(f"  DECISION: REFUSED (unknown task type '{ctx.task_type}')")
            for line in _refusal_what_next(
                "unknown_task", suggestion=close[0] if close else ""
            ):
                print(f"  {line}")
            print(f"  Artifacts -> {ctx.run_dir}")
            return 1

        if task_cls.needs_llm and ctx.llm is None:
            from milco.core.artifacts import write_artifact as _wa

            ctx.ensure_run_dir()
            summary_lines = [
                f"DECISION: REFUSED\n\nTask '{ctx.task_type}' requires LLM but none configured.\n",
                "\n" + "\n".join(_refusal_what_next("llm_required")) + "\n",
            ]
            _wa(ctx, "summary.md", "".join(summary_lines))
            ensure_all_artifacts_exist(ctx)
            _finalize_manifest(
                ctx,
                "run",
                started,
                repo_info,
                "REFUSED",
                1,
                notes=[
                    f"Task '{ctx.task_type}' requires LLM but none configured in milco.toml"
                ],
            )
            print(
                f"  DECISION: REFUSED (task '{ctx.task_type}' requires an LLM but none is configured)"
            )
            for line in _refusal_what_next("llm_required"):
                print(f"  {line}")
            print(f"  Artifacts -> {ctx.run_dir}")
            return 1

    audit_result = run_audit(ctx)
    print(f"  [audit] success={audit_result['success']}")

    fix_result = run_fix(ctx)
    print(f"  [fix] applied={fix_result['applied']}")

    gate_report = run_gates(ctx)
    overall = gate_report["overall_status"]
    print(f"  [gate] overall_status={overall}")

    ensure_all_artifacts_exist(ctx)

    if not audit_result["success"] or not fix_result.get("success", False):
        exit_code = 1
        decision = "FAIL"
    else:
        exit_code = gate_report["exit_code"]
        decision = overall

    _finalize_manifest(ctx, "run", started, repo_info, decision, exit_code)
    print(f"  Artifacts -> {ctx.run_dir}")
    print(f"  Run ID: {ctx.run_id}  (milco show {ctx.run_id})")
    return exit_code


def _cmd_history(args: argparse.Namespace) -> int:
    """Read-only: print run history from index.jsonl."""
    runs_dir = pathlib.Path.cwd() / "runs"

    if args.last < 1:
        print("Error: --last must be >= 1.", file=sys.stderr)
        return 1

    records, corrupt = load_index_records(runs_dir)

    if corrupt == -1:
        print("No runs found (runs/index.jsonl missing).")
        return 0
    if corrupt == -2:
        print("No runs found (runs/index.jsonl empty).")
        return 0

    filtered = filter_records(records, last=args.last, failed_only=args.failed)

    if args.json_output:
        output = format_json(filtered)
        if output:
            print(output)
    else:
        print(format_table(filtered))

    if corrupt > 0:
        print(f"Skipped {corrupt} corrupt index lines.")

    return 0


def _cmd_show(args: argparse.Namespace) -> int:
    """Read-only: show details of a specific run."""
    runs_dir = pathlib.Path.cwd() / "runs"

    if not args.run_id:
        ids = list_run_ids(runs_dir)
        if not ids:
            print("No runs found.")
            return 0
        print("Available runs:")
        for rid in ids:
            print(f"  {rid}")
        print("\nUse: milco show <run_id>")
        return 0

    run_dir = runs_dir / args.run_id
    if not run_dir.exists():
        print(f"Run not found: {args.run_id}", file=sys.stderr)
        ids = list_run_ids(runs_dir)
        if ids:
            print(f"Available: {', '.join(ids[-5:])}", file=sys.stderr)
        return 1

    manifest = load_manifest(run_dir)
    if manifest is None:
        print(f"manifest.json missing in runs/{args.run_id}/", file=sys.stderr)
        return 1

    if args.json_output:
        print(json.dumps(manifest, indent=2))
    else:
        print(format_run_detail(manifest))

    return 0


def main(argv: list[str] | None = None) -> int:
    parser = _build_parser()
    args = parser.parse_args(argv)

    if getattr(args, "version", False):
        from milco import __version__

        print(__version__)
        return 0

    if getattr(args, "list_tasks", False):
        import milco.tasks.map_gen  # noqa: F401
        import milco.tasks.scaffold_task  # noqa: F401

        try:
            import milco.tasks.doc_gen  # noqa: F401
        except ImportError:
            pass
        try:
            import milco.tasks.format_task  # noqa: F401
        except ImportError:
            pass
        try:
            import milco.tasks.lint_task  # noqa: F401
        except ImportError:
            pass
        from milco.tasks.registry import list_tasks, get_task

        print("Available task types:\n")
        for name in list_tasks():
            cls = get_task(name)
            llm_tag = "requires LLM" if cls.needs_llm else "deterministic"
            desc = cls.description or "(no description)"
            print(f"  {name:<12} ({llm_tag})  {desc}")
        return 0

    if getattr(args, "list_templates", False):
        import milco.tasks.scaffold_task as st  # noqa: F401

        print(
            "Scaffold templates (use with task type 'scaffold', ## Scaffold section):\n"
        )
        for name in st.SCAFFOLD_TEMPLATES:
            print(f"  {name}")
        return 0

    if not args.command:
        parser.print_help()
        return 0

    if args.command == "history":
        return _cmd_history(args)

    if args.command == "show":
        return _cmd_show(args)

    if args.command == "init":
        return _cmd_init(args)

    ctx = _make_ctx(args)

    commands = {
        "audit": _cmd_audit,
        "fix": _cmd_fix,
        "gate": _cmd_gate,
        "run": _cmd_run,
    }
    return commands[args.command](ctx)


if __name__ == "__main__":
    sys.exit(main())
