"""Session key caching — store the derived AES key in the OS keychain so
subsequent commands skip the master-password prompt for N minutes.

Backends (in priority order):
  1. keyring library  → macOS Keychain / Linux SecretService / Windows Credential
  2. Encrypted file   → ~/.pm/.session_key  (fallback when keyring unavailable)

If neither backend is usable the helper returns None silently and the caller
falls back to prompting for the master password.
"""

import hashlib
import json
import os
from datetime import datetime, timezone, timedelta
from pathlib import Path
from typing import Optional

KEYRING_SERVICE = "pm-vault"
DEFAULT_TIMEOUT = 15  # minutes
_SESSION_META_FILENAME = ".session"


# ── Helpers ───────────────────────────────────────────────────────────────────

def _vault_id(vault_path: Path) -> str:
    """Short stable ID derived from the vault path — used as keyring username."""
    return hashlib.sha256(str(vault_path.resolve()).encode()).hexdigest()[:16]


def _meta_file(vault_path: Path) -> Path:
    return vault_path.parent / _SESSION_META_FILENAME


def _fallback_key_file(vault_path: Path) -> Path:
    return vault_path.parent / ".session_key"


# ── Keyring backend ───────────────────────────────────────────────────────────

def _keyring_save(vault_id: str, hex_key: str) -> bool:
    try:
        import keyring  # type: ignore
        keyring.set_password(KEYRING_SERVICE, vault_id, hex_key)
        return True
    except Exception:
        return False


def _keyring_load(vault_id: str) -> Optional[str]:
    try:
        import keyring  # type: ignore
        return keyring.get_password(KEYRING_SERVICE, vault_id)
    except Exception:
        return None


def _keyring_clear(vault_id: str) -> None:
    try:
        import keyring  # type: ignore
        keyring.delete_password(KEYRING_SERVICE, vault_id)
    except Exception:
        pass


# ── Fallback: encrypted file backend ─────────────────────────────────────────
# The key is XOR-encrypted with a machine-unique secret so it isn't stored raw.

def _machine_secret() -> bytes:
    """Stable per-machine secret derived from machine UUID and home dir."""
    seed = (os.environ.get("USER", "") + str(Path.home())).encode()
    return hashlib.sha256(seed).digest()


def _file_save(vault_path: Path, hex_key: str) -> bool:
    try:
        kf = _fallback_key_file(vault_path)
        raw = bytes.fromhex(hex_key)
        secret = _machine_secret()
        obfuscated = bytes(b ^ secret[i % len(secret)] for i, b in enumerate(raw))
        kf.write_bytes(obfuscated)
        kf.chmod(0o600)
        return True
    except Exception:
        return False


def _file_load(vault_path: Path) -> Optional[str]:
    try:
        kf = _fallback_key_file(vault_path)
        if not kf.exists():
            return None
        obfuscated = kf.read_bytes()
        secret = _machine_secret()
        raw = bytes(b ^ secret[i % len(secret)] for i, b in enumerate(obfuscated))
        return raw.hex()
    except Exception:
        return None


def _file_clear(vault_path: Path) -> None:
    try:
        kf = _fallback_key_file(vault_path)
        if kf.exists():
            kf.unlink()
    except Exception:
        pass


# ── Public API ────────────────────────────────────────────────────────────────

def save_session(vault_path: Path, key: bytes, timeout_minutes: int = DEFAULT_TIMEOUT) -> None:
    """Cache *key* in the OS keychain and write a TTL metadata file."""
    vid = _vault_id(vault_path)
    hex_key = key.hex()

    # Try keyring first, fall back to file
    saved = _keyring_save(vid, hex_key) or _file_save(vault_path, hex_key)
    if not saved:
        return  # silently give up — caller will prompt next time

    expires = (datetime.now(timezone.utc) + timedelta(minutes=timeout_minutes)).isoformat()
    mf = _meta_file(vault_path)
    mf.write_text(json.dumps({"expires": expires, "vid": vid, "backend": "keyring"}))
    mf.chmod(0o600)


def load_session(vault_path: Path) -> Optional[bytes]:
    """Return cached key if session is still valid, else None."""
    mf = _meta_file(vault_path)
    if not mf.exists():
        return None
    try:
        data = json.loads(mf.read_text())
        expires = datetime.fromisoformat(data["expires"])
        if datetime.now(timezone.utc) > expires:
            clear_session(vault_path)
            return None
        vid = data.get("vid", _vault_id(vault_path))
        hex_key = _keyring_load(vid) or _file_load(vault_path)
        if hex_key:
            return bytes.fromhex(hex_key)
    except Exception:
        pass
    return None


def clear_session(vault_path: Path) -> None:
    """Remove all cached session data."""
    mf = _meta_file(vault_path)
    vid = _vault_id(vault_path)
    _keyring_clear(vid)
    _file_clear(vault_path)
    if mf.exists():
        try:
            mf.unlink()
        except Exception:
            pass


def session_expires_in(vault_path: Path) -> Optional[int]:
    """Return seconds remaining in the current session, or None if no session."""
    mf = _meta_file(vault_path)
    if not mf.exists():
        return None
    try:
        data = json.loads(mf.read_text())
        expires = datetime.fromisoformat(data["expires"])
        remaining = (expires - datetime.now(timezone.utc)).total_seconds()
        return max(0, int(remaining))
    except Exception:
        return None
