"""Read-only MCP/runtime health snapshot for the web UI (#430)."""

from __future__ import annotations

import hashlib
import os
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable
from urllib.parse import urlparse

import gitea_config
import reconciler_profile
from gitea_auth import REMOTES, api_request, get_auth_header, get_profile, gitea_url

from native_mcp_preference import shell_health_status

from webui.project_registry import ProjectRecord, load_registry

_RESTART_DOCS = "docs/llm-workflow-runbooks.md#issue-420-mcp-hot-reload"
_HOT_RELOAD_ISSUE = "#420"


@dataclass(frozen=True)
class FileHash:
    label: str
    path: str
    sha256: str | None
    error: str | None = None


@dataclass(frozen=True)
class RuntimeSnapshot:
    project_id: str
    repo_root: str
    remote: str
    host: str
    profile_name: str
    role_kind: str
    config_model: str
    profile_mode: str
    profile_source: str
    authenticated_username: str | None
    identity_error: str | None
    repo_sha: str | None
    remote_master_sha: str | None
    commits_behind_master: int | None
    stale_runtime_warning: str | None
    shell_health: dict[str, Any]
    workflow_hashes: tuple[FileHash, ...]
    schema_hashes: tuple[FileHash, ...]
    restart_guidance: str
    fetch_error: str | None = None


def _repo_root() -> Path:
    override = (os.environ.get("WEBUI_REPO_ROOT") or "").strip()
    if override:
        return Path(override).resolve()
    return Path(__file__).resolve().parent.parent


def _host_from_url(remote_host: str) -> str:
    parsed = urlparse(remote_host.strip())
    return parsed.netloc or remote_host.strip().rstrip("/")


def _remote_name_for_host(host: str) -> str:
    for name, profile in REMOTES.items():
        if profile.get("host") == host:
            return name
    return "custom"


def _config_model(config: dict | None) -> str:
    if config is None:
        return "env-only"
    version = config.get("version")
    if version == 1:
        return "v1"
    if version == 2:
        if "contexts" in config or config.get("shape") == "contexts":
            return "v2-contexts"
        return "v2-environments"
    return f"unknown-version-{version}"


def _profile_source() -> str:
    if os.environ.get("GITEA_PROFILE_NAME"):
        return "env var"
    if gitea_config.selected_profile_name():
        return "config file profile"
    return "default"


def _role_kind(allowed: list[str], forbidden: list[str]) -> str:
    if reconciler_profile.is_reconciler_profile(allowed, forbidden):
        return "reconciler"

    def can(op: str) -> bool:
        return gitea_config.check_operation(op, allowed, forbidden)[0]

    review = can("gitea.pr.approve") or can("gitea.pr.merge")
    author = can("gitea.pr.create") or can("gitea.branch.push")
    reconciler = can("gitea.pr.close") and not review and not author
    if review and author:
        return "mixed"
    if review:
        return "reviewer"
    if reconciler:
        return "reconciler"
    if author:
        return "author"
    return "limited"


def _sha256_file(path: Path) -> FileHash:
    label = path.name
    try:
        digest = hashlib.sha256(path.read_bytes()).hexdigest()
        return FileHash(label=label, path=str(path), sha256=digest)
    except OSError as exc:
        return FileHash(label=label, path=str(path), sha256=None, error=str(exc))


def _run_git(repo: Path, *args: str) -> str | None:
    try:
        completed = subprocess.run(
            ["git", *args],
            cwd=repo,
            check=True,
            capture_output=True,
            text=True,
        )
        return completed.stdout.strip() or None
    except (OSError, subprocess.CalledProcessError):
        return None


def _git_sync_status(repo: Path, remote: str, branch: str) -> tuple[str | None, str | None, int | None]:
    local_sha = _run_git(repo, "rev-parse", "HEAD")
    remote_sha = _run_git(repo, "rev-parse", f"{remote}/{branch}")
    behind = None
    if local_sha and remote_sha and local_sha != remote_sha:
        count = _run_git(repo, "rev-list", "--count", f"HEAD..{remote}/{branch}")
        if count is not None and count.isdigit():
            behind = int(count)
    return local_sha, remote_sha, behind


def _resolve_username(host: str) -> tuple[str | None, str | None]:
    auth = get_auth_header(host)
    if not auth:
        return None, f"Gitea credentials unavailable for {host}"
    try:
        data = api_request("GET", gitea_url(host, "/api/v1/user"), auth)
    except Exception as exc:  # noqa: BLE001 — operator-visible identity errors
        return None, f"Identity lookup failed: {exc}"
    username = (data or {}).get("login")
    if not username:
        return None, "Authenticated identity could not be resolved"
    return str(username), None


def _stale_warning(commits_behind: int | None) -> str | None:
    if commits_behind is None or commits_behind <= 0:
        return None
    return (
        f"Local checkout is {commits_behind} commit(s) behind remote master. "
        "Merged safety-gate changes may require an MCP server restart or profile "
        f"reload — see {_HOT_RELOAD_ISSUE} / {_RESTART_DOCS}."
    )


