"""Environment and API key management for last30days skill."""

import base64
import json
import os
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Dict, Any, Literal, Iterable, List

# Allow override via environment variable for testing
# Set LAST30DAYS_CONFIG_DIR="" for clean/no-config mode
# Set LAST30DAYS_CONFIG_DIR="/path/to/dir" for custom config location
_config_override = os.environ.get('LAST30DAYS_CONFIG_DIR')
if _config_override == "":
    # Empty string = no config file (clean mode)
    CONFIG_DIR = None
    CONFIG_FILE = None
elif _config_override:
    CONFIG_DIR = Path(_config_override)
    CONFIG_FILE = CONFIG_DIR / ".env"
else:
    CONFIG_DIR = Path.home() / ".config" / "last30days"
    CONFIG_FILE = CONFIG_DIR / ".env"

CODEX_AUTH_FILE = Path(os.environ.get("CODEX_AUTH_FILE", str(Path.home() / ".codex" / "auth.json")))

AuthSource = Literal["api_key", "codex", "none"]
AuthStatus = Literal["ok", "missing", "expired", "missing_account_id"]

AUTH_SOURCE_API_KEY: AuthSource = "api_key"
AUTH_SOURCE_CODEX: AuthSource = "codex"
AUTH_SOURCE_NONE: AuthSource = "none"

AUTH_STATUS_OK: AuthStatus = "ok"
AUTH_STATUS_MISSING: AuthStatus = "missing"
AUTH_STATUS_EXPIRED: AuthStatus = "expired"
AUTH_STATUS_MISSING_ACCOUNT_ID: AuthStatus = "missing_account_id"

PLACEHOLDER_LITERALS = {
    "...",
    "sk-...",
    "sk-or-...",
    "xai-...",
    "null",
    "none",
    "changeme",
    "change_me",
    "replace_me",
    "replace-me",
    "your_key",
    "your-api-key",
    "your_api_key",
}
PLACEHOLDER_PREFIXES = (
    "your_",
    "your-",
    "replace_",
    "replace-",
    "paste_",
    "paste-",
)
PLACEHOLDER_SUFFIXES = (
    "_here",
    "-here",
)


@dataclass(frozen=True)
class OpenAIAuth:
    token: Optional[str]
    source: AuthSource
    status: AuthStatus
    account_id: Optional[str]
    codex_auth_file: str


def load_env_file(path: Path) -> Dict[str, str]:
    """Load environment variables from a file."""
    env = {}
    if not path.exists():
        return env

    with open(path, 'r') as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            if '=' in line:
                key, _, value = line.partition('=')
                key = key.strip()
                value = value.strip()
                # Remove quotes if present
                if value and value[0] in ('"', "'") and value[-1] == value[0]:
                    value = value[1:-1]
                if key and value:
                    env[key] = value
    return env


