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__)

HUBSPOT_AUTHORIZE_URL = "https://app.hubspot.com/oauth/authorize"
HUBSPOT_TOKEN_URL = "https://api.hubapi.com/oauth/v1/token"
HUBSPOT_REVOKE_URL = "https://api.hubapi.com/oauth/v1/token/revoke"
HUBSPOT_TOKEN_INFO_URL = "https://api.hubapi.com/oauth/v1/access-tokens/{token}"
HUBSPOT_INTEGRATION_KEY = "hubspot"
HUBSPOT_ACCESS_TOKEN_KEY = "access_token"
HUBSPOT_REFRESH_TOKEN_KEY = "refresh_token"
HUBSPOT_METADATA_KEY = "oauth_metadata"
TOKEN_REFRESH_SKEW_SECONDS = 300


class HubSpotOAuthError(RuntimeError):
    def __init__(
        self,
        message: str,
        *,
        status_code: int = 500,
        code: str = "hubspot_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),
    re.compile(r"(pat-[a-z0-9-]*)([a-z0-9]{6,})", re.IGNORECASE),
)


def sanitize_hubspot_error_text(message: str) -> str:
    """Redact likely token values from provider/network error text."""
    sanitized = message or ""
    for pattern in _SENSITIVE_TOKEN_PATTERNS:
        sanitized = pattern.sub(r"\1[REDACTED]", sanitized)
    return sanitized


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 parse_hubspot_scopes() -> list[str]:
    raw = settings.HUBSPOT_OAUTH_SCOPES or ""
    scopes = [scope.strip() for scope in raw.split(",") if scope.strip()]
    return scopes


def build_hubspot_auth_url(
    *,
    state: str,
    redirect_uri: str,
    code_challenge: str | None = None,
) -> str:
    params = {
        "client_id": settings.HUBSPOT_CLIENT_ID,
        "redirect_uri": redirect_uri,
        "scope": " ".join(parse_hubspot_scopes()),
        "state": state,
    }
    if code_challenge:
        params["code_challenge"] = code_challenge
        params["code_challenge_method"] = "S256"
    return f"{HUBSPOT_AUTHORIZE_URL}?{urlencode(params)}"


def _classify_http_exception(exc: Exception) -> HubSpotOAuthError:
    message = sanitize_hubspot_error_text(str(exc))
    lower = message.lower()
    if isinstance(exc, httpx.ConnectError) or "failed to resolve" in lower or "getaddrinfo" in lower:
        return HubSpotOAuthError(
            "Network/DNS to HubSpot is unavailable from this environment.",
            status_code=503,
            code="hubspot_network_unavailable",
            retryable=True,
        )
    if isinstance(exc, httpx.ReadTimeout):
        return HubSpotOAuthError(
            "HubSpot request timed out.",
            status_code=504,
            code="hubspot_timeout",
            retryable=True,
        )
    return HubSpotOAuthError(
        "HubSpot API request failed.",
        status_code=502,
        code="hubspot_api_unreachable",
        retryable=True,
    )


def _sanitize_hubspot_error(status_code: int, payload: Dict[str, Any]) -> HubSpotOAuthError:
    error_value = str(payload.get("error") or "").strip().lower()
    error_description = sanitize_hubspot_error_text(
        str(payload.get("error_description") or payload.get("message") or "").strip()
    )
    if error_value in {"invalid_client", "invalid_grant", "unauthorized_client"}:
        return HubSpotOAuthError(
            error_description or "HubSpot OAuth configuration is invalid.",
            status_code=401,
            code=error_value or "hubspot_auth_error",
            retryable=False,
        )
    if status_code in (401, 403):
        return HubSpotOAuthError(
            error_description or "HubSpot authorization failed.",
            status_code=401,
            code="hubspot_unauthorized",
            retryable=False,
        )
    if status_code >= 500:
        return HubSpotOAuthError(
            "HubSpot service error.",
            status_code=502,
            code="hubspot_upstream_error",
            retryable=True,
        )
    return HubSpotOAuthError(
        error_description or "HubSpot OAuth request failed.",
        status_code=400,
        code="hubspot_oauth_failed",
        retryable=False,
    )


async def _post_token(data: Dict[str, str]) -> Dict[str, Any]:
    timeout = httpx.Timeout(30.0)
    try:
        async with httpx.AsyncClient(timeout=timeout) as client:
            response = await client.post(HUBSPOT_TOKEN_URL, data=data)
    except Exception as exc:  # pragma: no cover - network edge cases
        raise _classify_http_exception(exc) from exc

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

    if not response.is_success:
        raise _sanitize_hubspot_error(response.status_code, payload)

    if not payload.get("access_token"):
        raise HubSpotOAuthError(
            "HubSpot response missing access_token.",
            status_code=502,
            code="hubspot_missing_access_token",
            retryable=False,
        )
    return payload


