from __future__ import annotations

import os
from dataclasses import dataclass, field
from typing import Any, Mapping

from g_gremlin import __version__, dev_mode
from g_gremlin.auth.server_sessions import ServerSessionStore
from g_gremlin.auth.types import (
    AuthAttempt,
    AuthAttemptOutcome,
    AuthResolution,
    AuthSource,
    AuthSurface,
    RefreshStrategy,
)
from g_gremlin.credentials.models import INTEGRATION_REGISTRY, SecretSource
from g_gremlin.credentials.provider import CredentialProvider

DEFAULT_FOUNDRYOPS_API_BASE = "https://api.foundryops.io"

_BRIDGE_WORKSPACE_KEYS: dict[str, tuple[str, ...]] = {
    "apollo": ("workspace_id",),
    "hubspot": ("hubspot_workspace_id", "workspace_id"),
    "zoominfo": ("zoominfo_workspace_id", "workspace_id"),
    "outreach": ("outreach_workspace_id", "workspace_id"),
}

_BRIDGE_API_BASE_KEYS: dict[str, tuple[str, ...]] = {
    "apollo": ("foundryops_api_base", "api_base"),
    "hubspot": ("hubspot_foundryops_api_base", "foundryops_api_base", "api_base"),
    "zoominfo": ("zoominfo_foundryops_api_base", "foundryops_api_base", "api_base"),
    "outreach": ("outreach_foundryops_api_base", "foundryops_api_base", "api_base"),
}

_RUNTIME_CONFIG_KEYS: dict[str, dict[str, tuple[str, ...]]] = {
    "zoominfo": {
        "auth_mode": ("zoominfo_auth_mode", "auth_mode"),
        "access_token": ("zoominfo_access_token", "access_token"),
        "refresh_token": ("zoominfo_refresh_token", "refresh_token"),
        "token_expires_at": ("zoominfo_token_expires_at", "token_expires_at"),
        "client_id": ("zoominfo_client_id", "client_id"),
        "client_secret": ("zoominfo_client_secret", "client_secret"),
    },
    "outreach": {
        "auth_mode": ("outreach_auth_mode", "auth_mode"),
        "access_token": ("outreach_access_token", "access_token"),
        "refresh_token": ("outreach_refresh_token", "refresh_token"),
        "token_expires_at": ("outreach_token_expires_at", "token_expires_at"),
        "client_id": ("outreach_client_id", "client_id"),
        "client_secret": ("outreach_client_secret", "client_secret"),
    },
}

_DEPRECATED_GENERIC_CONFIG_KEYS: dict[str, set[str]] = {
    "zoominfo": {"auth_mode", "workspace_id", "foundryops_api_base"},
    "outreach": {"auth_mode", "workspace_id", "foundryops_api_base"},
}


@dataclass
class _Candidate:
    source: AuthSource
    detail: str
    field: str
    available: bool
    value_hint: str | None = None
    headers: dict[str, str] = field(default_factory=dict)
    credentials: dict[str, str] = field(default_factory=dict)
    workspace_id: str | None = None
    api_base: str | None = None
    mode: str | None = None
    refresh_strategy: RefreshStrategy = RefreshStrategy.NONE


def _normalize_text(value: Any) -> str:
    return str(value or "").strip()


def _map_secret_source(source: SecretSource) -> AuthSource:
    if source == SecretSource.ENV_VAR:
        return AuthSource.ENV
    if source == SecretSource.LEGACY_FILE:
        return AuthSource.LEGACY_FILE
    return AuthSource.PROVIDER


def _config_value(config: Mapping[str, Any] | None, keys: tuple[str, ...]) -> tuple[str | None, str | None]:
    if not config:
        return None, None
    for key in keys:
        value = _normalize_text(config.get(key))
        if value:
            return value, key
    return None, None


def _is_deprecated_generic_config_key(integration: str, key: str | None) -> bool:
    if not key:
        return False
    return key in _DEPRECATED_GENERIC_CONFIG_KEYS.get(integration, set())