def _deep_merge_dict(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
    """Recursively merge dictionaries."""
    result: Dict[str, Any] = dict(base)
    for key, value in override.items():
        if (
            key in result
            and isinstance(result[key], dict)
            and isinstance(value, dict)
        ):
            result[key] = _deep_merge_dict(result[key], value)
        else:
            result[key] = value
    return result


def _unique_paths(paths: Iterable[Path]) -> List[Path]:
    """Deduplicate paths while preserving order."""
    seen = set()
    result: List[Path] = []
    for path in paths:
        key = str(path)
        if key in seen:
            continue
        seen.add(key)
        result.append(path)
    return result


def _settings_path_candidates() -> List[Path]:
    """Build candidate settings.json paths.

    Priority:
    1) Global app settings (data/settings/settings.json)
    2) Local settings files (settings.json, .meta/settings.json)
    3) Project settings when PROJECT_ID is available
    """
    cwd = Path.cwd().resolve()
    ancestors = [cwd] + list(cwd.parents)
    # Keep search bounded; repo roots are usually within a few levels
    ancestors = ancestors[:8]
    project_id = (os.environ.get("PROJECT_ID") or "").strip()

    global_paths: List[Path] = []
    local_paths: List[Path] = []
    project_paths: List[Path] = []

    for base in ancestors:
        global_paths.append(base / "data" / "settings" / "settings.json")
        local_paths.append(base / "settings.json")
        local_paths.append(base / ".meta" / "settings.json")
        if project_id:
            project_paths.append(base / "data" / "projects" / project_id / "settings.json")
            project_paths.append(base / "data" / "projects" / project_id / ".meta" / "settings.json")

    # Later files override earlier files
    ordered = _unique_paths(global_paths + local_paths + project_paths)
    return ordered


def _load_settings_overrides() -> Dict[str, Any]:
    """Load app/project settings.json and extract API key overrides."""
    merged_settings: Dict[str, Any] = {}
    for path in _settings_path_candidates():
        if not path.exists():
            continue
        try:
            with open(path, "r") as f:
                data = json.load(f)
            if isinstance(data, dict):
                merged_settings = _deep_merge_dict(merged_settings, data)
        except Exception:
            continue

    if not merged_settings:
        return {}

    overrides: Dict[str, Any] = {}

    # search.apiKey (Tavily key used by app search provider)
    search_cfg = merged_settings.get("search")
    if isinstance(search_cfg, dict):
        search_key = search_cfg.get("apiKey")
        if isinstance(search_key, str):
            overrides["TAVILY_API_KEY"] = search_key

    # Provider model keys from app settings (fallbacks for skill runtime)
    for model_key in ("chatModel", "utilityModel", "embeddingsModel"):
        cfg = merged_settings.get(model_key)
        if not isinstance(cfg, dict):
            continue
        provider = str(cfg.get("provider", "")).strip().lower()
        api_key = cfg.get("apiKey")
        if not isinstance(api_key, str):
            continue
        if provider == "openai" and "OPENAI_API_KEY" not in overrides:
            overrides["OPENAI_API_KEY"] = api_key
        elif provider == "openrouter" and "OPENROUTER_API_KEY" not in overrides:
            overrides["OPENROUTER_API_KEY"] = api_key

    return overrides


def _decode_jwt_payload(token: str) -> Optional[Dict[str, Any]]:
    """Decode JWT payload without verification."""
    try:
        parts = token.split(".")
        if len(parts) < 2:
            return None
        payload_b64 = parts[1]
        pad = "=" * (-len(payload_b64) % 4)
        decoded = base64.urlsafe_b64decode(payload_b64 + pad)
        return json.loads(decoded.decode("utf-8"))
    except Exception:
        return None


def _token_expired(token: str, leeway_seconds: int = 60) -> bool:
    """Check if JWT token is expired."""
    payload = _decode_jwt_payload(token)
    if not payload:
        return False
    exp = payload.get("exp")
    if not exp:
        return False
    return exp <= (time.time() + leeway_seconds)


def extract_chatgpt_account_id(access_token: str) -> Optional[str]:
    """Extract chatgpt_account_id from JWT token."""
    payload = _decode_jwt_payload(access_token)
    if not payload:
        return None
    auth_claim = payload.get("https://api.openai.com/auth", {})
    if isinstance(auth_claim, dict):
        return auth_claim.get("chatgpt_account_id")
    return None


def load_codex_auth(path: Path = CODEX_AUTH_FILE) -> Dict[str, Any]:
    """Load Codex auth JSON."""
    if not path.exists():
        return {}
    try:
        with open(path, "r") as f:
            return json.load(f)
    except Exception:
        return {}


def get_codex_access_token() -> tuple[Optional[str], str]:
    """Get Codex access token from auth.json.

    Returns:
        (token, status) where status is 'ok', 'missing', or 'expired'
    """
    auth = load_codex_auth()
    token = None
    if isinstance(auth, dict):
        tokens = auth.get("tokens") or {}
        if isinstance(tokens, dict):
            token = tokens.get("access_token")
        if not token:
            token = auth.get("access_token")
    if not token:
        return None, AUTH_STATUS_MISSING
    if _token_expired(token):
        return None, AUTH_STATUS_EXPIRED
    return token, AUTH_STATUS_OK


def get_openai_auth(file_env: Dict[str, str], settings_overrides: Optional[Dict[str, Any]] = None) -> OpenAIAuth:
    """Resolve OpenAI auth from API key or Codex login."""
    settings_overrides = settings_overrides or {}
    api_key = _resolve_secret(
        os.environ.get('OPENAI_API_KEY'),
        file_env.get('OPENAI_API_KEY'),
        settings_overrides.get('OPENAI_API_KEY'),
    )
    if api_key:
        return OpenAIAuth(
            token=api_key,
            source=AUTH_SOURCE_API_KEY,
            status=AUTH_STATUS_OK,
            account_id=None,
            codex_auth_file=str(CODEX_AUTH_FILE),
        )

    codex_token, codex_status = get_codex_access_token()
    if codex_token:
        account_id = extract_chatgpt_account_id(codex_token)
        if account_id:
            return OpenAIAuth(
                token=codex_token,
                source=AUTH_SOURCE_CODEX,
                status=AUTH_STATUS_OK,
                account_id=account_id,
                codex_auth_file=str(CODEX_AUTH_FILE),
            )
        return OpenAIAuth(
            token=None,
            source=AUTH_SOURCE_CODEX,
            status=AUTH_STATUS_MISSING_ACCOUNT_ID,
            account_id=None,
            codex_auth_file=str(CODEX_AUTH_FILE),
        )

    return OpenAIAuth(
        token=None,
        source=AUTH_SOURCE_NONE,
        status=codex_status,
        account_id=None,
        codex_auth_file=str(CODEX_AUTH_FILE),
    )


def get_config() -> Dict[str, Any]:
    """Load configuration from ~/.config/last30days/.env and environment."""
    # Load from config file first (if configured)
    file_env = load_env_file(CONFIG_FILE) if CONFIG_FILE else {}
    settings_overrides = _load_settings_overrides()

    openai_auth = get_openai_auth(file_env, settings_overrides=settings_overrides)

    # Build config: Codex/OpenAI auth + process.env > .env file
    config = {
        'OPENAI_API_KEY': openai_auth.token,
        'OPENAI_AUTH_SOURCE': openai_auth.source,
        'OPENAI_AUTH_STATUS': openai_auth.status,
        'OPENAI_CHATGPT_ACCOUNT_ID': openai_auth.account_id,
        'CODEX_AUTH_FILE': openai_auth.codex_auth_file,
    }

    secret_keys = (
        'XAI_API_KEY',
        'OPENROUTER_API_KEY',
        'PARALLEL_API_KEY',
        'BRAVE_API_KEY',
        'TAVILY_API_KEY',
        'AUTH_TOKEN',
        'CT0',
    )
    for key in secret_keys:
        config[key] = _resolve_secret(
            os.environ.get(key),
            file_env.get(key),
            settings_overrides.get(key),
        )

    plain_keys = [
        ('OPENAI_MODEL_POLICY', 'auto'),
        ('OPENAI_MODEL_PIN', None),
        ('XAI_MODEL_POLICY', 'latest'),
        ('XAI_MODEL_PIN', None),
    ]
    for key, default in plain_keys:
        config[key] = os.environ.get(key) or file_env.get(key, default)

    return config


def config_exists() -> bool:
    """Check if configuration file exists."""
    return bool(CONFIG_FILE and CONFIG_FILE.exists())


def _normalize_secret(value: Optional[str]) -> Optional[str]:
    """Normalize secret value and ignore obvious placeholders."""
    if value is None:
        return None

    v = value.strip()
    if not v:
        return None

    if len(v) >= 2 and v[0] in ('"', "'") and v[-1] == v[0]:
        v = v[1:-1].strip()

    if not v:
        return None

    lower = v.lower()
    if lower in PLACEHOLDER_LITERALS:
        return None
    if "****" in v:
        return None
    if "..." in v:
        return None
    if lower.startswith(PLACEHOLDER_PREFIXES):
        return None
    if lower.endswith(PLACEHOLDER_SUFFIXES):
        return None

    return v


def _resolve_secret(*values: Optional[str]) -> Optional[str]:
    """Return the first non-placeholder secret value from a list."""
    for value in values:
        secret = _normalize_secret(value)
        if secret:
            return secret
    return None


def get_available_sources(config: Dict[str, Any]) -> str:
    """Determine which sources are available based on API keys.

    Returns: 'all', 'both', 'reddit', 'reddit-web', 'x', 'x-web', 'web', or 'none'
    """
    has_openai = bool(config.get('OPENAI_API_KEY')) and config.get('OPENAI_AUTH_STATUS') == AUTH_STATUS_OK
    has_xai = bool(config.get('XAI_API_KEY'))
    has_web = has_web_search_keys(config)

    if has_openai and has_xai:
        return 'all' if has_web else 'both'
    elif has_openai:
        return 'reddit-web' if has_web else 'reddit'
    elif has_xai:
        return 'x-web' if has_web else 'x'
    elif has_web:
        return 'web'
    else:
        return 'web'  # Fallback: assistant WebSearch (no API keys needed)


def has_web_search_keys(config: Dict[str, Any]) -> bool:
    """Check if any web search API keys are configured."""
    return bool(
        config.get('OPENROUTER_API_KEY')
        or config.get('PARALLEL_API_KEY')
        or config.get('BRAVE_API_KEY')
        or config.get('TAVILY_API_KEY')
    )


def get_web_search_source(config: Dict[str, Any]) -> Optional[str]:
    """Determine the best available web search backend.

    Priority: Parallel AI > Brave > Tavily > OpenRouter/Sonar Pro

    Returns: 'parallel', 'brave', 'openrouter', 'tavily', or None
    """
    if config.get('PARALLEL_API_KEY'):
        return 'parallel'
    if config.get('BRAVE_API_KEY'):
        return 'brave'
    if config.get('TAVILY_API_KEY'):
        return 'tavily'
    if config.get('OPENROUTER_API_KEY'):
        return 'openrouter'
    return None


def get_missing_keys(config: Dict[str, Any]) -> str:
    """Determine which sources are missing (accounting for Bird).

    Returns: 'all', 'both', 'reddit', 'x', 'web', or 'none'
    """
    has_openai = bool(config.get('OPENAI_API_KEY')) and config.get('OPENAI_AUTH_STATUS') == AUTH_STATUS_OK
    has_xai = bool(config.get('XAI_API_KEY'))
    has_web = has_web_search_keys(config)

    # Check if Bird provides X access (import here to avoid circular dependency)
    from . import bird_x
    has_bird = bird_x.is_bird_installed() and bird_x.is_bird_authenticated()

    has_x = has_xai or has_bird

    if has_openai and has_x and has_web:
        return 'none'
    elif has_openai and has_x:
        return 'web'  # Missing web search keys
    elif has_openai:
        return 'x'  # Missing X source (and possibly web)
    elif has_x:
        return 'reddit'  # Missing OpenAI key (and possibly web)
    else:
        return 'all'  # Missing everything


def validate_sources(requested: str, available: str, include_web: bool = False) -> tuple[str, Optional[str]]:
    """Validate requested sources against available keys.

    Args:
        requested: 'auto', 'reddit', 'x', 'both', or 'web'
        available: Result from get_available_sources()
        include_web: If True, add WebSearch to available sources

    Returns:
        Tuple of (effective_sources, error_message)
    """
    # No API keys at all
    if available == 'none':
        if requested == 'auto':
            return 'web', "No API keys configured. The assistant can still search the web if it has a search tool."
        elif requested == 'web':
            return 'web', None
        else:
            return 'web', f"No API keys configured. Add keys to ~/.config/last30days/.env for Reddit/X."

    # Web-only mode (only web search API keys)
    if available == 'web':
        if requested == 'auto':
            return 'web', None
        elif requested == 'web':
            return 'web', None
        else:
            return 'web', "Only web search keys configured. Add OPENAI_API_KEY (or run codex login) for Reddit, XAI_API_KEY for X."

    if requested == 'auto':
        # Add web to sources if include_web is set
        if include_web:
            if available == 'both':
                return 'all', None  # reddit + x + web
            elif available == 'reddit':
                return 'reddit-web', None
            elif available == 'x':
                return 'x-web', None
        return available, None

    if requested == 'web':
        return 'web', None

    if requested == 'both':
        if available not in ('both',):
            missing = 'xAI' if available == 'reddit' else 'OpenAI'
            return 'none', f"Requested both sources but {missing} key is missing. Use --sources=auto to use available keys."
        if include_web:
            return 'all', None
        return 'both', None

    if requested == 'reddit':
        if available == 'x':
            return 'none', "Requested Reddit but only xAI key is available."
        if include_web:
            return 'reddit-web', None
        return 'reddit', None

    if requested == 'x':
        if available == 'reddit':
            return 'none', "Requested X but only OpenAI key is available."
        if include_web:
            return 'x-web', None
        return 'x', None

    return requested, None


def get_x_source(config: Dict[str, Any]) -> Optional[str]:
    """Determine the best available X/Twitter source.

    Priority: Bird (free) → xAI (paid API)

    Args:
        config: Configuration dict from get_config()

    Returns:
        'bird' if Bird is installed and authenticated,
        'xai' if XAI_API_KEY is configured,
        None if no X source available.
    """
    # Import here to avoid circular dependency
    from . import bird_x

    # Check Bird first (free option)
    if bird_x.is_bird_installed():
        username = bird_x.is_bird_authenticated()
        if username:
            return 'bird'

    # Fall back to xAI if key exists
    if config.get('XAI_API_KEY'):
        return 'xai'

    return None


def is_ytdlp_available() -> bool:
    """Check if yt-dlp is installed for YouTube search."""
    from . import youtube_yt
    return youtube_yt.is_ytdlp_installed()


def is_hackernews_available() -> bool:
    """Check if Hacker News source is available.

    Always returns True - HN uses free Algolia API, no key needed.
    """
    return True


def is_polymarket_available() -> bool:
    """Check if Polymarket source is available.

    Always returns True - Gamma API is free, no key needed.
    """
    return True


def get_x_source_status(config: Dict[str, Any]) -> Dict[str, Any]:
    """Get detailed X source status for UI decisions.

    Returns:
        Dict with keys: source, bird_installed, bird_authenticated,
        bird_username, xai_available, can_install_bird
    """
    from . import bird_x

    bird_status = bird_x.get_bird_status()
    xai_available = bool(config.get('XAI_API_KEY'))

    # Determine active source
    if bird_status["authenticated"]:
        source = 'bird'
    elif xai_available:
        source = 'xai'
    else:
        source = None

    return {
        "source": source,
        "bird_installed": bird_status["installed"],
        "bird_authenticated": bird_status["authenticated"],
        "bird_username": bird_status["username"],
        "xai_available": xai_available,
        "can_install_bird": bird_status["can_install"],
    }
