"""Token manager for Slack bot/user token lifecycle."""

import os
from pathlib import Path

import httpx
from dotenv import load_dotenv

from slack_mpm.auth.models import SlackToken, TokenStatus

SLACK_API_BASE = "https://slack.com/api"


def _load_env() -> None:
    """Load environment variables from .env and .env.local files.

    Searches multiple candidate directories in priority order so that the MCP
    server finds credentials regardless of the process working directory:

    1. Path.cwd() and each of its parents — preserves CLI / uv-run behaviour
       where the user invokes the command from inside the project tree.
    2. The project root inferred from this file's location — always resolves to
       the correct directory even when the process CWD is / or ~ (e.g. when
       Claude Desktop launches the MCP server as a GUI app).
       token_manager.py lives at src/slack_mpm/auth/token_manager.py, so
       Path(__file__).resolve().parents[3] is the project root.
    3. Path.home() — machine-wide fallback for users who place .env.local in
       their home directory.

    For each candidate directory, .env is loaded first (override=False) so
    existing environment variables take precedence, then .env.local is loaded
    with override=True so local secrets win over the shared base file.

    Search stops as soon as SLACK_BOT_TOKEN is present in os.environ (the only
    required credential). SLACK_USER_TOKEN is optional and never blocks early
    exit. Duplicate candidate paths are skipped automatically.
    """
    # Build the ordered, deduplicated list of candidate directories.
    candidates: list[Path] = []
    seen: set[Path] = set()

    def _add(directory: Path) -> None:
        resolved = directory.resolve()
        if resolved not in seen:
            seen.add(resolved)
            candidates.append(resolved)

    # 1. CWD and all its parents (walk upward).
    for ancestor in [Path.cwd(), *Path.cwd().parents]:
        _add(ancestor)

    # 2. Project root derived from this file's absolute location.
    _add(Path(__file__).resolve().parents[3])

    # 3. Home directory fallback.
    _add(Path.home())

    for directory in candidates:
        # Load base config; do not override vars already set in the environment.
        env_path = directory / ".env"
        if env_path.exists():
            load_dotenv(env_path, override=False)

        # Load local overrides on top of the base config.
        env_local_path = directory / ".env.local"
        if env_local_path.exists():
            load_dotenv(env_local_path, override=True)

        # Stop once the required bot token has been found.
        if os.environ.get("SLACK_BOT_TOKEN"):
            break


class TokenManager:
    """Manages Slack bot and user tokens loaded from environment variables."""

    def __init__(self) -> None:
        """Initialize token manager and load tokens from environment."""
        _load_env()
        self._bot_token: str | None = os.environ.get("SLACK_BOT_TOKEN")
        self._user_token: str | None = os.environ.get("SLACK_USER_TOKEN")
        self._signing_secret: str | None = os.environ.get("SLACK_SIGNING_SECRET")
        self._team_id: str | None = os.environ.get("SLACK_TEAM_ID")

    @property
    def bot_token(self) -> str | None:
        """Return the bot token if available."""
        return self._bot_token

    @property
    def user_token(self) -> str | None:
        """Return the user token if available."""
        return self._user_token

    @property
    def signing_secret(self) -> str | None:
        """Return the signing secret if available."""
        return self._signing_secret

    @property
    def team_id(self) -> str | None:
        """Return the configured team ID if available."""
        return self._team_id

    def has_bot_token(self) -> bool:
        """Return True if a bot token is configured."""
        return bool(self._bot_token)

    def has_user_token(self) -> bool:
        """Return True if a user token is configured."""
        return bool(self._user_token)

    def get_token(self, prefer_user: bool = False) -> str:
        """Return the best available token.

        Args:
            prefer_user: If True, prefer user token over bot token.

        Returns:
            The token string.

        Raises:
            ValueError: If no token is configured.
        """
        if prefer_user and self._user_token:
            return self._user_token
        if self._bot_token:
            return self._bot_token
        if self._user_token:
            return self._user_token
        raise ValueError(
            "No Slack token found. Set SLACK_BOT_TOKEN in your environment, "
            ".env.local file, or in the MCP server's env config."
        )

    async def validate_bot_token(self) -> SlackToken:
        """Validate the bot token by calling auth.test API.

        Returns:
            SlackToken with validation results.

        Raises:
            ValueError: If no bot token is configured.
        """
        if not self._bot_token:
            return SlackToken(
                token="",  # nosec B106 — empty sentinel, not a hardcoded credential
                token_type="bot",
                status=TokenStatus.MISSING,
            )

        return await self._validate_token(self._bot_token, "bot")

    async def validate_user_token(self) -> SlackToken:
        """Validate the user token by calling auth.test API.

        Returns:
            SlackToken with validation results.

        Raises:
            ValueError: If no user token is configured.
        """
        if not self._user_token:
            return SlackToken(
                token="",  # nosec B106 — empty sentinel, not a hardcoded credential
                token_type="user",
                status=TokenStatus.MISSING,
            )

        return await self._validate_token(self._user_token, "user")

    async def _validate_token(self, token: str, token_type: str) -> SlackToken:
        """Call auth.test to validate a token and return token info.

        Args:
            token: The Slack token to validate.
            token_type: Either 'bot' or 'user'.

        Returns:
            SlackToken with status and metadata populated.
        """
        slack_token = SlackToken(token=token, token_type=token_type)

        try:
            async with httpx.AsyncClient(timeout=10.0) as client:
                response = await client.post(
                    f"{SLACK_API_BASE}/auth.test",
                    headers={
                        "Authorization": f"Bearer {token}",
                        "Content-Type": "application/json; charset=utf-8",
                    },
                    json={},
                )
                data = response.json()

                if data.get("ok"):
                    slack_token.status = TokenStatus.VALID
                    slack_token.team_id = data.get("team_id")
                    slack_token.team_name = data.get("team")
                    slack_token.user_id = data.get("user_id")
                    slack_token.bot_id = data.get("bot_id")
                else:
                    slack_token.status = TokenStatus.INVALID

        except httpx.RequestError:
            slack_token.status = TokenStatus.UNKNOWN

        return slack_token