def _build_attempts(
    candidates: list[_Candidate],
    selected_index: int | None,
) -> list[AuthAttempt]:
    attempts: list[AuthAttempt] = []
    for index, candidate in enumerate(candidates):
        if not candidate.available:
            outcome = AuthAttemptOutcome.MISSING
        elif selected_index is None:
            outcome = AuthAttemptOutcome.REJECTED
        elif index == selected_index:
            outcome = AuthAttemptOutcome.SELECTED
        else:
            outcome = AuthAttemptOutcome.SHADOWED
        attempts.append(
            AuthAttempt(
                source=candidate.source,
                outcome=outcome,
                detail=candidate.detail,
                field=candidate.field,
                value_hint=candidate.value_hint,
                workspace_id=candidate.workspace_id,
                api_base=candidate.api_base,
            )
        )
    return attempts


def _select_candidate(candidates: list[_Candidate]) -> tuple[_Candidate | None, int | None]:
    for index, candidate in enumerate(candidates):
        if candidate.available:
            return candidate, index
    return None, None


def _base_headers() -> dict[str, str]:
    return {
        "X-Gremlin-CLI-Version": __version__,
        "User-Agent": f"g-gremlin/{__version__}",
    }


def _load_license_token() -> str:
    try:
        from g_gremlin.credentials import license as license_mod

        return _normalize_text(license_mod.get_license_token())
    except Exception:
        return ""


def _foundrymatch_secret(
    credential_provider: CredentialProvider,
    key: str,
    profile: str,
) -> tuple[str, AuthSource, str]:
    secret = credential_provider.get_secret_or_none("foundrymatch", key, profile)
    if secret is None or not _normalize_text(secret.value):
        return "", AuthSource.NONE, f"foundrymatch.{key}"
    detail = f"foundrymatch.{key}"
    env_var = secret.metadata.get("env_var") if isinstance(secret.metadata, dict) else None
    if env_var:
        detail = f"{detail} via {env_var}"
    return _normalize_text(secret.value), _map_secret_source(secret.source), detail


def _resolve_api_base(
    *,
    integration: str,
    api_base: str | None,
    config: Mapping[str, Any] | None,
) -> tuple[str, list[AuthAttempt], str | None]:
    candidates: list[_Candidate] = []
    explicit = _normalize_text(api_base)
    candidates.append(
        _Candidate(
            source=AuthSource.EXPLICIT,
            detail="explicit --api-base",
            field="api_base",
            available=bool(explicit),
            value_hint=explicit or None,
            api_base=explicit or None,
        )
    )
    config_base, config_key = _config_value(config, _BRIDGE_API_BASE_KEYS.get(integration, ("foundryops_api_base", "api_base")))
    candidates.append(
        _Candidate(
            source=AuthSource.CONFIG,
            detail=f"config:{config_key}" if config_key else "config api base",
            field="api_base",
            available=bool(config_base),
            value_hint=config_base or None,
            api_base=config_base or None,
        )
    )
    env_base = _normalize_text(os.getenv("FOUNDRYOPS_API_URL"))
    candidates.append(
        _Candidate(
            source=AuthSource.ENV,
            detail="FOUNDRYOPS_API_URL",
            field="api_base",
            available=bool(env_base),
            value_hint=env_base or None,
            api_base=env_base or None,
        )
    )
    legacy_env_base = _normalize_text(os.getenv("FOUNDRYMATCH_API_URL"))
    candidates.append(
        _Candidate(
            source=AuthSource.ENV,
            detail="FOUNDRYMATCH_API_URL",
            field="api_base",
            available=bool(legacy_env_base),
            value_hint=legacy_env_base or None,
            api_base=legacy_env_base or None,
        )
    )
    default_base = DEFAULT_FOUNDRYOPS_API_BASE
    candidates.append(
        _Candidate(
            source=AuthSource.NONE,
            detail="default api base",
            field="api_base",
            available=True,
            value_hint=default_base,
            api_base=default_base,
        )
    )
    selected, selected_index = _select_candidate(candidates)
    attempts = _build_attempts(candidates, selected_index)
    selected_key: str | None = None
    if selected and selected.source == AuthSource.CONFIG:
        selected_key = selected.detail.removeprefix("config:")
    return _normalize_text(selected.api_base if selected else default_base).rstrip("/"), attempts, selected_key


