"""State Hub dual-run adapter → Repo Manager (RMGR-WP-0002/0003 Stage B).

Config file (same as repo-manager): ~/.repo-manager/dual-run.yaml
or RM_DUAL_RUN_CONFIG. Env overrides when set:

  RM_WRITEBACK, RM_RECONCILE, RM_WRITEBACK_PUSH, RM_PILOT_REPOS, RM_METER_PATH

Rollback: disable in config / unset env → native State Hub path.
"""

from __future__ import annotations

import json
import logging
import os
import subprocess
import sys
import uuid
from functools import lru_cache
from pathlib import Path
from typing import Any

import yaml

logger = logging.getLogger(__name__)

_TRUE = frozenset({"1", "true", "yes", "on"})
_FALSE = frozenset({"0", "false", "no", "off"})


def config_path() -> Path:
    raw = os.environ.get("RM_DUAL_RUN_CONFIG", "~/.repo-manager/dual-run.yaml")
    return Path(raw).expanduser()


@lru_cache(maxsize=1)
def _load_file_config() -> dict[str, Any]:
    path = config_path()
    if not path.is_file():
        return {}
    try:
        data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
    except (OSError, yaml.YAMLError):
        return {}
    return data if isinstance(data, dict) else {}


def reload_config() -> None:
    _load_file_config.cache_clear()


def _env_bool(name: str) -> bool | None:
    raw = os.environ.get(name)
    if raw is None or raw.strip() == "":
        return None
    v = raw.strip().lower()
    if v in _TRUE:
        return True
    if v in _FALSE:
        return False
    return None


def _cfg_bool(key: str, env_name: str, default: bool = False) -> bool:
    ev = _env_bool(env_name)
    if ev is not None:
        return ev
    cfg = _load_file_config()
    if key in cfg:
        val = cfg[key]
        if isinstance(val, bool):
            return val
        if isinstance(val, str):
            return val.strip().lower() in _TRUE
    return default


def writeback_enabled() -> bool:
    return _cfg_bool("writeback", "RM_WRITEBACK", False)


def reconcile_enabled() -> bool:
    return _cfg_bool("reconcile", "RM_RECONCILE", False)


def writeback_push_enabled() -> bool:
    return _cfg_bool("writeback_push", "RM_WRITEBACK_PUSH", False)


def pilot_slugs() -> set[str] | None:
    raw = os.environ.get("RM_PILOT_REPOS")
    if raw is not None:
        raw = raw.strip()
        if not raw:
            return set()
        return {s.strip() for s in raw.split(",") if s.strip()}
    cfg = _load_file_config()
    if "pilot_repos" not in cfg:
        return None
    val = cfg["pilot_repos"]
    if val is None:
        return None
    if isinstance(val, str):
        if not val.strip():
            return set()
        return {s.strip() for s in val.split(",") if s.strip()}
    if isinstance(val, list):
        return {str(s).strip() for s in val if str(s).strip()}
    return None


def slug_allowed(slug: str | None) -> bool:
    if not slug:
        return False
    pilots = pilot_slugs()
    if pilots is None:
        return True
    return slug in pilots


def writeback_for_repo(slug: str | None) -> bool:
    return writeback_enabled() and slug_allowed(slug)


def reconcile_for_repo(slug: str | None) -> bool:
    return reconcile_enabled() and slug_allowed(slug)


def meter_path() -> Path:
    raw = os.environ.get("RM_METER_PATH")
    if raw:
        return Path(raw).expanduser()
    cfg = _load_file_config()
    if cfg.get("meter_path"):
        return Path(str(cfg["meter_path"])).expanduser()
    return Path("~/.repo-manager/mutation-meter.jsonl").expanduser()


def record_mutation(
    *,
    source: str,
    kind: str,
    repo_slug: str | None,
    detail: dict[str, Any] | None = None,
) -> None:
    try:
        from datetime import UTC, datetime

        path = meter_path()
        path.parent.mkdir(parents=True, exist_ok=True)
        row = {
            "ts": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
            "source": source,
            "kind": kind,
            "repo_slug": repo_slug,
            "detail": detail or {},
        }
        with path.open("a", encoding="utf-8") as fh:
            fh.write(json.dumps(row, default=str) + "\n")
    except OSError:
        pass


def run_rmgr(args: list[str], *, timeout: int = 120) -> tuple[int, str, str]:
    env = os.environ.copy()
    custom = os.environ.get("RMGR_BIN", "").strip()
    if custom:
        cmd = custom.split() + args
    else:
        src = os.environ.get("REPO_MANAGER_SRC", "").strip()
        sibling = Path(src) if src else (Path.home() / "repo-manager" / "src")
        if not sibling.is_dir() and (Path.home() / "repo-manager" / "src").is_dir():
            sibling = Path.home() / "repo-manager" / "src"
        if sibling.is_dir():
            env["PYTHONPATH"] = str(sibling) + (
                os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else ""
            )
            cmd = [
                sys.executable,
                "-c",
                "from repo_manager.cli import main; import sys; raise SystemExit(main(sys.argv[1:]))",
                *args,
            ]
        else:
            cmd = ["rmgr", *args]

    proc = subprocess.run(
        cmd,
        capture_output=True,
        text=True,
        timeout=timeout,
        check=False,
        env=env,
    )
    return proc.returncode, proc.stdout, proc.stderr


