import os
from enum import Enum
from functools import cache
from pathlib import Path
from typing import Literal, overload

from schwab.client import AsyncClient, Client


DEFAULT_TOKEN_PATH = Path.home() / ".credentials" / "schwab-token.json"

AUTH_METHOD_ENV_VAR = "SCHWAB_PY_UTILS_AUTH_METHOD"
TOKEN_PATH_ENV_VAR = "SCHWAB_PY_UTILS_TOKEN_PATH"
ENVCHAIN_CMD_ENV_VAR = "SCHWAB_PY_UTILS_ENVCHAIN_CMD"

_TOKEN_ONLY_PLACEHOLDER_API_KEY = "TOKEN_ONLY"
_TOKEN_ONLY_PLACEHOLDER_SECRET = "TOKEN_ONLY"


class SchwabClientMethod(Enum):
    """
    Methods to instantiate a Schwab API client:

    - easy_client: Use schwab-py's easy_client with file-based token storage
    - envchain_token: Fetch OAuth token via envchain (requires envchain-python)
    - envchain: Fetch OAuth token + credentials via envchain (requires envchain-python)
    """

    easy_client = "easy_client"
    envchain_token = "envchain_token"
    envchain = "envchain"


def _get_env_credentials() -> tuple[str, str, str]:
    """Get Schwab API credentials from environment variables."""
    api_key = os.getenv("SCHWAB_API_KEY")
    app_secret = os.getenv("SCHWAB_SECRET")
    callback_url = os.getenv("SCHWAB_CALLBACK_URL")

    if not api_key or not app_secret or not callback_url:
        raise ValueError(
            "Please set SCHWAB_API_KEY, SCHWAB_SECRET, and SCHWAB_CALLBACK_URL environment variables."
        )
    return api_key, app_secret, callback_url


def _resolve_method(method: SchwabClientMethod | str | None) -> SchwabClientMethod:
    """Resolve auth method from explicit argument or environment variable."""
    if isinstance(method, SchwabClientMethod):
        return method

    raw_method = method
    if raw_method is None:
        raw_method = os.getenv(
            AUTH_METHOD_ENV_VAR, SchwabClientMethod.easy_client.value
        )

    try:
        return SchwabClientMethod(raw_method)
    except ValueError:
        raise ValueError(f"Unknown method: {raw_method}")


def _resolve_token_path(token_path: str | Path | None) -> Path:
    """Resolve token path from explicit argument, env var, or default."""
    if token_path is None:
        token_path = os.getenv(TOKEN_PATH_ENV_VAR)

    if token_path is None:
        token_path = DEFAULT_TOKEN_PATH

    return Path(token_path).expanduser()


def _resolve_envchain_cmd(envchain_cmd: str | None) -> str:
    """Resolve envchain binary path from explicit argument, env var, or default."""
    if envchain_cmd:
        return envchain_cmd

    return os.getenv(ENVCHAIN_CMD_ENV_VAR, "/opt/homebrew/bin/envchain")


def _resolve_token_auth_credentials() -> tuple[str, str]:
    """
    Resolve API key + secret used by token-file clients for refresh operations.

    Priority:
    1. SCHWAB_API_KEY + SCHWAB_SECRET env vars
    2. envchain namespace "schwab-app-data-studies"
    3. Placeholder values (works if token remains valid and no refresh is needed)
    """
    api_key = os.getenv("SCHWAB_API_KEY")
    app_secret = os.getenv("SCHWAB_SECRET")
    if api_key and app_secret:
        return api_key, app_secret

    try:
        import envchain_python  # pyright: ignore[reportMissingImports]

        non_token_credentials = envchain_python.get_vars(
            "schwab-app-data-studies", "SCHWAB_API_KEY", "SCHWAB_SECRET"
        )

        envchain_api_key = non_token_credentials.get("SCHWAB_API_KEY")
        envchain_app_secret = non_token_credentials.get("SCHWAB_SECRET")
        if envchain_api_key and envchain_app_secret:
            return envchain_api_key, envchain_app_secret
    except Exception:
        pass

    return (
        api_key or _TOKEN_ONLY_PLACEHOLDER_API_KEY,
        app_secret or _TOKEN_ONLY_PLACEHOLDER_SECRET,
    )


