"""
ID Wispera Passphrase Provider
Resolves the vault passphrase through a priority chain of strategies.

Resolution order:
  1. Session token  — IDW_SESSION_TOKEN env var -> sidecar decrypt
  2. OS keychain    — keyring (optional)
  3. Env var / .env — IDW_PASSPHRASE from environment or .env file
  4. Interactive     — masked terminal prompt (CLI only)
"""

import os
import stat
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, Literal, Optional, Tuple

from id_wispera.auth.keychain_provider import KeychainProvider
from id_wispera.auth.session_token import SessionTokenManager, SESSION_TOKEN_ENV_VAR

# ──────────────────────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────────────────────

PASSPHRASE_ENV_VAR = "IDW_PASSPHRASE"

NO_PASSPHRASE_ERROR = (
    "No passphrase available. Options:\n"
    "  - Run 'idw auth login' to store in OS keychain\n"
    "  - Run 'idw auth token create' for headless / CI environments\n"
    f"  - Set {PASSPHRASE_ENV_VAR} in your environment or a .env file"
)

# ──────────────────────────────────────────────────────────────────────────────
# Types
# ──────────────────────────────────────────────────────────────────────────────

PassphraseSource = Literal[
    "session-token", "keychain", "env-var", "dotenv-file", "interactive"
]


@dataclass
class PassphraseResult:
    passphrase: str
    source: PassphraseSource


# ──────────────────────────────────────────────────────────────────────────────
# Provider
# ──────────────────────────────────────────────────────────────────────────────


class PassphraseProvider:
    """
    Resolves the vault passphrase by walking a strategy chain.

    Args:
        allow_interactive: Allow masked terminal prompt (default False).
        allow_env_var: Allow the IDW_PASSPHRASE env var and .env file
            lookup (default True).
        prompt_fn: Custom prompt function ``(message) -> passphrase``.
        sidecar_path: Custom path for the session token sidecar file.
    """

    def __init__(
        self,
        *,
        allow_interactive: bool = False,
        allow_env_var: bool = True,
        prompt_fn: Optional[Callable[[str], str]] = None,
        sidecar_path: Optional[str] = None,
    ) -> None:
        self._allow_interactive = allow_interactive
        self._allow_env_var = allow_env_var
        self._prompt_fn = prompt_fn
        self._keychain = KeychainProvider()
        self._token_manager = SessionTokenManager(sidecar_path)

    def resolve(self) -> PassphraseResult:
        """
        Resolve the vault passphrase by walking the strategy chain.

        Raises:
            RuntimeError: If no strategy succeeds.
        """

        # ── 1. Session token ────────────────────────────────────────────
        token = SessionTokenManager.get_token_from_env()
        if token:
            passphrase = self._token_manager.resolve(token)
            return PassphraseResult(passphrase=passphrase, source="session-token")

        # ── 2. OS keychain ──────────────────────────────────────────────
        kc_passphrase = self._keychain.retrieve()
        if kc_passphrase is not None:
            return PassphraseResult(passphrase=kc_passphrase, source="keychain")

        # ── 3. IDW_PASSPHRASE env var or .env file ──────────────────────
        if self._allow_env_var:
            result = self._resolve_from_env_or_dotenv()
            if result is not None:
                return result

        # ── 4. Interactive prompt ───────────────────────────────────────
        if self._allow_interactive:
            passphrase = self._prompt()
            if passphrase:
                return PassphraseResult(passphrase=passphrase, source="interactive")

        # ── Nothing worked ──────────────────────────────────────────────
        raise RuntimeError(NO_PASSPHRASE_ERROR)

    def get_passphrase(self) -> str:
        """Convenience: resolve and return just the passphrase string."""
        return self.resolve().passphrase

    # ── internal ─────────────────────────────────────────────────────────

    @staticmethod
    def _resolve_from_env_or_dotenv() -> Optional[PassphraseResult]:
        """Check process env, then .env files for IDW_PASSPHRASE."""
        # Process environment first
        env_val = os.environ.get(PASSPHRASE_ENV_VAR)
        if env_val:
            return PassphraseResult(passphrase=env_val, source="env-var")

        # .env files: CWD first, then ~/.id-wispera/
        candidates = [
            Path.cwd() / ".env",
            Path.home() / ".id-wispera" / ".env",
        ]
        for filepath in candidates:
            val = PassphraseProvider._read_passphrase_from_dotenv(filepath)
            if val:
                PassphraseProvider._warn_if_world_readable(filepath)
                return PassphraseResult(passphrase=val, source="dotenv-file")
        return None

    @staticmethod
    def _read_passphrase_from_dotenv(filepath: Path) -> Optional[str]:
        """Parse a .env file and return the IDW_PASSPHRASE value, or None."""
        try:
            for line in filepath.read_text(encoding="utf-8").splitlines():
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                eq_idx = line.find("=")
                if eq_idx == -1:
                    continue
                key = line[:eq_idx].strip()
                if key != PASSPHRASE_ENV_VAR:
                    continue
                val = line[eq_idx + 1 :].strip()
                # Strip surrounding quotes
                if len(val) >= 2 and (
                    (val[0] == '"' and val[-1] == '"')
                    or (val[0] == "'" and val[-1] == "'")
                ):
                    val = val[1:-1]
                return val or None
        except (OSError, UnicodeDecodeError):
            pass
        return None

    @staticmethod
    def _warn_if_world_readable(filepath: Path) -> None:
        """Warn to stderr if a .env file is world-readable."""
        try:
            mode = filepath.stat().st_mode
            if mode & stat.S_IROTH:
                print(
                    f"[WARNING] {filepath} is world-readable. "
                    f"Consider running: chmod 600 {filepath}",
                    file=sys.stderr,
                )
        except OSError:
            pass

    def _prompt(self) -> Optional[str]:
        if self._prompt_fn is not None:
            return self._prompt_fn("Enter vault passphrase:")

        try:
            import getpass

            return getpass.getpass("Enter vault passphrase: ")
        except (EOFError, KeyboardInterrupt):
            return None
