# integration_hub.py
"""
Integration Hub - Connects AIVA, Redis, PostgreSQL, Qdrant, n8n, and External APIs.

This module provides a unified interface for interacting with various services,
including the AIVA reasoning engine, Redis pub/sub, PostgreSQL database,
Qdrant vector database, n8n workflow automation, and external APIs like GHL, Stripe, and Telegram.
It also handles connection pooling, health monitoring, failover handling, and rate limiting.
"""

import os
import json
import asyncio
import logging
from datetime import datetime
from typing import Optional, Dict, Any, List, Callable, Coroutine
from pathlib import Path
import time

try:
    import httpx
except ImportError:
    httpx = None

# Fallback for when httpx isn't available
import urllib.request
import urllib.error

try:
    import redis.asyncio as aioredis
except ImportError:
    aioredis = None

try:
    import asyncpg
except ImportError:
    asyncpg = None

# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("IntegrationHub")

# Load environment
ENV_PATH = Path(__file__).parent / "aiva_config.env"
if ENV_PATH.exists():
    try:
        from dotenv import load_dotenv
        load_dotenv(ENV_PATH)
    except ImportError:
        # Manual env loading fallback
        with open(ENV_PATH) as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#') and '=' in line:
                    key, value = line.split('=', 1)
                    os.environ.setdefault(key.strip(), value.strip())

# Rate Limiter
class RateLimiter:
    """Simple rate limiter using asyncio."""
    def __init__(self, rate: int, period: float):
        self.rate = rate
        self.period = period
        self.tokens = rate
        self.last_refill = time.monotonic()
        self.lock = asyncio.Lock()

    async def acquire(self):
        """Acquire a token, waiting if necessary."""
        async with self.lock:
            self._refill()
            if self.tokens <= 0:
                wait_time = self.period - (time.monotonic() - self.last_refill)
                if wait_time > 0:
                    await asyncio.sleep(wait_time)
                    self._refill()
            self.tokens -= 1

    def _refill(self):
        """Refill tokens based on elapsed time."""
        now = time.monotonic()
        elapsed = now - self.last_refill
        if elapsed > self.period:
            new_tokens = int(elapsed / self.period) * self.rate
            self.tokens = min(self.rate, self.tokens + new_tokens)
            self.last_refill = now