@overload
def get_client(
    asyncio_: Literal[True],
    method: SchwabClientMethod | str | None = None,
    token_path: str | Path | None = None,
    envchain_cmd: str | None = None,
) -> AsyncClient: ...


@overload
def get_client(
    asyncio_: Literal[False] = False,
    method: SchwabClientMethod | str | None = None,
    token_path: str | Path | None = None,
    envchain_cmd: str | None = None,
) -> Client: ...


@overload
def get_client(
    asyncio_: bool = False,
    method: SchwabClientMethod | str | None = None,
    token_path: str | Path | None = None,
    envchain_cmd: str | None = None,
) -> Client | AsyncClient: ...


@cache
def get_client(
    asyncio_: bool = False,
    method: SchwabClientMethod | str | None = None,
    token_path: str | Path | None = None,
    envchain_cmd: str | None = None,
) -> Client | AsyncClient:
    """
    Get a Schwab API client.

    Args:
        asyncio_: If True, return an async client
        method: Authentication method to use. If not provided, resolves from
            SCHWAB_PY_UTILS_AUTH_METHOD or defaults to "easy_client".
        token_path: Path to token file (for easy_client method).
            If omitted, resolves from SCHWAB_PY_UTILS_TOKEN_PATH or defaults to
            ~/.credentials/schwab-token.json
        envchain_cmd: Path to envchain command (for envchain methods)

    Returns:
        Schwab Client or AsyncClient

    Note:
        Results are cached. Call `get_client.cache_clear()` if credentials change.
    """
    method = _resolve_method(method)
    envchain_cmd = _resolve_envchain_cmd(envchain_cmd)

    if method == SchwabClientMethod.easy_client:
        from schwab.auth import client_from_token_file, easy_client

        token_path = _resolve_token_path(token_path)
        token_path.parent.mkdir(parents=True, exist_ok=True)

        # Token-first mode: if token file exists, we can create a fully usable
        # client without requiring SCHWAB_* env vars.
        if token_path.is_file():
            api_key, app_secret = _resolve_token_auth_credentials()

            return client_from_token_file(
                token_path=str(token_path),
                api_key=api_key,
                app_secret=app_secret,
                asyncio=asyncio_,
            )

        # Fall back to easy_client login flow when no token exists.
        api_key, app_secret, callback_url = _get_env_credentials()

        return easy_client(
            api_key=api_key,
            app_secret=app_secret,
            callback_url=callback_url,
            token_path=str(token_path),
            asyncio=asyncio_,
        )

    elif method == SchwabClientMethod.envchain_token:
        from schwab.auth import client_from_access_functions

        from .auth import create_envchain_token_handlers

        api_key, app_secret, _ = _get_env_credentials()
        token_read_func, token_write_func = create_envchain_token_handlers(
            envchain_cmd=envchain_cmd
        )

        return client_from_access_functions(
            api_key=api_key,
            app_secret=app_secret,
            token_read_func=token_read_func,
            token_write_func=token_write_func,
            asyncio=asyncio_,
        )

    elif method == SchwabClientMethod.envchain:
        from schwab.auth import client_from_access_functions

        try:
            import envchain_python  # pyright: ignore[reportMissingImports]
        except ImportError:
            raise ImportError(
                "envchain-python is required for envchain method. "
                "Install with: pip install schwab-py-utils[envchain]"
            )

        from .auth import create_envchain_token_handlers

        non_token_credentials = envchain_python.get_vars(
            "schwab-app-data-studies", "SCHWAB_API_KEY", "SCHWAB_SECRET"
        )
        api_key = non_token_credentials["SCHWAB_API_KEY"]
        app_secret = non_token_credentials["SCHWAB_SECRET"]

        token_read_func, token_write_func = create_envchain_token_handlers(
            envchain_cmd=envchain_cmd
        )

        return client_from_access_functions(
            api_key=api_key,
            app_secret=app_secret,
            token_read_func=token_read_func,
            token_write_func=token_write_func,
            asyncio=asyncio_,
        )

    else:
        raise ValueError(f"Unknown method: {method}")


# Alias for backwards compatibility
get_schwab_client = get_client
SchwabPyGetClientMethod = SchwabClientMethod
