#!/usr/bin/env python3
"""echo_config.py — resolve the vault owner, endpoint, and API key for ECHO.

The plugin ships NO owner, endpoint, or key — it is fully user-agnostic. On each
machine, a machine-local JSON config supplies them:

    ~/.claude/echo-memory/config.json
    {
      "owner":    "Full Name (how the agent refers to the vault owner, third person)",
      "endpoint": "https://your-obsidian-rest-endpoint.example.com",
      "key":      "<obsidian-local-rest-api-bearer-token>"
    }

This file is per-machine: it must be created on each fresh install, is never
committed, and is never written into a vault note. To create it, run
`python3 echo.py config init` (writes a placeholder template when absent) and edit
it, or `python3 echo.py config set --owner ... --endpoint ... --key ...`.

Resolution — a COMPLETE baked credential set wins unconditionally:

  If the artifact carries both a baked endpoint AND key (a per-user build), those
  baked values are used as-is and CANNOT be shadowed by env vars or a config file.
  This is the default path: a delivered per-user plugin "just works" with no setup,
  and a stale ECHO_* env var or a leftover/placeholder ~/.claude config.json can
  never override or break it.

  Otherwise (nothing baked — the generic published plugin), fall back per-field,
  first hit wins:
    owner     env ECHO_OWNER  ->  config "owner"
    endpoint  env ECHO_BASE   ->  config "endpoint"
    key       env ECHO_KEY    ->  config "key"

The config directory honors $CLAUDE_CONFIG_DIR (defaults to ~/.claude); the config
file path itself can be overridden with $ECHO_CONFIG. Pure stdlib only.

BAKED CREDENTIALS (the default tier): the DEFAULT_* constants below are EMPTY in
source — the committed plugin ships zero credentials. `build.py --bake-key`
substitutes a specific user's owner/endpoint/key into them at package time,
producing a per-user artifact that needs no config file. This is how the plugin
works in the CoWork sandbox, where the user's ~/.claude is not bridged: the plugin
itself (mounted read-only every session) carries the values. A baked artifact is
secret-bearing — deliver it directly to that one user, NEVER commit or publish it.
On a normal host the empty defaults mean the env/config-file path takes over.
"""
from __future__ import annotations

import json
import os
from pathlib import Path

# Build-time injection targets — MUST stay empty string literals in source so the
# committed tree ships no secret. `build.py --bake-key` rewrites these lines.
DEFAULT_OWNER = ""
DEFAULT_BASE = ""
DEFAULT_KEY = ""

TEMPLATE = {
    "owner": "Full Name — how the agent should refer to the vault owner (third person)",
    "endpoint": "https://your-obsidian-rest-endpoint.example.com",
    "key": "<obsidian-local-rest-api-bearer-token>",
}


def claude_dir() -> Path:
    """The Claude config directory ($CLAUDE_CONFIG_DIR, else ~/.claude)."""
    return Path(os.environ.get("CLAUDE_CONFIG_DIR") or (Path.home() / ".claude"))


def config_path() -> Path:
    """Absolute path to the machine-local config file."""
    override = os.environ.get("ECHO_CONFIG")
    if override:
        return Path(override).expanduser()
    return claude_dir() / "echo-memory" / "config.json"


def _from_file() -> dict:
    p = config_path()
    if not p.exists():
        return {}
    try:
        data = json.loads(p.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}
    return data if isinstance(data, dict) else {}


def baked_complete() -> bool:
    """True when the artifact carries a usable baked endpoint AND key — i.e. a
    per-user `--bake-key` build. When True, the baked values win unconditionally."""
    return bool(DEFAULT_BASE and DEFAULT_KEY)


def load() -> dict:
    """Return {owner, endpoint, key}. A complete baked set wins unconditionally;
    otherwise env overrides the file, missing -> "".

    Never raises — callers that REQUIRE a field use resolve_endpoint()/resolve_key(),
    which raise a helpful error. This keeps `--help`, `config`, and `doctor` usable
    even when nothing is configured yet."""
    f = _from_file()
    if baked_complete():
        # Per-user artifact: baked creds are authoritative and must not be shadowed
        # by a stale env var or a leftover/placeholder config file. Owner is purely
        # descriptive, so it may still fall back when not baked.
        return {
            "owner": DEFAULT_OWNER or os.environ.get("ECHO_OWNER") or f.get("owner") or "",
            "endpoint": DEFAULT_BASE.rstrip("/"),
            "key": DEFAULT_KEY,
        }
    endpoint = (os.environ.get("ECHO_BASE") or f.get("endpoint") or "").rstrip("/")
    return {
        "owner": os.environ.get("ECHO_OWNER") or f.get("owner") or "",
        "endpoint": endpoint,
        "key": os.environ.get("ECHO_KEY") or f.get("key") or "",
    }