def rm_update_task_status(
    *,
    repo_path: str | Path,
    task_id: str,
    status: str,
    repo_slug: str | None = None,
    reason: str = "state-hub dual-run",
    push: bool | None = None,
    correlation_id: str | None = None,
) -> dict[str, Any]:
    correlation_id = correlation_id or str(uuid.uuid4())
    if push is None:
        push = writeback_push_enabled()
    args = [
        "update-task-status",
        "--path",
        str(repo_path),
        "--task-id",
        str(task_id),
        "--status",
        status,
        "--reason",
        reason,
        "--correlation-id",
        correlation_id,
        "--idempotency-key",
        f"sh-dual-{task_id}-{status}-{correlation_id}",
    ]
    if repo_slug:
        args.extend(["--slug", repo_slug])
    if push:
        args.append("--push")

    code, out, err = run_rmgr(args)
    try:
        result = json.loads(out.strip() or "{}")
    except json.JSONDecodeError:
        result = {
            "status": "failed",
            "error": {
                "code": "internal",
                "message": f"rmgr non-json exit={code} stderr={err!r} stdout={out[:500]!r}",
            },
            "correlation_id": correlation_id,
        }
    if code != 0 and result.get("status") not in ("applied", "rejected"):
        result.setdefault("status", "failed")
        result.setdefault(
            "error",
            {"code": "internal", "message": f"rmgr exit={code} stderr={err!r}"},
        )
    return result


def rm_update_workplan(
    *,
    repo_path: str | Path,
    workplan_id: str,
    operation: str = "update",
    title: str | None = None,
    goal: str | None = None,
    status: str | None = None,
    owner: str | None = None,
    domain: str | None = None,
    topic_slug: str | None = None,
    repo_slug: str | None = None,
    reason: str = "state-hub dual-run",
    push: bool | None = None,
    correlation_id: str | None = None,
    confirm_archive: bool = False,
) -> dict[str, Any]:
    """Delegate a file-backed workplan mutation to Repo Manager.

    ``operation=archive`` is the recoverable counterpart of State Hub's
    DELETE workplan route; it never erases repository history.
    """
    correlation_id = correlation_id or str(uuid.uuid4())
    if operation not in {"create", "update", "archive"}:
        return {
            "status": "rejected",
            "correlation_id": correlation_id,
            "error": {"code": "validation_error", "message": f"invalid operation {operation!r}"},
        }
    if push is None:
        push = writeback_push_enabled()
    cli_operation = "delete" if operation == "archive" else operation
    args = [
        "workplan",
        cli_operation,
        "--path",
        str(repo_path),
        "--workplan-id",
        str(workplan_id),
        "--reason",
        reason,
        "--correlation-id",
        correlation_id,
        "--idempotency-key",
        f"sh-dual-workplan-{operation}-{workplan_id}-{correlation_id}",
    ]
    for flag, value in (
        ("--title", title),
        ("--goal", goal),
        ("--status", status),
        ("--owner", owner),
        ("--domain", domain),
        ("--topic-slug", topic_slug),
        ("--slug", repo_slug),
    ):
        if value is not None:
            args.extend([flag, value])
    if operation == "archive" and confirm_archive:
        args.append("--confirm")
    if push:
        args.append("--push")

    code, out, err = run_rmgr(args)
    try:
        result = json.loads(out.strip() or "{}")
    except json.JSONDecodeError:
        result = {
            "status": "failed",
            "error": {
                "code": "internal",
                "message": f"rmgr non-json exit={code} stderr={err!r} stdout={out[:500]!r}",
            },
            "correlation_id": correlation_id,
        }
    if code != 0 and result.get("status") not in ("applied", "rejected"):
        result.setdefault("status", "failed")
        result.setdefault(
            "error",
            {"code": "internal", "message": f"rmgr exit={code} stderr={err!r}"},
        )
    return result