async def exchange_code_for_tokens(
    *,
    code: str,
    redirect_uri: str,
    code_verifier: str | None = None,
) -> Dict[str, Any]:
    if not settings.HUBSPOT_CLIENT_ID or not settings.HUBSPOT_CLIENT_SECRET:
        raise HubSpotOAuthError(
            "HubSpot OAuth is not configured on the server.",
            status_code=503,
            code="hubspot_oauth_not_configured",
            retryable=False,
        )
    payload = {
        "grant_type": "authorization_code",
        "client_id": settings.HUBSPOT_CLIENT_ID,
        "client_secret": settings.HUBSPOT_CLIENT_SECRET,
        "redirect_uri": redirect_uri,
        "code": code,
    }
    if code_verifier:
        payload["code_verifier"] = code_verifier
    return await _post_token(payload)


async def refresh_access_token(refresh_token: str) -> Dict[str, Any]:
    if not settings.HUBSPOT_CLIENT_ID or not settings.HUBSPOT_CLIENT_SECRET:
        raise HubSpotOAuthError(
            "HubSpot OAuth is not configured on the server.",
            status_code=503,
            code="hubspot_oauth_not_configured",
            retryable=False,
        )
    return await _post_token(
        {
            "grant_type": "refresh_token",
            "client_id": settings.HUBSPOT_CLIENT_ID,
            "client_secret": settings.HUBSPOT_CLIENT_SECRET,
            "refresh_token": refresh_token,
        }
    )


async def revoke_token(token: str) -> None:
    timeout = httpx.Timeout(20.0)
    try:
        async with httpx.AsyncClient(timeout=timeout) as client:
            await client.post(HUBSPOT_REVOKE_URL, data={"token": token})
    except Exception as exc:  # pragma: no cover - best effort
        logger.warning("HubSpot token revoke failed: %s", exc)


async def fetch_token_identity(access_token: str) -> Dict[str, Any]:
    timeout = httpx.Timeout(20.0)
    url = HUBSPOT_TOKEN_INFO_URL.format(token=access_token)
    try:
        async with httpx.AsyncClient(timeout=timeout) as client:
            response = await client.get(url)
    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 = {}
        raise _sanitize_hubspot_error(response.status_code, payload)

    try:
        return response.json()
    except ValueError as exc:  # pragma: no cover - unlikely
        raise HubSpotOAuthError(
            "HubSpot identity response was invalid JSON.",
            status_code=502,
            code="hubspot_invalid_identity_payload",
            retryable=False,
        ) from exc


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 21600
    except (TypeError, ValueError):
        expires_in = 21600
    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",
        }
    )
    return metadata


async def store_hubspot_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_hubspot_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 HubSpotOAuthError(
            "HubSpot token response missing access token.",
            status_code=502,
            code="hubspot_missing_access_token",
            retryable=False,
        )

    await secret_backend.set_secret(
        db=db,
        integration=HUBSPOT_INTEGRATION_KEY,
        key=HUBSPOT_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=HUBSPOT_INTEGRATION_KEY,
            key=HUBSPOT_REFRESH_TOKEN_KEY,
            value=refresh_token,
            workspace_id=account_id,
            created_by=created_by,
        )

    await secret_backend.set_secret(
        db=db,
        integration=HUBSPOT_INTEGRATION_KEY,
        key=HUBSPOT_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_hubspot_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=HUBSPOT_INTEGRATION_KEY,
        key=HUBSPOT_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_hubspot_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=HUBSPOT_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_hubspot_metadata(db, account_id)
    access_token = await get_hubspot_secret(db, account_id, HUBSPOT_ACCESS_TOKEN_KEY)
    refresh_token = await get_hubspot_secret(db, account_id, HUBSPOT_REFRESH_TOKEN_KEY)

    if not access_token:
        raise HubSpotOAuthError(
            "HubSpot is not connected.",
            status_code=404,
            code="hubspot_not_connected",
            retryable=False,
        )
    if not refresh_token:
        raise HubSpotOAuthError(
            "HubSpot refresh token is missing. Reconnect required.",
            status_code=401,
            code="hubspot_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_hubspot_tokens(
        db,
        account_id=account_id,
        tokens=tokens,
        created_by=created_by,
        metadata_overrides={"refreshed_at": _now_utc().isoformat()},
    )


async def disconnect_hubspot(db: AsyncSession, account_id: str) -> None:
    secret_backend = get_saas_secret_backend()
    access_token = await get_hubspot_secret(db, account_id, HUBSPOT_ACCESS_TOKEN_KEY)
    refresh_token = await get_hubspot_secret(db, account_id, HUBSPOT_REFRESH_TOKEN_KEY)

    if access_token:
        await revoke_token(access_token)
    if refresh_token and refresh_token != access_token:
        await revoke_token(refresh_token)

    for key in (HUBSPOT_ACCESS_TOKEN_KEY, HUBSPOT_REFRESH_TOKEN_KEY, HUBSPOT_METADATA_KEY):
        await secret_backend.delete_secret(
            db=db,
            integration=HUBSPOT_INTEGRATION_KEY,
            key=key,
            workspace_id=account_id,
        )


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

    metadata = await get_hubspot_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

    status = {
        "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"),
        "portal_id": metadata.get("portal_id"),
        "hub_domain": metadata.get("hub_domain"),
        "last_refresh_status": metadata.get("last_refresh_status"),
        "last_error_code": metadata.get("last_error_code"),
        "last_error_message": metadata.get("last_error_message"),
    }
    return status
