"""BYOD runtime helpers shared by the CLI, the MCP client factory, and tests.

The single source of truth for "is BYOD active for platform X" is the
``manifest.json`` written into ``~/.mureo/byod/`` by ``mureo byod import``.

This module is read-light (one small JSON file) and intentionally has no
external dependencies so it can be imported from
``mureo/mcp/_client_factory.py`` without dragging the heavier installer
or adapter code into the MCP server's startup path.
"""

from __future__ import annotations

import contextlib
import json
import logging
import os
import tempfile
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

SCHEMA_VERSION = 1
SUPPORTED_PLATFORMS = (
    "google_ads",
    "meta_ads",
)

_USER_DIR_NAME = ".mureo"
_BYOD_SUBDIR = "byod"
_MANIFEST_NAME = "manifest.json"
_BYOD_DIR_ENV = "MUREO_BYOD_DIR"


def byod_data_dir() -> Path:
    """Return the BYOD data directory (does not create it).

    Resolution precedence:
      1. ``MUREO_BYOD_DIR`` environment variable. Set by the
         install-desktop wrapper to ``<workspace>/byod`` for the MCP
         runtime, and also temporarily set by ``install_desktop()``
         itself during ``--with-demo`` seeding. Each Claude Desktop
         workspace sees its own BYOD store and demo data does not
         leak across workspaces.
      2. A non-default ``RuntimeContext`` (an alternate backend
         registered via the ``mureo.runtime_context_factory``
         entry-point group whose ``state_store`` exposes a filesystem
         ``workspace``). Resolves to ``<workspace>/byod`` so a
         backend-supplied workspace co-locates its BYOD data with the
         rest of the workspace's state (STATE.json / STRATEGY.md /
         credentials.json). Only triggers when
         ``workspace_id != "default"`` so the existing legacy CLI /
         Claude Code single-workspace behaviour is preserved by
         construction.
      3. Legacy ``~/.mureo/byod/`` for existing CLI / Claude Code
         users — preserves backward compatibility.

    An empty or whitespace-only env var is treated as 'unset' so a
    stray ``MUREO_BYOD_DIR=`` (or `` `` from a malformed shell rc)
    cannot silently redirect BYOD into the cwd or a path named
    ``" "``. ``~`` in the value is expanded.
    """
    override = os.environ.get(_BYOD_DIR_ENV)
    if override and override.strip():
        return Path(override.strip()).expanduser()
    ws_path = _runtime_context_workspace()
    if ws_path is not None:
        return ws_path / _BYOD_SUBDIR
    return Path.home() / _USER_DIR_NAME / _BYOD_SUBDIR


def _runtime_context_workspace() -> Path | None:
    """Return the resolved workspace dir from the RuntimeContext, or
    ``None`` when the runtime is the default single-workspace one.

    Imported lazily inside this helper so ``mureo.byod.runtime`` keeps
    its "no external dependencies" promise (see module docstring) for
    callers that never need to touch the runtime layer.
    """
    try:
        from mureo.core.runtime_context import (
            DEFAULT_WORKSPACE_ID,
            get_runtime_context,
        )
    except ImportError:  # pragma: no cover — keeps the module standalone
        return None
    ctx = get_runtime_context()
    if ctx.workspace_id == DEFAULT_WORKSPACE_ID:
        # Single-workspace runtime: keep the legacy ~/.mureo/byod/
        # location so existing CLI / Claude Code users are unaffected.
        return None
    workspace = getattr(ctx.state_store, "workspace", None)
    if workspace is None:
        return None
    return Path(workspace)


def manifest_path() -> Path:
    return byod_data_dir() / _MANIFEST_NAME


def read_manifest() -> dict[str, Any] | None:
    """Read manifest.json; return ``None`` on absence, parse error, or
    unknown schema version (caller treats those as 'BYOD inactive').
    """
    p = manifest_path()
    if not p.exists():
        return None
    try:
        data = json.loads(p.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError) as exc:
        logger.warning("BYOD manifest unreadable, treating as inactive: %s", exc)
        return None
    if not isinstance(data, dict):
        return None
    if data.get("schema_version") != SCHEMA_VERSION:
        logger.warning(
            "BYOD manifest schema_version=%r != supported %d; treating as inactive",
            data.get("schema_version"),
            SCHEMA_VERSION,
        )
        return None
    if not isinstance(data.get("platforms"), dict):
        return None
    return data


def write_manifest(data: dict[str, Any]) -> None:
    """Atomically replace ``manifest.json`` with the given dict."""
    p = manifest_path()
    p.parent.mkdir(parents=True, exist_ok=True)
    fd, tmp_name = tempfile.mkstemp(
        prefix=".manifest.", suffix=".json", dir=str(p.parent)
    )
    try:
        with os.fdopen(fd, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2, sort_keys=False)
            f.write("\n")
        os.replace(tmp_name, p)
    except Exception:
        with contextlib.suppress(OSError):
            os.unlink(tmp_name)
        raise


def byod_has(platform: str) -> bool:
    """True if BYOD data is registered AND its files exist on disk.

    Detects "stale manifest" cases where the user removed the platform's
    directory out of band (e.g. ``rm -rf ~/.mureo/byod/google_ads/``)
    while leaving ``manifest.json`` untouched.
    """
    if platform not in SUPPORTED_PLATFORMS:
        return False
    manifest = read_manifest()
    if manifest is None:
        return False
    if platform not in manifest["platforms"]:
        return False
    platform_dir = byod_data_dir() / platform
    if not platform_dir.is_dir():
        logger.warning(
            "BYOD manifest references %s but %s is missing on disk; "
            "treating as inactive. Re-run `mureo byod import` or "
            "`mureo byod remove --%s`.",
            platform,
            platform_dir,
            platform.replace("_", "-"),
        )
        return False
    return True


def byod_active_platforms() -> list[str]:
    """Return platforms currently in BYOD mode (in canonical order)."""
    manifest = read_manifest()
    if manifest is None:
        return []
    return [p for p in SUPPORTED_PLATFORMS if p in manifest["platforms"]]


def byod_platform_info(platform: str) -> dict[str, Any] | None:
    """Return per-platform manifest entry, or ``None`` if absent."""
    manifest = read_manifest()
    if manifest is None:
        return None
    info = manifest["platforms"].get(platform)
    if not isinstance(info, dict):
        return None
    return info