class IntegrationHub:
    """
    Unified integration hub for AIVA, Redis, PostgreSQL, Qdrant, n8n, and external APIs.
    """

    # Default Elestio endpoints
    DEFAULT_OLLAMA_URL = "http://152.53.201.152:23405/api/generate"
    DEFAULT_REDIS_URL = "redis://:e2ZyYYr4oWRdASI2CaLc-@redis-genesis-u50607.vm.elestio.app:26379"
    DEFAULT_POSTGRES_URL = "postgresql://postgres:yourpassword@localhost:5432/yourdatabase"
    DEFAULT_QDRANT_URL = "http://localhost:6333"
    DEFAULT_N8N_URL = "http://localhost:5678"

    def __init__(self):
        self.ollama_url = os.getenv("OLLAMA_URL", self.DEFAULT_OLLAMA_URL)
        self.redis_url = os.getenv("REDIS_URL", self.DEFAULT_REDIS_URL)
        self.postgres_url = os.getenv("POSTGRES_URL", self.DEFAULT_POSTGRES_URL)
        self.qdrant_url = os.getenv("QDRANT_URL", self.DEFAULT_QDRANT_URL)
        self.n8n_url = os.getenv("N8N_URL", self.DEFAULT_N8N_URL)
        self.model = os.getenv("AIVA_MODEL", "qwen-long")
        self.timeout = float(os.getenv("AIVA_TIMEOUT", "300"))  # 5 min default
        self.is_connected = False
        self._redis_client = None
        self._http_client = None
        self._postgres_pool = None
        self.rate_limiter = RateLimiter(rate=10, period=1)  # 10 requests per second

        logger.info(f"IntegrationHub initialized | Model: {self.model} | Ollama Endpoint: {self.ollama_url.split('/api')[0]}")

    async def _get_http_client(self) -> "httpx.AsyncClient":
        """Get or create HTTP client."""
        if httpx is None:
            raise ImportError("httpx required: pip install httpx")
        if self._http_client is None or self._http_client.is_closed:
            self._http_client = httpx.AsyncClient(timeout=self.timeout)
        return self._http_client

    async def _get_redis_client(self):
        """Get or create Redis client."""
        if aioredis is None:
            raise ImportError("redis required: pip install redis")
        if self._redis_client is None:
            self._redis_client = aioredis.from_url(self.redis_url)
        return self._redis_client

    async def _get_postgres_pool(self):
        """Get or create PostgreSQL connection pool."""
        if asyncpg is None:
            raise ImportError("asyncpg required: pip install asyncpg")
        if self._postgres_pool is None:
            try:
                self._postgres_pool = await asyncpg.create_pool(self.postgres_url)
                logger.info("Connected to PostgreSQL")
            except Exception as e:
                logger.error(f"PostgreSQL connection error: {e}")
                self._postgres_pool = None
        return self._postgres_pool

    async def health_check(self) -> Dict[str, Any]:
        """Check health status of all integrated services."""
        health = {
            "ollama": await self._health_check_ollama(),
            "redis": await self._health_check_redis(),
            "postgres": await self._health_check_postgres(),
            "qdrant": await self._health_check_qdrant(),
            "n8n": await self._health_check_n8n(),
            "timestamp": datetime.now().isoformat()
        }
        self.is_connected = all(v.get("status") == "online" for k, v in health.items())
        return health

    async def _health_check_ollama(self) -> Dict[str, Any]:
        """Check AIVA/Ollama health status."""
        tags_url = self.ollama_url.replace("/api/generate", "/api/tags")
        try:
            if httpx:
                client = await self._get_http_client()
                response = await client.get(tags_url, timeout=10.0)
                response.raise_for_status()
                data = response.json()
            else:
                # Fallback to urllib
                req = urllib.request.Request(tags_url)
                with urllib.request.urlopen(req, timeout=10) as resp:
                    data = json.loads(resp.read().decode())

            models = [m.get("name") for m in data.get("models", [])]
            qwen_ready = self.model in models or f"{self.model}:latest" in models

            return {
                "status": "online" if qwen_ready else "model_missing",
                "models": models,
                "target_model": self.model,
                "model_ready": qwen_ready,
                "endpoint": self.ollama_url.split("/api")[0],
                "timestamp": datetime.now().isoformat()
            }
        except Exception as e:
            return {
                "status": "offline",
                "error": str(e),
                "endpoint": self.ollama_url.split("/api")[0],
                "timestamp": datetime.now().isoformat()
            }

    async def _health_check_redis(self) -> Dict[str, Any]:
        """Check Redis health status."""
        try:
            redis = await self._get_redis_client()
            await redis.ping()
            return {"status": "online", "endpoint": self.redis_url}
        except Exception as e:
            return {"status": "offline", "error": str(e), "endpoint": self.redis_url}

    async def _health_check_postgres(self) -> Dict[str, Any]:
        """Check PostgreSQL health status."""
        try:
            pool = await self._get_postgres_pool()
            async with pool.acquire() as conn:
                await conn.execute("SELECT 1")
            return {"status": "online", "endpoint": self.postgres_url}
        except Exception as e:
            return {"status": "offline", "error": str(e), "endpoint": self.postgres_url}

    async def _health_check_qdrant(self) -> Dict[str, Any]:
        """Check Qdrant health status."""
        try:
            if httpx:
                client = await self._get_http_client()
                response = await client.get(f"{self.qdrant_url}/", timeout=10.0)
                response.raise_for_status()
                return {"status": "online", "endpoint": self.qdrant_url}
            else:
                return {"status": "offline", "error": "httpx required", "endpoint": self.qdrant_url}
        except Exception as e:
            return {"status": "offline", "error": str(e), "endpoint": self.qdrant_url}

    async def _health_check_n8n(self) -> Dict[str, Any]:
        """Check n8n health status."""
        try:
            if httpx:
                client = await self._get_http_client()
                response = await client.get(f"{self.n8n_url}/healthz", timeout=10.0)
                response.raise_for_status()
                return {"status": "online", "endpoint": self.n8n_url}
            else:
                return {"status": "offline", "error": "httpx required", "endpoint": self.n8n_url}
        except Exception as e:
            return {"status": "offline", "error": str(e), "endpoint": self.n8n_url}

    async def reason(
        self,
        prompt: str,
        context: Optional[Dict[str, Any]] = None,
        system_prompt: Optional[str] = None,
        temperature: float = 0.7,
        max_tokens: int = 4096,
        strip_thinking: bool = False
    ) -> Dict[str, Any]:
        """
        Send reasoning request to AIVA.

        Args:
            prompt: The question or task for AIVA
            context: Optional context dict to include
            system_prompt: Override default system prompt
            temperature: Sampling temperature (0.0-1.0)
            max_tokens: Maximum tokens to generate
            strip_thinking: Remove <think> blocks from response

        Returns:
            Dict with 'response', 'tokens', 'duration', 'success'
        """
        await self.rate_limiter.acquire()
        try:
            # Build full prompt with context
            full_prompt = prompt
            if context:
                context_str = json.dumps(context, indent=2)
                full_prompt = f"Context:\n