def _missing(field: str, env: str) -> RuntimeError:
    return RuntimeError(
        f"echo: no {field} configured. Set {env}, or create {config_path()} "
        f"(run `echo.py config init` to scaffold it, then edit). "
        f'Expected JSON keys: "owner", "endpoint", "key".'
    )


def resolve_endpoint() -> str:
    ep = load()["endpoint"]
    if not ep:
        raise _missing("endpoint", "ECHO_BASE")
    return ep


def resolve_key() -> str:
    k = load()["key"]
    if not k:
        raise _missing("key", "ECHO_KEY")
    return k


def resolve_owner() -> str:
    """The owner display name. Descriptive only, so an empty value is tolerated."""
    return load()["owner"]


def is_configured(cfg: dict | None = None) -> bool:
    """True only when endpoint AND key are present and NOT the untouched template
    placeholders — so a freshly `config init`-ed (but unedited) file reads as not yet
    configured, which is what triggers the first-run "ask for the key file" prompt."""
    c = cfg if cfg is not None else load()
    ep, key = c.get("endpoint", ""), c.get("key", "")
    if not ep or not key:
        return False
    if "your-obsidian-rest-endpoint" in ep or key.startswith("<"):
        return False
    return True


def source(field: str) -> str:
    """Where a field is currently coming from — for `config`/`doctor` reporting."""
    baked = {"owner": DEFAULT_OWNER, "endpoint": DEFAULT_BASE, "key": DEFAULT_KEY}[field]
    # A complete baked set wins unconditionally (see load()): endpoint/key always
    # come from the artifact, and owner too whenever it was baked.
    if baked_complete() and (field in ("endpoint", "key") or baked):
        return "baked-in (plugin)"
    env = {"owner": "ECHO_OWNER", "endpoint": "ECHO_BASE", "key": "ECHO_KEY"}[field]
    if os.environ.get(env):
        return f"{env} env"
    if config_path().exists() and _from_file().get(field):
        return "config file"
    if baked:
        return "baked-in (plugin)"
    return "unset"


def template_text() -> str:
    return json.dumps(TEMPLATE, indent=2, ensure_ascii=False) + "\n"


def _secure(p: Path) -> None:
    try:
        os.chmod(p, 0o600)  # advisory on filesystems without POSIX mode bits (e.g. Windows)
    except OSError:
        pass


def write_config(owner: str, endpoint: str, key: str) -> Path:
    """Persist a filled-in config (merging over any existing values), chmod 0600."""
    cur = _from_file()
    merged = {
        "owner": owner if owner is not None else cur.get("owner", ""),
        "endpoint": (endpoint if endpoint is not None else cur.get("endpoint", "")).rstrip("/"),
        "key": key if key is not None else cur.get("key", ""),
    }
    p = config_path()
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(json.dumps(merged, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    _secure(p)
    return p


def import_file(src) -> Path:
    """Adopt a config file someone was handed: validate it has a usable endpoint+key,
    then copy it into config_path(). The 'provide the file -> plugin installs it' path."""
    p = Path(src).expanduser()
    if not p.exists():
        raise RuntimeError(f"config import: file not found: {p}")
    try:
        data = json.loads(p.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError) as exc:
        raise RuntimeError(f"config import: {p} is not valid JSON ({exc})")
    if not isinstance(data, dict) or not is_configured(data):
        raise RuntimeError(
            'config import: file must be JSON with a real "endpoint" and "key" '
            '(and ideally "owner") — not the template placeholders.')
    return write_config(data.get("owner", ""), data["endpoint"], data["key"])


def init_template() -> tuple[Path, bool]:
    """Write the placeholder template to config_path() if absent.
    Returns (path, created) — created is False when a config already exists."""
    p = config_path()
    if p.exists():
        return p, False
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(template_text(), encoding="utf-8")
    _secure(p)
    return p, True
