from __future__ import annotations

from datetime import datetime, timedelta, timezone
import json
import logging
import re
from typing import Any, Dict, Optional
from urllib.parse import urlencode

import httpx
from sqlalchemy.ext.asyncio import AsyncSession

from .. import settings
from .secret_storage import get_saas_secret_backend

logger = logging.getLogger(__name__)

OUTREACH_AUTHORIZE_URL = "https://api.outreach.io/oauth/authorize"
OUTREACH_TOKEN_URL = "https://api.outreach.io/oauth/token"
OUTREACH_ME_URL = "https://api.outreach.io/api/v2/users/me"
OUTREACH_API_ROOT_URL = "https://api.outreach.io/api/v2"
OUTREACH_INTEGRATION_KEY = "outreach"
OUTREACH_ACCESS_TOKEN_KEY = "access_token"
OUTREACH_REFRESH_TOKEN_KEY = "refresh_token"
OUTREACH_METADATA_KEY = "oauth_metadata"
TOKEN_REFRESH_SKEW_SECONDS = 300
DEFAULT_EXPIRES_IN_SECONDS = 7200

SCOPE_PROFILES: Dict[str, list[str]] = {
    "default": [
        "prospects.read",
        "prospects.write",
        "sequences.read",
        "sequenceStates.read",
        "sequenceStates.write",
        "users.read",
        "mailboxes.read",
    ],
    "read_only": [
        "prospects.read",
        "sequences.read",
        "sequenceStates.read",
        "users.read",
        "mailboxes.read",
        "accounts.read",
        "tasks.read",
    ],
    "admin": [
        "prospects.all",
        "sequences.all",
        "sequenceStates.all",
        "users.read",
        "mailboxes.all",
        "accounts.all",
        "tasks.all",
        "webhooks.all",
        "roles.read",
        "teams.read",
    ],
}


class OutreachOAuthError(RuntimeError):
    def __init__(
        self,
        message: str,
        *,
        status_code: int = 500,
        code: str = "outreach_oauth_error",
        retryable: bool = False,
    ) -> None:
        super().__init__(message)
        self.status_code = status_code
        self.code = code
        self.retryable = retryable


_SENSITIVE_TOKEN_PATTERNS = (
    re.compile(r"(access_token=)([^&\s]+)", re.IGNORECASE),
    re.compile(r"(refresh_token=)([^&\s]+)", re.IGNORECASE),
    re.compile(r"(authorization:\s*bearer\s+)([^\s]+)", re.IGNORECASE),
)


def sanitize_outreach_error_text(message: str) -> str:
    sanitized = message or ""
    for pattern in _SENSITIVE_TOKEN_PATTERNS:
        sanitized = pattern.sub(r"\1[REDACTED]", sanitized)
    return sanitized


def resolve_scope_profile(scope_profile: str | None) -> str:
    value = (scope_profile or "default").strip().lower()
    if value in SCOPE_PROFILES:
        return value
    return "default"


def scopes_for_profile(scope_profile: str | None) -> list[str]:
    return list(SCOPE_PROFILES[resolve_scope_profile(scope_profile)])


def _now_utc() -> datetime:
    return datetime.now(timezone.utc)


def _clean_iso(value: str | None) -> Optional[datetime]:
    if not value:
        return None
    try:
        parsed = datetime.fromisoformat(value)
    except ValueError:
        return None
    if parsed.tzinfo is None:
        return parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def build_outreach_auth_url(
    *,
    state: str,
    redirect_uri: str,
    scope_profile: str = "default",
) -> str:
    params = {
        "client_id": settings.OUTREACH_CLIENT_ID,
        "redirect_uri": redirect_uri,
        "response_type": "code",
        "scope": " ".join(scopes_for_profile(scope_profile)),
        "state": state,
    }
    return f"{OUTREACH_AUTHORIZE_URL}?{urlencode(params)}"