def resolve_bridge_auth(
    *,
    integration: str,
    auth_token: str | None = None,
    api_key: str | None = None,
    workspace_id: str | None = None,
    api_base: str | None = None,
    profile: str = "default",
    allow_dev: bool = False,
    credential_provider: CredentialProvider | None = None,
    config: Mapping[str, Any] | None = None,
) -> AuthResolution:
    provider = credential_provider or CredentialProvider()
    base_headers = _base_headers()
    foundrymatch_api_key, fm_api_key_source, fm_api_key_detail = _foundrymatch_secret(provider, "api_key", profile)
    foundrymatch_workspace, fm_workspace_source, fm_workspace_detail = _foundrymatch_secret(provider, "workspace_id", profile)
    license_token = _load_license_token()
    dev_enabled = allow_dev or dev_mode.is_dev_mode_enabled()

    auth_candidates = [
        _Candidate(
            source=AuthSource.EXPLICIT,
            detail="explicit bearer token",
            field="auth",
            available=bool(_normalize_text(auth_token)),
            value_hint=_normalize_text(auth_token) or None,
            headers={**base_headers, "Authorization": f"Bearer {_normalize_text(auth_token)}"} if _normalize_text(auth_token) else {},
        ),
        _Candidate(
            source=AuthSource.ENV,
            detail="FOUNDRYMATCH_AUTH_TOKEN",
            field="auth",
            available=bool(_normalize_text(os.getenv("FOUNDRYMATCH_AUTH_TOKEN"))),
            value_hint=_normalize_text(os.getenv("FOUNDRYMATCH_AUTH_TOKEN")) or None,
            headers={**base_headers, "Authorization": f"Bearer {_normalize_text(os.getenv('FOUNDRYMATCH_AUTH_TOKEN'))}"}
            if _normalize_text(os.getenv("FOUNDRYMATCH_AUTH_TOKEN"))
            else {},
        ),
        _Candidate(
            source=AuthSource.EXPLICIT,
            detail="explicit API key",
            field="auth",
            available=bool(_normalize_text(api_key)),
            value_hint=_normalize_text(api_key) or None,
            headers={**base_headers, "X-API-Key": _normalize_text(api_key)} if _normalize_text(api_key) else {},
        ),
        _Candidate(
            source=AuthSource.ENV,
            detail="FOUNDRYMATCH_API_KEY",
            field="auth",
            available=bool(_normalize_text(os.getenv("FOUNDRYMATCH_API_KEY"))),
            value_hint=_normalize_text(os.getenv("FOUNDRYMATCH_API_KEY")) or None,
            headers={**base_headers, "X-API-Key": _normalize_text(os.getenv("FOUNDRYMATCH_API_KEY"))}
            if _normalize_text(os.getenv("FOUNDRYMATCH_API_KEY"))
            else {},
        ),
        _Candidate(
            source=AuthSource.LICENSE_JWT,
            detail="license JWT",
            field="auth",
            available=bool(license_token),
            value_hint=license_token or None,
            headers={**base_headers, "Authorization": f"Bearer {license_token}"} if license_token else {},
        ),
        _Candidate(
            source=fm_api_key_source,
            detail=fm_api_key_detail,
            field="auth",
            available=bool(foundrymatch_api_key),
            value_hint=foundrymatch_api_key or None,
            headers={**base_headers, "X-API-Key": foundrymatch_api_key} if foundrymatch_api_key else {},
        ),
        _Candidate(
            source=AuthSource.DEV_MODE,
            detail="dev mode",
            field="auth",
            available=dev_enabled,
            value_hint="dev-token" if dev_enabled else None,
            headers={**base_headers, "Authorization": "Bearer dev-token"} if dev_enabled else {},
        ),
    ]
    selected_auth, selected_auth_index = _select_candidate(auth_candidates)

    workspace_candidates = [
        _Candidate(
            source=AuthSource.EXPLICIT,
            detail="explicit workspace_id",
            field="workspace_id",
            available=bool(_normalize_text(workspace_id)),
            value_hint=_normalize_text(workspace_id) or None,
            workspace_id=_normalize_text(workspace_id) or None,
        )
    ]
    config_workspace, config_workspace_key = _config_value(
        config,
        _BRIDGE_WORKSPACE_KEYS.get(integration, ("workspace_id",)),
    )
    workspace_candidates.append(
        _Candidate(
            source=AuthSource.CONFIG,
            detail=f"config:{config_workspace_key}" if config_workspace_key else "config workspace_id",
            field="workspace_id",
            available=bool(config_workspace),
            value_hint=config_workspace or None,
            workspace_id=config_workspace or None,
        )
    )
    workspace_candidates.append(
        _Candidate(
            source=fm_workspace_source,
            detail=fm_workspace_detail,
            field="workspace_id",
            available=bool(foundrymatch_workspace),
            value_hint=foundrymatch_workspace or None,
            workspace_id=foundrymatch_workspace or None,
        )
    )
    selected_workspace, selected_workspace_index = _select_candidate(workspace_candidates)

    resolved_api_base, api_base_attempts, selected_api_base_key = _resolve_api_base(
        integration=integration,
        api_base=api_base,
        config=config,
    )

    attempts = []
    attempts.extend(_build_attempts(auth_candidates, selected_auth_index))
    attempts.extend(_build_attempts(workspace_candidates, selected_workspace_index))
    attempts.extend(api_base_attempts)

    warnings: list[str] = []
    if any(attempt.outcome == AuthAttemptOutcome.SHADOWED for attempt in attempts):
        warnings.append("shadowed")
    if selected_auth and selected_workspace and selected_workspace.source != selected_auth.source:
        warnings.append("workspace_separate_from_auth")
    if not selected_auth:
        warnings.append("missing_auth")
    if _is_deprecated_generic_config_key(integration, config_workspace_key):
        warnings.append(f"deprecated_config_key:{config_workspace_key}")
    if _is_deprecated_generic_config_key(integration, selected_api_base_key):
        warnings.append(f"deprecated_config_key:{selected_api_base_key}")

    return AuthResolution(
        surface=AuthSurface.BRIDGE,
        integration=integration,
        selected_source=selected_auth.source if selected_auth else AuthSource.NONE,
        attempts=attempts,
        headers=dict(selected_auth.headers) if selected_auth else {},
        workspace_id=selected_workspace.workspace_id if selected_workspace else None,
        api_base=resolved_api_base,
        warnings=warnings,
    )


