"""Secret registry for Prismor Warden cloaking.

Stores real secret values on disk under ``$PRISMOR_SECRETS_DIR`` (default:
``~/.prismor/secrets``) with tight permissions (directory ``0700``, files
``0600``). Each file's name is the placeholder identifier the model will
reference as ``@@SECRET:<name>@@``; the file contents are the real value.

Design notes:
  * Values are never printed by ``list_secrets``. Only names are listed.
  * ``add_secret`` refuses empty values and rejects names that would traverse
    out of the secrets directory.
  * Removing a secret leaves any commands that referenced it failing closed
    — the decloak hook denies the tool call when a referenced secret
    file is missing.
"""
from __future__ import annotations

import os
import re
import stat
from pathlib import Path
from typing import List

# Valid placeholder names mirror the regex used by the decloak hook.
_NAME_RE = re.compile(r"^[a-zA-Z0-9_-]{1,64}$")


def secrets_dir() -> Path:
    """Return the secrets directory path, honoring $PRISMOR_SECRETS_DIR."""
    override = os.environ.get("PRISMOR_SECRETS_DIR")
    if override:
        return Path(override).expanduser()
    return Path(os.environ.get("PRISMOR_HOME", Path.home() / ".prismor")) / "secrets"


def _ensure_dir() -> Path:
    path = secrets_dir()
    path.mkdir(parents=True, exist_ok=True)
    try:
        path.chmod(0o700)
    except PermissionError:
        pass
    return path


def _validate_name(name: str) -> str:
    if not _NAME_RE.fullmatch(name):
        raise ValueError(
            f"Invalid secret name {name!r}: must match {_NAME_RE.pattern}"
        )
    return name


def add_secret(name: str, value: str) -> Path:
    """Register a real secret value under ``name``. Overwrites if exists.

    The caller is responsible for reading ``value`` from a safe source
    (stdin or a file) rather than argv, to avoid leaking via process lists.
    """
    _validate_name(name)
    if not value:
        raise ValueError("Secret value is empty — refusing to store.")
    path = _ensure_dir() / name
    path.write_text(value, encoding="utf-8")
    try:
        path.chmod(0o600)
    except PermissionError:
        pass
    return path


def remove_secret(name: str) -> bool:
    """Delete a registered secret. Returns True if something was removed."""
    _validate_name(name)
    path = secrets_dir() / name
    if path.exists():
        path.unlink()
        return True
    return False


def list_secrets() -> List[dict]:
    """List registered secrets (names and metadata — never values).

    Returns a list of dicts with ``name``, ``bytes``, and ``modified`` keys.
    """
    path = secrets_dir()
    if not path.exists():
        return []
    entries: List[dict] = []
    for child in sorted(path.iterdir()):
        if not child.is_file():
            continue
        try:
            st = child.stat()
        except OSError:
            continue
        entries.append(
            {
                "name": child.name,
                "bytes": st.st_size,
                "modified": int(st.st_mtime),
                "auto": child.name.startswith("auto_"),
            }
        )
    return entries


def check_permissions() -> List[str]:
    """Audit permission modes on the secrets dir and files. Returns warnings."""
    warnings: List[str] = []
    path = secrets_dir()
    if not path.exists():
        return warnings
    dir_mode = stat.S_IMODE(path.stat().st_mode)
    if dir_mode & 0o077:
        warnings.append(
            f"secrets directory {path} has mode {oct(dir_mode)}; expected 0700"
        )
    for child in path.iterdir():
        if not child.is_file():
            continue
        mode = stat.S_IMODE(child.stat().st_mode)
        if mode & 0o077:
            warnings.append(
                f"{child.name} has mode {oct(mode)}; expected 0600"
            )
    return warnings