def _classify_http_exception(exc: Exception) -> OutreachOAuthError:
    message = sanitize_outreach_error_text(str(exc))
    lower = message.lower()
    if isinstance(exc, httpx.ConnectError) or "failed to resolve" in lower or "getaddrinfo" in lower:
        return OutreachOAuthError(
            "Network/DNS to Outreach is unavailable from this environment.",
            status_code=503,
            code="outreach_network_unavailable",
            retryable=True,
        )
    if isinstance(exc, httpx.ReadTimeout):
        return OutreachOAuthError(
            "Outreach request timed out.",
            status_code=504,
            code="outreach_timeout",
            retryable=True,
        )
    return OutreachOAuthError(
        "Outreach API request failed.",
        status_code=502,
        code="outreach_api_unreachable",
        retryable=True,
    )


def _sanitize_outreach_error(status_code: int, payload: Dict[str, Any]) -> OutreachOAuthError:
    error_value = str(payload.get("error") or payload.get("code") or "").strip().lower()
    error_description = sanitize_outreach_error_text(
        str(payload.get("error_description") or payload.get("message") or "").strip()
    )
    if error_value in {"invalid_client", "invalid_grant", "unauthorized_client"}:
        return OutreachOAuthError(
            error_description or "Outreach OAuth configuration is invalid.",
            status_code=401,
            code=error_value or "outreach_auth_error",
            retryable=False,
        )
    if status_code in (401, 403):
        return OutreachOAuthError(
            error_description or "Outreach authorization failed.",
            status_code=401,
            code="outreach_unauthorized",
            retryable=False,
        )
    if status_code >= 500:
        return OutreachOAuthError(
            "Outreach service error.",
            status_code=502,
            code="outreach_upstream_error",
            retryable=True,
        )
    return OutreachOAuthError(
        error_description or "Outreach OAuth request failed.",
        status_code=400,
        code="outreach_oauth_failed",
        retryable=False,
    )


async def _post_token(payload: Dict[str, Any]) -> Dict[str, Any]:
    timeout = httpx.Timeout(30.0)
    try:
        async with httpx.AsyncClient(timeout=timeout) as client:
            response = await client.post(
                OUTREACH_TOKEN_URL,
                data=payload,
                headers={
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Accept": "application/json",
                },
            )
    except Exception as exc:  # pragma: no cover - network edge cases
        raise _classify_http_exception(exc) from exc

    response_payload: Dict[str, Any]
    try:
        response_payload = response.json()
    except ValueError:
        response_payload = {}

    if not response.is_success:
        raise _sanitize_outreach_error(response.status_code, response_payload)

    if not response_payload.get("access_token"):
        raise OutreachOAuthError(
            "Outreach response missing access_token.",
            status_code=502,
            code="outreach_missing_access_token",
            retryable=False,
        )
    return response_payload


async def exchange_code_for_tokens(
    *,
    code: str,
    redirect_uri: str,
) -> Dict[str, Any]:
    if not settings.OUTREACH_CLIENT_ID or not settings.OUTREACH_CLIENT_SECRET:
        raise OutreachOAuthError(
            "Outreach OAuth is not configured on the server.",
            status_code=503,
            code="outreach_oauth_not_configured",
            retryable=False,
        )

    payload = {
        "grant_type": "authorization_code",
        "client_id": settings.OUTREACH_CLIENT_ID,
        "client_secret": settings.OUTREACH_CLIENT_SECRET,
        "code": code,
        "redirect_uri": redirect_uri,
    }
    return await _post_token(payload)


async def refresh_access_token(refresh_token: str) -> Dict[str, Any]:
    if not settings.OUTREACH_CLIENT_ID or not settings.OUTREACH_CLIENT_SECRET:
        raise OutreachOAuthError(
            "Outreach OAuth is not configured on the server.",
            status_code=503,
            code="outreach_oauth_not_configured",
            retryable=False,
        )

    payload = {
        "grant_type": "refresh_token",
        "client_id": settings.OUTREACH_CLIENT_ID,
        "client_secret": settings.OUTREACH_CLIENT_SECRET,
        "refresh_token": refresh_token,
    }
    return await _post_token(payload)