def _runtime_refresh_strategy(mode: str | None, credentials: Mapping[str, Any]) -> RefreshStrategy:
    if _normalize_text(mode).lower() == "server":
        return RefreshStrategy.SERVER_REFRESH
    if _normalize_text(credentials.get("refresh_token")):
        return RefreshStrategy.LOCAL_REFRESH
    return RefreshStrategy.NONE


def _runtime_config_credentials(
    integration: str,
    config: Mapping[str, Any] | None,
) -> tuple[dict[str, str], dict[str, str]]:
    keys = _RUNTIME_CONFIG_KEYS.get(integration, {})
    resolved: dict[str, str] = {}
    selected_keys: dict[str, str] = {}
    for logical_key, config_keys in keys.items():
        value, selected_key = _config_value(config, config_keys)
        if value:
            resolved[logical_key] = value
            if selected_key:
                selected_keys[logical_key] = selected_key
    return resolved, selected_keys


def _runtime_explicit_available(
    explicit_values: Mapping[str, str],
    required_keys: list[str],
) -> bool:
    explicit_mode = _normalize_text(explicit_values.get("auth_mode")).lower()
    if explicit_mode == "server":
        return bool(_normalize_text(explicit_values.get("access_token")))
    return all(explicit_values.get(key) for key in required_keys)


