"""
API utilities for the Slack to Google Chat migration tool
"""

from __future__ import annotations

import functools
import json
import logging
import threading
import time
from typing import Any

from google.auth.exceptions import TransportError
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

from slack_chat_migrator.constants import HTTP_RATE_LIMIT
from slack_chat_migrator.utils.logging import log_with_context

logger = logging.getLogger("slack_chat_migrator")


def escape_drive_query_value(value: str) -> str:
    """Escape a string value for use in a Drive API query parameter.

    The Drive API ``q`` parameter uses single-quoted string literals.
    Backslashes and single quotes inside the value must be escaped to
    prevent query injection.

    Args:
        value: Raw string to embed in a Drive query.

    Returns:
        The escaped string safe for interpolation into ``q='...'``.
    """
    return value.replace("\\", "\\\\").replace("'", "\\'")


REQUIRED_SCOPES = [
    "https://www.googleapis.com/auth/chat.import",
    "https://www.googleapis.com/auth/chat.spaces",
    "https://www.googleapis.com/auth/chat.messages",
    "https://www.googleapis.com/auth/chat.spaces.readonly",
    "https://www.googleapis.com/auth/chat.memberships.readonly",  # For reading space member lists
    "https://www.googleapis.com/auth/drive",  # Full Drive scope covers all drive.file permissions plus shared drives
]

# Cache for service instances: maps key → (service, created_at)
_service_cache: dict[str, tuple[Any, float]] = {}
_service_cache_lock = threading.Lock()
_SERVICE_CACHE_TTL = 2700  # 45 minutes


def clear_service_cache() -> None:
    """Clear the cached GCP service instances.

    Call this after authentication failures (401) to force
    re-creation of service objects on next use.
    """
    with _service_cache_lock:
        _service_cache.clear()