def _extract_identity(payload: Dict[str, Any]) -> Dict[str, Optional[str]]:
    data = payload.get("data") if isinstance(payload, dict) else {}
    if not isinstance(data, dict):
        data = {}
    attrs = data.get("attributes") if isinstance(data.get("attributes"), dict) else {}
    rels = data.get("relationships") if isinstance(data.get("relationships"), dict) else {}

    user_id = data.get("id")
    email = attrs.get("email")
    if not email:
        emails = attrs.get("emails") if isinstance(attrs.get("emails"), list) else []
        if emails:
            email = emails[0]

    name = attrs.get("name")
    if not name:
        first = attrs.get("firstName") or attrs.get("first_name")
        last = attrs.get("lastName") or attrs.get("last_name")
        combined = " ".join([part for part in [first, last] if part])
        name = combined or None

    org_id: Optional[str] = None
    org_rel = rels.get("organization") or rels.get("org") or {}
    if isinstance(org_rel, dict):
        rel_data = org_rel.get("data") or {}
        if isinstance(rel_data, dict) and rel_data.get("id") is not None:
            org_id = str(rel_data.get("id"))

    if not org_id and attrs.get("organizationId") is not None:
        org_id = str(attrs.get("organizationId"))
    if not org_id and attrs.get("orgId") is not None:
        org_id = str(attrs.get("orgId"))

    org_name: Optional[str] = None
    included = payload.get("included") if isinstance(payload.get("included"), list) else []
    for item in included:
        if not isinstance(item, dict):
            continue
        item_type = str(item.get("type") or "")
        item_id = item.get("id")
        if item_type not in ("organization", "org"):
            continue
        if org_id and str(item_id) != org_id:
            continue
        item_attrs = item.get("attributes") if isinstance(item.get("attributes"), dict) else {}
        if item_attrs.get("name"):
            org_name = str(item_attrs.get("name"))
        if not org_id and item_id is not None:
            org_id = str(item_id)
        if org_name:
            break

    meta = payload.get("meta") if isinstance(payload.get("meta"), dict) else {}
    meta_org = meta.get("org") if isinstance(meta.get("org"), dict) else {}
    if not org_id and meta_org.get("id") is not None:
        org_id = str(meta_org.get("id"))
    if not org_name and meta_org.get("name") is not None:
        org_name = str(meta_org.get("name"))

    return {
        "user_id": str(user_id) if user_id is not None else None,
        "email": str(email) if email else None,
        "name": str(name) if name else None,
        "org_id": str(org_id) if org_id else None,
        "org_name": str(org_name) if org_name else None,
    }


def _extract_identity_from_root(payload: Dict[str, Any]) -> Dict[str, Optional[str]]:
    meta = payload.get("meta") if isinstance(payload, dict) else {}
    if not isinstance(meta, dict):
        meta = {}
    user = meta.get("user") if isinstance(meta.get("user"), dict) else {}
    org = meta.get("org") if isinstance(meta.get("org"), dict) else {}

    user_id = user.get("id") or user.get("userId")
    email = user.get("email")
    name = user.get("name")
    org_id = org.get("id")
    org_name = org.get("name")

    return {
        "user_id": str(user_id) if user_id is not None else None,
        "email": str(email) if email else None,
        "name": str(name) if name else None,
        "org_id": str(org_id) if org_id is not None else None,
        "org_name": str(org_name) if org_name else None,
    }


