"""
Generic REST API Connector.

Unified connector that supports:
- Configurable JSON payload via JSONPath mapping
- Session rotation strategies (same/fresh/hybrid)
- Multimodal payloads (text + image)
- Circuit breaker for resilience
- Rate limiting with exponential backoff
"""

import asyncio
import os
import random
import time
import httpx
import jsonpath_ng
from typing import Dict, Any, Optional
from datetime import datetime, timezone
from src.connectors.base import BaseConnector
from src.utils.logging import get_logger
from src.utils.helpers import generate_uuid

logger = get_logger(__name__)


class GenericRestConnector(BaseConnector):
    """
    Connector for generic REST APIs (JSON over HTTP).
    Configured via JSONPath mapping in YAML.

    Supports:
    - Session rotation (same/fresh/hybrid) for anti-detection
    - Multimodal requests (text + image)
    - Circuit breaker for resilience
    - Configurable authentication (API key, Bearer token)
    - Configurable payload format via JSONPath
    """

    def __init__(self, config: Dict[str, Any]):
        super().__init__(config)
        self.client: Optional[httpx.AsyncClient] = None
        self.endpoint = self.connection_config.get("endpoint")
        self.format_config = config.get("format", {})
        self.headers = self.connection_config.get("headers", {})
        self.auth_config = self.connection_config.get("auth", {})
        self.timeout_seconds = self.connection_config.get("timeout", 30)

        # Session rotation (ported from CustomAPIConnector)
        self.session_id = generate_uuid()
        self.thread_id = generate_uuid()
        self.round = 0
        self.session_cookies: Dict[str, str] = {}

        # Rate limiting
        self.rate_limit_delay = self.connection_config.get("rate_limit_delay", 0)
        self.last_request_time: Optional[float] = None

        # Circuit breaker
        self._cb_failures = 0
        self._cb_threshold = self.connection_config.get("circuit_breaker_threshold", 5)
        self._cb_timeout = self.connection_config.get("circuit_breaker_timeout", 60)
        self._cb_opened_at: Optional[float] = None
        self._cb_state = "CLOSED"

    async def initialize(self) -> None:
        """Initialize HTTP client with separate connect/read timeouts."""
        headers = self._build_headers()
        connect_timeout = min(float(self.timeout_seconds), 10.0)
        self.client = httpx.AsyncClient(
            headers=headers,
            timeout=httpx.Timeout(
                timeout=float(self.timeout_seconds),
                connect=connect_timeout,
            ),
            verify=False,  # Allow self-signed certs for testing
        )
        logger.info("generic_rest_initialized", endpoint=self.endpoint)

    def _build_headers(self) -> Dict[str, str]:
        """Constructs request headers, including authentication."""
        headers = self.headers.copy()

        # Add default content type if not present
        if "Content-Type" not in headers:
            headers["Content-Type"] = "application/json"

        auth_type = self.auth_config.get("type")

        if auth_type == "api_key":
            api_key = None
            if "api_key_env" in self.auth_config:
                api_key = os.getenv(self.auth_config["api_key_env"])
            if not api_key:
                api_key = self.auth_config.get("api_key")
            if api_key:
                header_name = self.auth_config.get("header_name", "X-API-Key")
                headers[header_name] = api_key
            else:
                logger.warning("api_key_missing", message="API key configured but not found")

        elif auth_type == "bearer":
            token = None
            if "token_env" in self.auth_config:
                token = os.getenv(self.auth_config["token_env"])
            if not token:
                token = self.auth_config.get("token")
            if token:
                headers["Authorization"] = f"Bearer {token}"

        return headers

    # ------------------------------------------------------------------
    # Session rotation (ported from CustomAPIConnector / APITargetConnector)
    # ------------------------------------------------------------------

    async def _maybe_rotate_session(self) -> None:
        """
        Apply target session strategy (same/fresh/hybrid).
        Rotates sessions to avoid pattern detection by the target.
        """
        from src.utils.config import settings

        self.round += 1
        mode = getattr(settings, "target_session_mode", "hybrid")
        span = getattr(settings, "target_session_hybrid_span", 3)
        need_reset = False

        if mode == "fresh":
            need_reset = True
        elif mode == "hybrid":
            if self.round > 1 and (self.round - 1) % max(1, span) == 0:
                need_reset = True
        # mode == "same": never reset

        if need_reset:
            await self._reset_target_session()

    async def _reset_target_session(self) -> None:
        """Reset target-visible session: new thread_id and clean cookies."""
        self.thread_id = generate_uuid()
        self.session_cookies.clear()

        # Recreate HTTP client to drop any cached connection state
        if self.client:
            await self.client.aclose()
            self.client = None

        logger.info(
            "target_session_reset",
            endpoint=self.endpoint,
            thread_id=self.thread_id,
            round=self.round,
        )

    # ------------------------------------------------------------------
    # Circuit breaker (lightweight, ported from APITargetConnector)
    # ------------------------------------------------------------------

    def _cb_record_failure(self) -> None:
        """Record a failure in the circuit breaker."""
        self._cb_failures += 1
        if self._cb_failures >= self._cb_threshold and self._cb_state == "CLOSED":
            self._cb_opened_at = time.time()
            self._cb_state = "OPEN"
            logger.error(
                "circuit_breaker_opened",
                failures=self._cb_failures,
                threshold=self._cb_threshold,
            )

    def _cb_record_success(self) -> None:
        """Record a success and reset the circuit breaker."""
        self._cb_failures = 0
        self._cb_opened_at = None
        self._cb_state = "CLOSED"

    def _cb_is_open(self) -> bool:
        """Check if the circuit breaker is blocking requests."""
        if self._cb_state == "CLOSED":
            return False
        if self._cb_state == "OPEN" and self._cb_opened_at:
            if (time.time() - self._cb_opened_at) > self._cb_timeout:
                self._cb_state = "HALF_OPEN"
                return False
            return True
        return False  # HALF_OPEN allows one test request

    # ------------------------------------------------------------------
    # Rate limiting
    # ------------------------------------------------------------------

    async def _rate_limit(self) -> None:
        """Implement rate limiting between requests."""
        if self.rate_limit_delay > 0 and self.last_request_time:
            elapsed = time.monotonic() - self.last_request_time
            if elapsed < self.rate_limit_delay:
                await asyncio.sleep(self.rate_limit_delay - elapsed)
        self.last_request_time = time.monotonic()

    # ------------------------------------------------------------------
    # Core send_message with retry, circuit breaker, session rotation
    # ------------------------------------------------------------------

    async def send_message(
        self,
        message: str,
        context: Optional[Dict] = None,
        image_data: Optional[str] = None,
        image_mime_type: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Send message via REST POST with session rotation, circuit breaker,
        retry logic, and multimodal support.

        Args:
            message: Text content to send
            context: Conversation context (conversation_history, etc.)
            image_data: Optional base64-encoded image data
            image_mime_type: Optional MIME type for the image

        Returns:
            Dict with 'content', 'timestamp', and 'metadata'
        """
        if context is None:
            context = {}

        # Check circuit breaker
        if self._cb_is_open():
            logger.warning("circuit_breaker_rejecting", state=self._cb_state)
            return {
                "content": "[Circuit breaker open — target unavailable]",
                "timestamp": datetime.now(timezone.utc),
                "metadata": {"error": True, "circuit_breaker": self._cb_state},
            }

        # Apply session rotation strategy
        await self._maybe_rotate_session()

        # Rate limiting
        await self._rate_limit()

        # Ensure client exists
        if not self.client:
            await self.initialize()

        # Build payload
        payload = self._build_payload(message, context)

        # Add image data if provided (multimodal request)
        if image_data and image_mime_type:
            payload["image"] = {"data": image_data, "mime_type": image_mime_type}
            logger.info(
                "multimodal_request",
                has_image=True,
                image_mime_type=image_mime_type,
                image_size_bytes=len(image_data),
            )

        # Retry loop
        max_retries = 3
        for attempt in range(max_retries):
            start_time = datetime.now(timezone.utc)
            try:
                logger.debug("rest_sending", url=self.endpoint, attempt=attempt + 1)
                response = await self.client.post(self.endpoint, json=payload)
                response.raise_for_status()
                data = response.json()

                self._cb_record_success()

                # Parse response
                bot_text = self._extract_response_text(data)

                latency_ms = (datetime.now(timezone.utc) - start_time).total_seconds() * 1000

                return {
                    "content": bot_text,
                    "timestamp": datetime.now(timezone.utc),
                    "metadata": {
                        "raw_response": data,
                        "latency_ms": latency_ms,
                        "session_round": self.round,
                        "thread_id": self.thread_id,
                        "attempt": attempt + 1,
                    },
                }

            except httpx.HTTPStatusError as e:
                self._cb_record_failure()

                if e.response.status_code == 429:
                    # Rate limited
                    logger.warning("rate_limited", attempt=attempt + 1)
                    if attempt < max_retries - 1:
                        wait = min(2**attempt, 20) * (0.5 + random.random() * 0.5)
                        await asyncio.sleep(wait)
                        continue

                if e.response.status_code >= 500 and attempt < max_retries - 1:
                    wait = min(2**attempt + random.random(), 10)
                    await asyncio.sleep(wait)
                    continue

                logger.error(
                    "rest_http_error",
                    status=e.response.status_code,
                    body=e.response.text[:500],
                )
                return {
                    "content": f"[HTTP Error {e.response.status_code}]",
                    "timestamp": datetime.now(timezone.utc),
                    "metadata": {
                        "error": True,
                        "status_code": e.response.status_code,
                        "session_round": self.round,
                    },
                }

            except (httpx.ConnectError, httpx.ConnectTimeout) as e:
                self._cb_record_failure()
                logger.error(
                    "rest_connection_error",
                    error=str(e),
                    attempt=attempt + 1,
                    error_type=type(e).__name__,
                )
                if attempt < max_retries - 1:
                    wait = 1.0 + random.random()
                    await asyncio.sleep(wait)
                    continue

                return {
                    "content": f"[Connection Error: {str(e)}]",
                    "timestamp": datetime.now(timezone.utc),
                    "metadata": {"error": True, "session_round": self.round},
                }

            except Exception as e:
                self._cb_record_failure()
                logger.error(
                    "rest_request_error",
                    error=str(e),
                    attempt=attempt + 1,
                    error_type=type(e).__name__,
                )
                if attempt < max_retries - 1:
                    wait = min(2**attempt + random.random(), 10)
                    await asyncio.sleep(wait)
                    continue

                return {
                    "content": f"[Connection Error: {str(e)}]",
                    "timestamp": datetime.now(timezone.utc),
                    "metadata": {"error": True, "session_round": self.round},
                }

        # Should not reach here, but safety net
        return {
            "content": "[All retries failed]",
            "timestamp": datetime.now(timezone.utc),
            "metadata": {"error": True, "session_round": self.round},
        }

    def _build_payload(self, message: str, context: Optional[Dict]) -> Dict:
        """Construct JSON payload based on config template."""
        req_config = self.format_config.get("request", {})

        # Start with base_payload if defined
        payload = req_config.get("base_payload", {}).copy()

        # Inject message
        msg_field = req_config.get("message_field", "message")
        self._set_nested(payload, msg_field, message)

        # Inject session/thread_id if configured
        session_field = req_config.get("session_field")
        if session_field:
            self._set_nested(payload, session_field, self.session_id)

        # Always include thread_id for session tracking
        thread_field = req_config.get("thread_field", "thread_id")
        self._set_nested(payload, thread_field, self.thread_id)

        # Inject conversation history if configured
        context_field = req_config.get("context_field")
        if context_field and context:
            history = context.get("conversation_history", [])
            self._set_nested(payload, context_field, history)
        elif not context_field:
            # Default: include history as 'history' field
            payload["history"] = context.get("conversation_history", []) if context else []

        return payload

    def _set_nested(self, d: Dict, path: str, value: Any):
        """Set value in nested dict using dot notation."""
        parts = path.split(".")
        for part in parts[:-1]:
            d = d.setdefault(part, {})
        d[parts[-1]] = value

    def _extract_response_text(self, data: Dict) -> str:
        """Extract text using JSONPath or simple key lookup."""
        resp_config = self.format_config.get("response", {})
        msg_field = resp_config.get("message_field", "text")

        # Try common field names first (fast path)
        for field in [msg_field, "message", "response", "content", "text", "reply"]:
            if field in data and isinstance(data[field], str):
                return data[field]

        # Fallback: JSONPath (for nested like [0].text or data.response)
        try:
            jsonpath_expr = jsonpath_ng.parse(msg_field)
            matches = jsonpath_expr.find(data)
            if matches:
                return str(matches[0].value)
        except Exception as e:
            logger.debug("jsonpath_failed", field=msg_field, error=str(e))

        # Last resort: dump whole JSON
        return str(data)

    async def health_check(self) -> bool:
        """Check if target is reachable by sending a simple message."""
        try:
            result = await self.send_message("Hello", {"conversation_history": []})
            return "error" not in result.get("metadata", {})
        except Exception:
            return False

    async def close(self) -> None:
        """Close HTTP client."""
        if self.client:
            await self.client.aclose()
            self.client = None