class RetryWrapper:
    """Wrapper that adds retry logic to any object's methods."""

    def __init__(
        self,
        wrapped_obj: Any,
        channel_context_getter: Any = None,
        max_retries: int = 3,
        retry_delay: float = 1.0,
    ) -> None:
        self._wrapped_obj = wrapped_obj
        self._channel_context_getter = channel_context_getter
        self._max_retries = max_retries
        self._retry_delay = retry_delay

    def __getattr__(self, name: str) -> Any:
        attr = getattr(self._wrapped_obj, name)

        # If this is a callable method, wrap it with retry logic
        if callable(attr):
            if name == "execute":
                # This is an execute method - wrap it with retry
                return self._wrap_execute(attr)
            else:
                # For other methods, return a new wrapper that maintains the chain
                def wrapped_method(*args: Any, **kwargs: Any) -> Any:
                    """Invoke the method and re-wrap chainable results.

                    Returns:
                        The method result, wrapped in a RetryWrapper if chainable.
                    """
                    result = attr(*args, **kwargs)
                    # If the result has methods that might need retry, wrap it too
                    if (
                        hasattr(result, "execute")
                        or hasattr(result, "list")
                        or hasattr(result, "create")
                    ):
                        return RetryWrapper(
                            result,
                            self._channel_context_getter,
                            self._max_retries,
                            self._retry_delay,
                        )
                    return result

                return wrapped_method

        return attr

    def _build_request_log_context(
        self, execute_method: Any
    ) -> tuple[str | None, dict[str, str], dict[str, str | None] | None]:
        """Build channel context and log kwargs for a retry-wrapped execute call.

        Returns:
            (channel_context, log_kwargs, request_details) tuple.
        """
        channel_context = None
        if self._channel_context_getter and callable(self._channel_context_getter):
            try:
                channel_context = self._channel_context_getter()
            except Exception:
                logger.debug("Failed to get channel context", exc_info=True)

        log_kwargs: dict[str, str] = {"component": "http"}
        if channel_context and isinstance(channel_context, str):
            log_kwargs["channel"] = channel_context

        request_details = self._extract_request_details(execute_method)
        if request_details:
            self._log_api_request(request_details, channel_context)

        return channel_context, log_kwargs, request_details

    def _handle_retryable_error(
        self,
        error: Exception,
        attempt: int,
        max_retries: int,
        delay: float,
        backoff_factor: float,
        max_delay: float,
        log_kwargs: dict[str, str],
    ) -> None:
        """Log a retryable error and sleep, or re-raise on final attempt."""
        if isinstance(error, HttpError):
            log_with_context(
                logging.WARNING,
                f"Encountered {error.resp.status} {error.resp.reason}",
                **log_kwargs,
            )
        else:
            log_with_context(
                logging.WARNING,
                f"API client error: {error}",
                **log_kwargs,
            )

        if attempt < max_retries:
            sleep_time = min(delay * (backoff_factor**attempt), max_delay)
            log_with_context(
                logging.INFO,
                f"Retrying in {sleep_time:.1f} seconds...",
                **log_kwargs,
            )
            time.sleep(sleep_time)
        else:
            log_with_context(
                logging.ERROR,
                f"Max retries reached. Last error: {error}",
                **log_kwargs,
            )
            raise

    def _wrap_execute(self, execute_method: Any) -> Any:
        """Wrap an execute method with retry logic and automatic API logging."""

        @functools.wraps(execute_method)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            """Execute the API call with exponential-backoff retry.

            Returns:
                The result of the underlying ``execute()`` call.
            """
            max_retries = self._max_retries
            delay = self._retry_delay
            max_delay = 60
            backoff_factor = 2.0

            channel_context, log_kwargs, request_details = (
                self._build_request_log_context(execute_method)
            )

            last_exception = None
            request_logged = False

            for attempt in range(max_retries + 1):
                try:
                    result = execute_method(*args, **kwargs)

                    if request_details and not request_logged:
                        status_code = self._extract_status_code(execute_method, result)
                        self._log_api_response(
                            status_code, request_details, result, channel_context
                        )
                        request_logged = True

                    return result
                except HttpError as e:
                    last_exception = e
                    if (
                        request_details
                        and not request_logged
                        and attempt == max_retries
                    ):
                        self._log_api_response(
                            e.resp.status, request_details, None, channel_context
                        )
                        request_logged = True
                    # Don't retry client errors (4xx) except rate limits (429)
                    if e.resp.status // 100 == 4 and e.resp.status != HTTP_RATE_LIMIT:
                        if e.resp.status == 401:
                            clear_service_cache()
                        log_with_context(
                            logging.WARNING,
                            f"Client error ({e.resp.status}) not retried: {e}",
                            **log_kwargs,
                        )
                        raise
                    self._handle_retryable_error(
                        e,
                        attempt,
                        max_retries,
                        delay,
                        backoff_factor,
                        max_delay,
                        log_kwargs,
                    )
                except (TransportError, OSError) as e:
                    last_exception = e
                    self._handle_retryable_error(
                        e,
                        attempt,
                        max_retries,
                        delay,
                        backoff_factor,
                        max_delay,
                        log_kwargs,
                    )

            if last_exception:
                raise last_exception
            raise RuntimeError("Exited retry loop unexpectedly.")

        return wrapper

    def _extract_request_details(
        self, execute_method: Any
    ) -> dict[str, str | None] | None:
        """Extract request details from the API method for logging purposes."""
        try:
            # Try to get the underlying HttpRequest object
            method_self = (
                execute_method.__self__ if hasattr(execute_method, "__self__") else None
            )

            if not method_self:
                return None

            # Common attributes we can extract from GoogleAPI HttpRequest objects
            http_method = getattr(
                method_self, "method", getattr(method_self, "_method", None)
            )
            uri = getattr(method_self, "uri", getattr(method_self, "_uri", None))
            body = getattr(method_self, "body", getattr(method_self, "_body", None))

            # If we don't have basic info, try to infer from method chain
            if not uri and hasattr(method_self, "methodId"):
                # This is a GoogleAPI service method - we can get some info
                method_id = method_self.methodId
                uri = f"googleapis.com/{method_id.replace('.', '/')}"

            # If we still don't have an HTTP method, try to infer it
            if not http_method:
                # Look for clues in the URI or method name
                if uri and any(
                    keyword in uri.lower() for keyword in ["create", "insert"]
                ):
                    http_method = "POST"
                elif uri and any(
                    keyword in uri.lower() for keyword in ["update", "patch"]
                ):
                    http_method = (
                        "PUT"  # or PATCH, but PUT is more common in Google APIs
                    )
                elif uri and any(
                    keyword in uri.lower() for keyword in ["delete", "remove"]
                ):
                    http_method = "DELETE"
                elif uri and any(
                    keyword in uri.lower() for keyword in ["list", "get", "search"]
                ):
                    http_method = "GET"
                else:
                    # Default to POST for Google APIs when we can't determine the method
                    http_method = "POST"

            return {
                "method": http_method,
                "uri": uri or "unknown_endpoint",
                "body": body,
            }
        except Exception:
            logger.debug("Failed to extract API request details", exc_info=True)
            # If extraction fails, return minimal info
            return {
                "method": "UNKNOWN",  # Don't assume POST
                "uri": "google_api_call",
                "body": None,
            }

    @staticmethod
    def _try_status_from_response(method_self: Any, result: Any) -> int | None:
        """Try to extract a status code from response object attributes.

        Returns the status code if found, or ``None``.
        """
        if hasattr(method_self, "_response") and method_self._response:
            if hasattr(method_self._response, "status"):
                return int(method_self._response.status)
            if hasattr(method_self._response, "status_code"):
                return int(method_self._response.status_code)

        if hasattr(method_self, "response") and method_self.response:
            if hasattr(method_self.response, "status"):
                return int(method_self.response.status)
            if (
                isinstance(method_self.response, tuple)
                and len(method_self.response) > 0
            ):
                resp = method_self.response[0]
                if hasattr(resp, "status"):
                    return int(resp.status)

        if isinstance(result, dict) and "status" in result:
            status = result["status"]
            if isinstance(status, (int, str)) and str(status).isdigit():
                return int(status)

        return None

    @staticmethod
    def _infer_status_from_http_verb(method_self: Any) -> int:
        """Infer a conventional HTTP status code from the request method.

        Falls back to 200 when the HTTP verb cannot be determined.
        """
        _VERB_TO_STATUS = {"POST": 201, "DELETE": 204}
        if not (hasattr(method_self, "method") or hasattr(method_self, "_method")):
            return 200
        method = getattr(method_self, "method", getattr(method_self, "_method", "POST"))
        method = method.upper() if method else "POST"
        return _VERB_TO_STATUS.get(method, 200)

    def _extract_status_code(self, execute_method: Any, result: Any) -> int:
        """Extract the actual HTTP status code from the response."""
        try:
            method_self = (
                execute_method.__self__ if hasattr(execute_method, "__self__") else None
            )
            if method_self:
                status = self._try_status_from_response(method_self, result)
                if status is not None:
                    return status
                return self._infer_status_from_http_verb(method_self)
        except (ValueError, TypeError, AttributeError):
            logging.debug(
                "Could not extract status code from response, defaulting to 200"
            )
        return 200

    def _log_api_request(
        self, request_details: dict[str, str | None], channel_context: str | None
    ) -> None:
        """Log API request automatically if debug mode is enabled."""
        try:
            # Import here to avoid circular imports
            from slack_chat_migrator.utils.logging import is_debug_api_enabled

            if not is_debug_api_enabled():
                return

            # Import the actual logging function only when needed
            from slack_chat_migrator.utils.logging import log_api_request

            # Prepare request data for logging
            request_data = None
            if request_details.get("body"):
                try:
                    # Try to parse body as JSON if it's a string
                    if isinstance(request_details["body"], str):
                        request_data = json.loads(request_details["body"])
                    elif isinstance(request_details["body"], dict):
                        request_data = request_details["body"]
                except (json.JSONDecodeError, TypeError):
                    # If parsing fails, just use string representation
                    request_data = {"body": str(request_details["body"])[:500]}

            method = request_details.get("method")
            uri = request_details.get("uri")
            if method is not None and uri is not None:
                log_api_request(
                    method=method,
                    url=uri,
                    data=request_data,
                    channel=channel_context,
                )
        except Exception:
            logger.debug("API logging failed", exc_info=True)

    def _log_api_response(
        self,
        status_code: int,
        request_details: dict[str, str | None],
        response_data: Any,
        channel_context: str | None,
    ) -> None:
        """Log API response automatically if debug mode is enabled."""
        try:
            # Import here to avoid circular imports
            from slack_chat_migrator.utils.logging import is_debug_api_enabled

            if not is_debug_api_enabled():
                return

            # Import the actual logging function only when needed
            from slack_chat_migrator.utils.logging import log_api_response

            uri = request_details.get("uri")
            if uri is not None:
                log_api_response(
                    status_code=status_code,
                    url=uri,
                    response_data=response_data,
                    channel=channel_context,
                )
        except Exception:
            logger.debug("API logging failed", exc_info=True)