async def fetch_outreach_identity(access_token: str) -> Dict[str, Optional[str]]:
    timeout = httpx.Timeout(20.0)
    try:
        async with httpx.AsyncClient(timeout=timeout) as client:
            headers = {
                "Authorization": f"Bearer {access_token}",
                "Accept": "application/vnd.api+json",
            }
            response = await client.get(
                OUTREACH_ME_URL,
                headers=headers,
            )
            if response.status_code in (404, 405):
                root_response = await client.get(
                    OUTREACH_API_ROOT_URL,
                    headers=headers,
                )
            else:
                root_response = None
    except Exception as exc:  # pragma: no cover - network edge cases
        raise _classify_http_exception(exc) from exc

    if not response.is_success:
        payload: Dict[str, Any]
        try:
            payload = response.json()
        except ValueError:
            payload = {}
        if root_response is not None:
            if not root_response.is_success:
                root_payload: Dict[str, Any]
                try:
                    root_payload = root_response.json()
                except ValueError:
                    root_payload = {}
                raise _sanitize_outreach_error(root_response.status_code, root_payload)
            try:
                root_parsed = root_response.json()
            except ValueError as exc:  # pragma: no cover - unlikely
                raise OutreachOAuthError(
                    "Outreach identity fallback response was invalid JSON.",
                    status_code=502,
                    code="outreach_invalid_identity_payload",
                    retryable=False,
                ) from exc
            return _extract_identity_from_root(root_parsed if isinstance(root_parsed, dict) else {})
        raise _sanitize_outreach_error(response.status_code, payload)

    try:
        parsed = response.json()
    except ValueError as exc:  # pragma: no cover - unlikely
        raise OutreachOAuthError(
            "Outreach identity response was invalid JSON.",
            status_code=502,
            code="outreach_invalid_identity_payload",
            retryable=False,
        ) from exc

    return _extract_identity(parsed if isinstance(parsed, dict) else {})