def resolve_runtime_auth(
    *,
    integration: str,
    explicit_credentials: Mapping[str, str | None] | None = None,
    profile: str = "default",
    credential_provider: CredentialProvider | None = None,
    config: Mapping[str, Any] | None = None,
) -> AuthResolution:
    provider = credential_provider or CredentialProvider()
    explicit_credentials = explicit_credentials or {}
    spec = INTEGRATION_REGISTRY.get(integration)
    if spec is None:
        raise ValueError(f"Unknown integration: {integration}")

    explicit_values = {
        key: _normalize_text(value)
        for key, value in explicit_credentials.items()
        if _normalize_text(value)
    }
    explicit_available = _runtime_explicit_available(explicit_values, spec.required_keys)
    explicit_mode = _normalize_text(explicit_values.get("auth_mode")) or "local"
    explicit_candidate = _Candidate(
        source=AuthSource.EXPLICIT,
        detail="explicit runtime credentials",
        field="auth",
        available=explicit_available,
        credentials=explicit_values if explicit_available else {},
        mode=explicit_mode,
        refresh_strategy=_runtime_refresh_strategy(explicit_mode, explicit_values),
    )

    session_payload = ServerSessionStore().load_session(integration) or {}
    session_access_token = _normalize_text(session_payload.get("access_token"))
    session_candidate = _Candidate(
        source=AuthSource.SERVER_SESSION,
        detail="server session store",
        field="auth",
        available=bool(session_access_token),
        credentials={k: str(v) for k, v in session_payload.items() if v not in (None, "")},
        mode="server" if session_access_token else None,
        refresh_strategy=RefreshStrategy.SERVER_REFRESH if session_access_token else RefreshStrategy.NONE,
    )

    config_credentials, selected_config_keys = _runtime_config_credentials(integration, config)
    config_mode = _normalize_text(config_credentials.get("auth_mode")) or "local"
    config_server_available = config_mode.lower() == "server" and bool(
        _normalize_text(config_credentials.get("access_token"))
    )
    config_server_candidate = _Candidate(
        source=AuthSource.CONFIG,
        detail="config server session",
        field="auth",
        available=config_server_available,
        credentials=config_credentials if config_server_available else {},
        mode="server" if config_server_available else None,
        refresh_strategy=RefreshStrategy.SERVER_REFRESH if config_server_available else RefreshStrategy.NONE,
    )

    provider_credentials: dict[str, str] = {}
    provider_sources: list[AuthSource] = []
    for key in spec.required_keys + spec.optional_keys:
        secret = provider.get_secret_or_none(integration, key, profile)
        if secret is None or not _normalize_text(secret.value):
            continue
        provider_credentials[key] = _normalize_text(secret.value)
        provider_sources.append(_map_secret_source(secret.source))
    provider_available = all(provider_credentials.get(key) for key in spec.required_keys)
    provider_source = provider_sources[0] if provider_sources else AuthSource.PROVIDER
    provider_mode = _normalize_text(provider_credentials.get("auth_mode")) or "local"
    provider_candidate = _Candidate(
        source=provider_source,
        detail="credential provider",
        field="auth",
        available=provider_available,
        credentials=provider_credentials if provider_available else {},
        mode=provider_mode if provider_available else None,
        refresh_strategy=_runtime_refresh_strategy(provider_mode, provider_credentials),
    )

    legacy_credentials = dict(config_credentials)
    legacy_mode = _normalize_text(legacy_credentials.get("auth_mode")) or "local"
    legacy_available = all(legacy_credentials.get(key) for key in spec.required_keys)
    legacy_candidate = _Candidate(
        source=AuthSource.LEGACY_FILE,
        detail="legacy config",
        field="auth",
        available=legacy_available,
        credentials=legacy_credentials if legacy_available else {},
        mode=legacy_mode if legacy_available else None,
        refresh_strategy=_runtime_refresh_strategy(legacy_mode, legacy_credentials),
    )

    candidates = [
        explicit_candidate,
        session_candidate,
        config_server_candidate,
        provider_candidate,
        legacy_candidate,
    ]
    selected, selected_index = _select_candidate(candidates)
    attempts = _build_attempts(candidates, selected_index)
    warnings: list[str] = []
    if any(attempt.outcome == AuthAttemptOutcome.SHADOWED for attempt in attempts):
        warnings.append("shadowed")
    if selected and selected.source == AuthSource.LEGACY_FILE:
        warnings.append("deprecated_source")
    if selected and selected.source == AuthSource.CONFIG:
        warnings.append("config_server_session")
    if provider_available and len(set(provider_sources)) > 1:
        warnings.append("mixed_provider_sources")
    for selected_key in selected_config_keys.values():
        if _is_deprecated_generic_config_key(integration, selected_key):
            warning = f"deprecated_config_key:{selected_key}"
            if warning not in warnings:
                warnings.append(warning)

    credentials = dict(selected.credentials) if selected else {}
    workspace = _normalize_text(credentials.get("workspace_id")) or None
    api_base_value = _normalize_text(credentials.get("api_base")) or None
    return AuthResolution(
        surface=AuthSurface.RUNTIME,
        integration=integration,
        selected_source=selected.source if selected else AuthSource.NONE,
        attempts=attempts,
        credentials=credentials,
        workspace_id=workspace,
        api_base=api_base_value,
        mode=selected.mode if selected else None,
        refresh_strategy=selected.refresh_strategy if selected else RefreshStrategy.NONE,
        warnings=warnings,
    )