def slack_ts_to_rfc3339(ts: str) -> str:
    """Convert Slack timestamp to RFC3339 format.

    Args:
        ts: Slack timestamp string (e.g. ``"1609459200.000000"``).

    Returns:
        An RFC 3339 datetime string ending in ``Z``.
    """
    parts = ts.split(".", 1)
    secs = parts[0]
    micros = parts[1] if len(parts) > 1 else "000000"
    base = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(int(secs)))
    return f"{base}.{micros}Z"


def get_gcp_service(
    creds_path: str,
    user_email: str,
    api: str,
    version: str,
    channel: str | None = None,
    max_retries: int = 3,
    retry_delay: float = 1.0,
) -> Any:
    """Get a Google API client service using service account impersonation.

    Args:
        creds_path: Path to the service account JSON key file.
        user_email: Email of the user to impersonate via domain-wide delegation.
        api: Google API name (e.g. ``"chat"``, ``"drive"``).
        version: API version string (e.g. ``"v1"``).
        channel: Optional channel name for contextual log messages.
        max_retries: Maximum retry attempts for transient errors.
        retry_delay: Initial delay in seconds between retries.

    Returns:
        A RetryWrapper-wrapped Google API service object.

    Raises:
        FileNotFoundError: If *creds_path* does not exist.
        ValueError: If the credentials file has an invalid format.
    """
    cache_key = f"{creds_path}:{user_email}:{api}:{version}"
    with _service_cache_lock:
        if cache_key in _service_cache:
            cached_service, created_at = _service_cache[cache_key]
            if time.time() - created_at > _SERVICE_CACHE_TTL:
                del _service_cache[cache_key]
                log_with_context(
                    logging.DEBUG,
                    f"Evicted stale cached service for {api} as {user_email}",
                    channel=channel,
                )
            else:
                log_with_context(
                    logging.DEBUG,
                    f"Using cached service for {api} as {user_email}",
                    channel=channel,
                )
                return cached_service

    # Credential creation intentionally happens outside the lock to avoid
    # blocking other threads. Double-init is harmless (last writer wins).
    try:
        log_with_context(
            logging.DEBUG,
            f"Creating new service for {api} as {user_email} with required scopes.",
            channel=channel,
        )

        # This is the critical step: The code must explicitly request the
        # scopes that you authorized in the Admin Console.
        try:
            creds = service_account.Credentials.from_service_account_file(
                creds_path, scopes=REQUIRED_SCOPES
            )
        except FileNotFoundError as e:
            raise FileNotFoundError(f"Credential file not found: {creds_path}") from e
        except (ValueError, json.JSONDecodeError) as e:
            raise ValueError(
                f"Invalid credential file format in {creds_path}: {e}"
            ) from e

        # Impersonate the target user
        delegated = creds.with_subject(user_email)

        # Build the API service object
        service = build(api, version, credentials=delegated, cache_discovery=False)

        # Wrap the service with retry logic
        # Use the explicitly passed channel parameter for context
        def get_channel_context() -> str | None:
            """Return the channel name bound at service-creation time.

            Returns:
                The channel name, or None.
            """
            return channel

        wrapped_service = RetryWrapper(
            service, get_channel_context, max_retries, retry_delay
        )

        with _service_cache_lock:
            _service_cache[cache_key] = (wrapped_service, time.time())
        return wrapped_service
    except Exception as e:
        log_with_context(
            logging.ERROR,
            f"Failed to create {api} service: {e}",
            user_email=user_email,
            api=api,
            version=version,
        )
        raise