def rm_update_register_entry(
    *,
    repo_path: str | Path,
    kind: str,
    entry_id: str,
    operation: str = "put",
    title: str | None = None,
    status: str | None = None,
    data: dict[str, Any] | None = None,
    note: str | None = None,
    author: str | None = None,
    repo_slug: str | None = None,
    reason: str = "state-hub retirement adapter",
    push: bool | None = None,
    correlation_id: str | None = None,
) -> dict[str, Any]:
    """Delegate register create/update/defer/note to Repo Manager."""
    correlation_id = correlation_id or str(uuid.uuid4())
    if operation not in {"put", "defer", "note"}:
        return {
            "status": "rejected",
            "correlation_id": correlation_id,
            "error": {"code": "validation_error", "message": f"invalid operation {operation!r}"},
        }
    if push is None:
        push = writeback_push_enabled()
    args = [
        "register",
        operation,
        "--path",
        str(repo_path),
        "--kind",
        kind,
        "--entry-id",
        entry_id,
        "--reason",
        reason,
        "--correlation-id",
        correlation_id,
        "--idempotency-key",
        f"sh-dual-register-{operation}-{kind}-{entry_id}-{correlation_id}",
    ]
    for flag, value in (
        ("--title", title),
        ("--status", status),
        ("--note", note),
        ("--author", author),
        ("--slug", repo_slug),
    ):
        if value is not None:
            args.extend([flag, value])
    if data is not None:
        args.extend(["--data-json", json.dumps(data, separators=(",", ":"))])
    if push:
        args.append("--push")

    code, out, err = run_rmgr(args)
    try:
        result = json.loads(out.strip() or "{}")
    except json.JSONDecodeError:
        result = {
            "status": "failed",
            "error": {
                "code": "internal",
                "message": f"rmgr non-json exit={code} stderr={err!r} stdout={out[:500]!r}",
            },
            "correlation_id": correlation_id,
        }
    if code != 0 and result.get("status") not in ("applied", "rejected"):
        result.setdefault("status", "failed")
        result.setdefault(
            "error",
            {"code": "internal", "message": f"rmgr exit={code} stderr={err!r}"},
        )
    return result


def rm_scan_sbom(
    *,
    repo_path: str | Path,
    repo_slug: str | None = None,
) -> dict[str, Any]:
    """Derive a versioned SBOM snapshot from repository-owned sources."""
    args = ["sbom", "scan", "--path", str(repo_path)]
    if repo_slug:
        args.extend(["--slug", repo_slug])

    code, out, err = run_rmgr(args)
    try:
        result = json.loads(out.strip() or "{}")
    except json.JSONDecodeError:
        result = {
            "ok": False,
            "error": f"rmgr non-json exit={code} stderr={err!r} stdout={out[:500]!r}",
        }
    if code != 0:
        result.setdefault("ok", False)
        result.setdefault("error", f"rmgr exit={code} stderr={err!r}")
    result["exit_code"] = code
    return result


def rm_scaffold(
    *,
    repo_path: str | Path,
    flavor: str,
    wp_prefix: str | None = None,
    slug: str | None = None,
    domain: str = "infotech",
    force: bool = False,
    commit: bool = False,
) -> dict[str, Any]:
    """Delegate repository scaffolding to ``rmgr scaffold`` (STATE-WP-0080-T02)."""
    args = [
        "scaffold",
        "--path",
        str(repo_path),
        "--flavor",
        flavor,
        "--domain",
        domain,
    ]
    if slug:
        args.extend(["--slug", slug])
    if wp_prefix:
        args.extend(["--wp-prefix", wp_prefix])
    if force:
        args.append("--force")
    if not commit:
        args.append("--no-commit")
    code, out, err = run_rmgr(args)
    try:
        result = json.loads(out.strip() or "{}")
    except json.JSONDecodeError:
        result = {
            "status": "failed",
            "error": {
                "code": "internal",
                "message": f"rmgr non-json exit={code} stderr={err!r} stdout={out[:500]!r}",
            },
        }
    if code != 0 and result.get("status") not in ("applied", "rejected"):
        result.setdefault("status", "failed")
        result.setdefault(
            "error",
            {"code": "internal", "message": f"rmgr exit={code} stderr={err!r}"},
        )
    result["exit_code"] = code
    return result


def rm_reconcile(*, repo_path: str | Path, repo_slug: str | None = None) -> dict[str, Any]:
    args = ["reconcile", "--path", str(repo_path)]
    if repo_slug:
        args.extend(["--slug", repo_slug])
    code, out, err = run_rmgr(args)
    try:
        result = json.loads(out.strip() or "{}")
    except json.JSONDecodeError:
        result = {"ok": False, "error": err or out, "exit_code": code}
    result["exit_code"] = code
    return result


def try_writeback_for_task(
    *,
    repo_path: str | Path | None,
    repo_slug: str | None,
    task_id: str,
    status: str,
    reason: str = "state-hub dual-run PATCH /tasks",
) -> dict[str, Any] | None:
    if not writeback_for_repo(repo_slug):
        return None
    if not repo_path:
        logger.warning("RM dual-run writeback skipped: no repo_path for %s", repo_slug)
        return None
    path = Path(repo_path)
    if not path.is_dir():
        logger.warning("RM dual-run writeback skipped: missing path %s", path)
        return None

    result = rm_update_task_status(
        repo_path=path,
        task_id=task_id,
        status=status,
        repo_slug=repo_slug,
        reason=reason,
        push=writeback_push_enabled(),
    )
    if result.get("status") == "applied":
        record_mutation(
            source="repo-manager",
            kind="task_status_writeback_via_sh_facade",
            repo_slug=repo_slug,
            detail={
                "task_id": task_id,
                "status": status,
                "git_sha": (result.get("evidence") or {}).get("git_sha"),
                "push_ok": (result.get("evidence") or {}).get("push_ok"),
            },
        )
    else:
        logger.warning(
            "RM dual-run writeback failed for %s task=%s: %s",
            repo_slug,
            task_id,
            result.get("error") or result,
        )
    return result