def load_runtime_snapshot(
    project_id: str | None = None,
    *,
    resolve_username: Callable[[str], tuple[str | None, str | None]] | None = None,
    git_sync: Callable[[Path, str, str], tuple[str | None, str | None, int | None]] | None = None,
) -> RuntimeSnapshot:
    """Build a read-only runtime health snapshot for the default registry project."""
    registry = load_registry()
    project: ProjectRecord | None = None
    if project_id:
        for entry in registry.projects:
            if entry.id == project_id:
                project = entry
                break
    else:
        project = registry.projects[0] if registry.projects else None

    if project is None:
        return RuntimeSnapshot(
            project_id=project_id or "",
            repo_root=str(_repo_root()),
            remote="",
            host="",
            profile_name="",
            role_kind="",
            config_model="",
            profile_mode="",
            profile_source="",
            authenticated_username=None,
            identity_error="project not found in registry",
            repo_sha=None,
            remote_master_sha=None,
            commits_behind_master=None,
            stale_runtime_warning=None,
            shell_health={},
            workflow_hashes=(),
            schema_hashes=(),
            restart_guidance=_RESTART_DOCS,
            fetch_error="project not found in registry",
        )

    repo = _repo_root()
    host = _host_from_url(project.remote_host)
    remote = _remote_name_for_host(host)
    offline_test = (os.environ.get("WEBUI_TEST_OFFLINE") or "").strip() in {
        "1",
        "true",
        "yes",
    }
    profile = get_profile()
    allowed = profile.get("allowed_operations") or []
    forbidden = profile.get("forbidden_operations") or []
    config = None
    try:
        config = gitea_config.load_config()
    except gitea_config.ConfigError as exc:
        return RuntimeSnapshot(
            project_id=project.id,
            repo_root=str(repo),
            remote=remote,
            host=host,
            profile_name=profile.get("profile_name") or "",
            role_kind=_role_kind(allowed, forbidden),
            config_model="config-error",
            profile_mode="unknown",
            profile_source=_profile_source(),
            authenticated_username=None,
            identity_error=str(exc),
            repo_sha=None,
            remote_master_sha=None,
            commits_behind_master=None,
            stale_runtime_warning=None,
            shell_health=shell_health_status(),
            workflow_hashes=(),
            schema_hashes=(),
            restart_guidance=_RESTART_DOCS,
            fetch_error=str(exc),
        )

    identity_fn = resolve_username
    if identity_fn is None:
        if offline_test:
            identity_fn = lambda _host: (None, "offline web UI test mode")
        else:
            identity_fn = _resolve_username
    username, identity_error = identity_fn(host)

    git_fn = git_sync
    if git_fn is None:
        if offline_test:
            git_fn = lambda _repo, _remote, _branch: (None, None, None)
        else:
            git_fn = _git_sync_status
    remote_ref = "prgs" if remote == "prgs" else "origin"
    local_sha, remote_sha, behind = git_fn(repo, remote_ref, project.default_branch)

    workflow_hashes = tuple(
        _sha256_file(repo / rel_path)
        for rel_path in project.workflow_paths.values()
    )
    schema_hashes = tuple(
        _sha256_file(repo / rel_path)
        for rel_path in project.schema_paths.values()
    )

    switching = gitea_config.is_runtime_switching_enabled()
    return RuntimeSnapshot(
        project_id=project.id,
        repo_root=str(repo),
        remote=remote,
        host=host,
        profile_name=str(profile.get("profile_name") or ""),
        role_kind=_role_kind(allowed, forbidden),
        config_model=_config_model(config),
        profile_mode="dynamic-profile" if switching else "static-profile",
        profile_source=_profile_source(),
        authenticated_username=username,
        identity_error=identity_error,
        repo_sha=local_sha,
        remote_master_sha=remote_sha,
        commits_behind_master=behind,
        stale_runtime_warning=_stale_warning(behind),
        shell_health=shell_health_status(),
        workflow_hashes=workflow_hashes,
        schema_hashes=schema_hashes,
        restart_guidance=_RESTART_DOCS,
    )


def snapshot_to_dict(snapshot: RuntimeSnapshot) -> dict[str, Any]:
    def _hash_row(item: FileHash) -> dict[str, Any]:
        return {
            "label": item.label,
            "path": item.path,
            "sha256": item.sha256,
            "error": item.error,
        }

    return {
        "project_id": snapshot.project_id,
        "repo_root": snapshot.repo_root,
        "remote": snapshot.remote,
        "host": snapshot.host,
        "profile_name": snapshot.profile_name,
        "role_kind": snapshot.role_kind,
        "config_model": snapshot.config_model,
        "profile_mode": snapshot.profile_mode,
        "profile_source": snapshot.profile_source,
        "authenticated_username": snapshot.authenticated_username,
        "identity_error": snapshot.identity_error,
        "repo_sha": snapshot.repo_sha,
        "remote_master_sha": snapshot.remote_master_sha,
        "commits_behind_master": snapshot.commits_behind_master,
        "stale_runtime_warning": snapshot.stale_runtime_warning,
        "shell_health": snapshot.shell_health,
        "workflow_hashes": [_hash_row(item) for item in snapshot.workflow_hashes],
        "schema_hashes": [_hash_row(item) for item in snapshot.schema_hashes],
        "restart_guidance": snapshot.restart_guidance,
        "fetch_error": snapshot.fetch_error,
    }