def _build_metadata(tokens: Dict[str, Any], previous: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
    now = _now_utc()
    expires_in_raw = tokens.get("expires_in")
    try:
        expires_in = int(expires_in_raw) if expires_in_raw is not None else DEFAULT_EXPIRES_IN_SECONDS
    except (TypeError, ValueError):
        expires_in = DEFAULT_EXPIRES_IN_SECONDS

    metadata = dict(previous or {})
    metadata.update(
        {
            "token_type": tokens.get("token_type"),
            "scope": tokens.get("scope"),
            "expires_in": expires_in,
            "expires_at": (now + timedelta(seconds=expires_in)).isoformat(),
            "updated_at": now.isoformat(),
            "last_refresh_status": "ok",
            "last_error_code": None,
            "last_error_message": None,
            "last_error_at": None,
        }
    )
    return metadata


async def store_outreach_tokens(
    db: AsyncSession,
    *,
    account_id: str,
    tokens: Dict[str, Any],
    created_by: Optional[str] = None,
    metadata_overrides: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    secret_backend = get_saas_secret_backend()
    previous_metadata = await get_outreach_metadata(db, account_id)
    metadata = _build_metadata(tokens, previous=previous_metadata)
    if metadata_overrides:
        metadata.update(metadata_overrides)

    access_token = str(tokens.get("access_token") or "")
    refresh_token = str(tokens.get("refresh_token") or "")
    if not access_token:
        raise OutreachOAuthError(
            "Outreach token response missing access token.",
            status_code=502,
            code="outreach_missing_access_token",
            retryable=False,
        )

    await secret_backend.set_secret(
        db=db,
        integration=OUTREACH_INTEGRATION_KEY,
        key=OUTREACH_ACCESS_TOKEN_KEY,
        value=access_token,
        workspace_id=account_id,
        created_by=created_by,
        validated_at=_now_utc(),
    )

    if refresh_token:
        await secret_backend.set_secret(
            db=db,
            integration=OUTREACH_INTEGRATION_KEY,
            key=OUTREACH_REFRESH_TOKEN_KEY,
            value=refresh_token,
            workspace_id=account_id,
            created_by=created_by,
        )

    await secret_backend.set_secret(
        db=db,
        integration=OUTREACH_INTEGRATION_KEY,
        key=OUTREACH_METADATA_KEY,
        value=json.dumps(metadata, separators=(",", ":"), sort_keys=True),
        workspace_id=account_id,
        created_by=created_by,
        validated_at=_now_utc(),
    )

    return metadata


async def get_outreach_metadata(db: AsyncSession, account_id: str) -> Dict[str, Any]:
    secret_backend = get_saas_secret_backend()
    metadata_secret = await secret_backend.get_secret(
        db=db,
        integration=OUTREACH_INTEGRATION_KEY,
        key=OUTREACH_METADATA_KEY,
        workspace_id=account_id,
    )
    if not metadata_secret:
        return {}
    try:
        loaded = json.loads(metadata_secret.value)
        return loaded if isinstance(loaded, dict) else {}
    except json.JSONDecodeError:
        return {}


async def get_outreach_secret(
    db: AsyncSession,
    account_id: str,
    key: str,
) -> Optional[str]:
    secret_backend = get_saas_secret_backend()
    secret = await secret_backend.get_secret(
        db=db,
        integration=OUTREACH_INTEGRATION_KEY,
        key=key,
        workspace_id=account_id,
    )
    if not secret:
        return None
    return secret.value


async def refresh_if_needed(
    db: AsyncSession,
    *,
    account_id: str,
    force: bool = False,
    created_by: Optional[str] = "system",
) -> Dict[str, Any]:
    metadata = await get_outreach_metadata(db, account_id)
    access_token = await get_outreach_secret(db, account_id, OUTREACH_ACCESS_TOKEN_KEY)
    refresh_token = await get_outreach_secret(db, account_id, OUTREACH_REFRESH_TOKEN_KEY)

    if not access_token:
        raise OutreachOAuthError(
            "Outreach is not connected.",
            status_code=404,
            code="outreach_not_connected",
            retryable=False,
        )
    if not refresh_token:
        raise OutreachOAuthError(
            "Outreach refresh token is missing. Reconnect required.",
            status_code=401,
            code="outreach_refresh_missing",
            retryable=False,
        )

    expires_at = _clean_iso(str(metadata.get("expires_at") or ""))
    needs_refresh = force
    if not needs_refresh:
        if expires_at is None:
            needs_refresh = True
        else:
            needs_refresh = expires_at <= (_now_utc() + timedelta(seconds=TOKEN_REFRESH_SKEW_SECONDS))

    if not needs_refresh:
        return metadata

    tokens = await refresh_access_token(refresh_token)
    return await store_outreach_tokens(
        db,
        account_id=account_id,
        tokens=tokens,
        created_by=created_by,
        metadata_overrides={"refreshed_at": _now_utc().isoformat()},
    )


async def disconnect_outreach(db: AsyncSession, account_id: str) -> None:
    secret_backend = get_saas_secret_backend()
    for key in (OUTREACH_ACCESS_TOKEN_KEY, OUTREACH_REFRESH_TOKEN_KEY, OUTREACH_METADATA_KEY):
        await secret_backend.delete_secret(
            db=db,
            integration=OUTREACH_INTEGRATION_KEY,
            key=key,
            workspace_id=account_id,
        )


async def get_outreach_status(db: AsyncSession, account_id: str) -> Dict[str, Any]:
    access_token = await get_outreach_secret(db, account_id, OUTREACH_ACCESS_TOKEN_KEY)
    if not access_token:
        return {"connected": False, "status": "disconnected"}

    metadata = await get_outreach_metadata(db, account_id)
    expires_at = _clean_iso(str(metadata.get("expires_at") or ""))
    expires_in_seconds: Optional[int] = None
    needs_refresh = False
    if expires_at is not None:
        expires_in_seconds = int((expires_at - _now_utc()).total_seconds())
        needs_refresh = expires_in_seconds <= TOKEN_REFRESH_SKEW_SECONDS

    return {
        "connected": True,
        "status": "connected",
        "needs_refresh": needs_refresh,
        "expires_at": expires_at.isoformat() if expires_at else None,
        "expires_in_seconds": expires_in_seconds,
        "scope": metadata.get("scope"),
        "scope_profile": metadata.get("scope_profile") or "default",
        "org_id": metadata.get("org_id"),
        "org_name": metadata.get("org_name"),
        "user_id": metadata.get("user_id"),
        "email": metadata.get("email"),
        "name": metadata.get("name"),
        "last_refresh_status": metadata.get("last_refresh_status"),
        "last_error_code": metadata.get("last_error_code"),
        "last_error_message": metadata.get("last_error_message"),
    }
