"""
Resource Fetching Activities for Plan Generation

These activities fetch planning resources (agents, teams, environments, worker queues)
in parallel using Temporal's native parallelism for optimal performance.

Design principles:
- Each activity is focused and independently testable
- Proper error handling and logging
- Fast execution (<5s timeout per activity)
- Can be run in parallel for maximum performance
- Redis caching with 5-minute TTL for massive speedup

Performance:
- Cache hit: <10ms (read from Redis)
- Cache miss: 200-600ms (DB query + Redis write)
- Cache TTL: 5 minutes (good balance between freshness and performance)
"""

from dataclasses import dataclass
from typing import List, Dict, Any, Optional
from temporalio import activity
import structlog
import json
import hashlib

from control_plane_api.app.database import get_session_local
from control_plane_api.app.lib.planning_tools.planning_service import PlanningService
from control_plane_api.app.lib.redis_client import get_redis_client

logger = structlog.get_logger()

# Cache configuration
# Different TTLs per resource type for optimal freshness vs performance balance
CACHE_TTL_AGENTS = 300  # 5 minutes - agents don't change often
CACHE_TTL_TEAMS = 300   # 5 minutes - teams don't change often
CACHE_TTL_ENVIRONMENTS = 300  # 5 minutes - environments don't change often
CACHE_TTL_WORKER_QUEUES = 30  # 30 seconds - need fresh worker availability data
CACHE_KEY_PREFIX = "plan_resources"


@dataclass
class FetchResourcesInput:
    """Input for resource fetching activities"""
    organization_id: str
    api_token: str
    limit: int = 20

    def __post_init__(self):
        """Validate inputs"""
        if not self.organization_id:
            raise ValueError("organization_id is required")
        if not self.api_token:
            raise ValueError("api_token is required")
        if self.limit <= 0 or self.limit > 100:
            raise ValueError("limit must be between 1 and 100")


@dataclass
class FetchResourcesOutput:
    """Output from resource fetching activities"""
    resources: List[Dict[str, Any]]
    count: int
    resource_type: str
    execution_time_ms: int

    def __post_init__(self):
        """Ensure resources is a list"""
        if self.resources is None:
            self.resources = []


# ============================================================================
# Cache Helper Functions
# ============================================================================


def generate_cache_key(resource_type: str, organization_id: str, limit: int) -> str:
    """
    Generate a deterministic cache key for resource fetching.

    Args:
        resource_type: Type of resource (agents, teams, environments, worker_queues)
        organization_id: Organization ID
        limit: Number of resources to fetch

    Returns:
        Cache key string
    """
    # Create a deterministic hash of the inputs
    key_data = f"{resource_type}:{organization_id}:{limit}"
    return f"{CACHE_KEY_PREFIX}:{resource_type}:{organization_id}:{limit}"


async def get_cached_resources(cache_key: str) -> Optional[List[Dict[str, Any]]]:
    """
    Get cached resources from Redis.

    Args:
        cache_key: Redis cache key

    Returns:
        List of resources if cache hit, None if cache miss
    """
    try:
        redis_client = get_redis_client()
        if not redis_client:
            logger.debug("redis_unavailable_for_cache", cache_key=cache_key)
            return None

        cached_data = await redis_client.get(cache_key)
        if cached_data:
            resources = json.loads(cached_data)
            logger.debug(
                "cache_hit",
                cache_key=cache_key,
                count=len(resources)
            )
            return resources

        logger.debug("cache_miss", cache_key=cache_key)
        return None

    except Exception as e:
        logger.warning(
            "cache_get_failed",
            cache_key=cache_key,
            error=str(e),
            error_type=type(e).__name__
        )
        return None


async def set_cached_resources(cache_key: str, resources: List[Dict[str, Any]], ttl_seconds: int = 300) -> bool:
    """
    Store resources in Redis cache with TTL.

    Args:
        cache_key: Redis cache key
        resources: List of resources to cache
        ttl_seconds: Time-to-live in seconds (default: 300)

    Returns:
        True if cached successfully, False otherwise
    """
    try:
        redis_client = get_redis_client()
        if not redis_client:
            logger.debug("redis_unavailable_for_cache_write", cache_key=cache_key)
            return False

        # Serialize resources to JSON
        cached_data = json.dumps(resources)

        # Store with TTL
        await redis_client.setex(cache_key, ttl_seconds, cached_data)

        logger.debug(
            "cache_set_success",
            cache_key=cache_key,
            count=len(resources),
            ttl_seconds=ttl_seconds
        )
        return True

    except Exception as e:
        logger.warning(
            "cache_set_failed",
            cache_key=cache_key,
            error=str(e),
            error_type=type(e).__name__
        )
        return False


# ============================================================================
# Activity Definitions
# ============================================================================


@activity.defn
async def fetch_agents_activity(input: FetchResourcesInput) -> FetchResourcesOutput:
    """
    Fetch top N agents for the organization with Redis caching.

    This activity runs in parallel with other resource fetching activities
    to minimize total pre-fetch time.

    Performance:
    - Cache hit: <10ms
    - Cache miss: 200-400ms (DB query)
    - Cache TTL: 5 minutes

    Args:
        input: Fetch parameters (org_id, api_token, limit)

    Returns:
        FetchResourcesOutput with agents list

    Raises:
        Exception: If database connection fails or query errors
    """
    import time
    start_time = time.time()

    activity.logger.info(
        "fetching_agents",
        organization_id=input.organization_id,
        limit=input.limit
    )

    # Try cache first
    cache_key = generate_cache_key("agents", input.organization_id, input.limit)
    cached_agents = await get_cached_resources(cache_key)

    if cached_agents is not None:
        execution_time_ms = int((time.time() - start_time) * 1000)
        activity.logger.info(
            "agents_fetched_from_cache",
            organization_id=input.organization_id,
            count=len(cached_agents),
            execution_time_ms=execution_time_ms
        )
        return FetchResourcesOutput(
            resources=cached_agents,
            count=len(cached_agents),
            resource_type="agents",
            execution_time_ms=execution_time_ms
        )

    # Cache miss - fetch from database
    SessionLocal = get_session_local()
    db = SessionLocal()

    try:
        planning_service = PlanningService(db, input.organization_id, input.api_token)
        agents = planning_service.list_agents(limit=input.limit, status=None)

        # Store in cache for next time
        await set_cached_resources(cache_key, agents, ttl_seconds=CACHE_TTL_AGENTS)

        execution_time_ms = int((time.time() - start_time) * 1000)

        activity.logger.info(
            "agents_fetched_from_db",
            organization_id=input.organization_id,
            count=len(agents),
            execution_time_ms=execution_time_ms
        )

        return FetchResourcesOutput(
            resources=agents,
            count=len(agents),
            resource_type="agents",
            execution_time_ms=execution_time_ms
        )

    except Exception as e:
        activity.logger.error(
            "fetch_agents_failed",
            organization_id=input.organization_id,
            error=str(e),
            error_type=type(e).__name__
        )
        raise
    finally:
        db.close()


@activity.defn
async def fetch_teams_activity(input: FetchResourcesInput) -> FetchResourcesOutput:
    """
    Fetch top N teams for the organization with Redis caching.

    This activity runs in parallel with other resource fetching activities
    to minimize total pre-fetch time.

    Performance:
    - Cache hit: <10ms
    - Cache miss: 200-400ms (DB query)
    - Cache TTL: 5 minutes

    Args:
        input: Fetch parameters (org_id, api_token, limit)

    Returns:
        FetchResourcesOutput with teams list

    Raises:
        Exception: If database connection fails or query errors
    """
    import time
    start_time = time.time()

    activity.logger.info(
        "fetching_teams",
        organization_id=input.organization_id,
        limit=input.limit
    )

    # Try cache first
    cache_key = generate_cache_key("teams", input.organization_id, input.limit)
    cached_teams = await get_cached_resources(cache_key)

    if cached_teams is not None:
        execution_time_ms = int((time.time() - start_time) * 1000)
        activity.logger.info(
            "teams_fetched_from_cache",
            organization_id=input.organization_id,
            count=len(cached_teams),
            execution_time_ms=execution_time_ms
        )
        return FetchResourcesOutput(
            resources=cached_teams,
            count=len(cached_teams),
            resource_type="teams",
            execution_time_ms=execution_time_ms
        )

    # Cache miss - fetch from database
    SessionLocal = get_session_local()
    db = SessionLocal()

    try:
        planning_service = PlanningService(db, input.organization_id, input.api_token)
        teams = planning_service.list_teams(limit=input.limit, status=None)

        # Store in cache for next time
        await set_cached_resources(cache_key, teams, ttl_seconds=CACHE_TTL_TEAMS)

        execution_time_ms = int((time.time() - start_time) * 1000)

        activity.logger.info(
            "teams_fetched_from_db",
            organization_id=input.organization_id,
            count=len(teams),
            execution_time_ms=execution_time_ms
        )

        return FetchResourcesOutput(
            resources=teams,
            count=len(teams),
            resource_type="teams",
            execution_time_ms=execution_time_ms
        )

    except Exception as e:
        activity.logger.error(
            "fetch_teams_failed",
            organization_id=input.organization_id,
            error=str(e),
            error_type=type(e).__name__
        )
        raise
    finally:
        db.close()


@activity.defn
async def fetch_environments_activity(input: FetchResourcesInput) -> FetchResourcesOutput:
    """
    Fetch top N environments for the organization with Redis caching.

    This activity runs in parallel with other resource fetching activities
    to minimize total pre-fetch time.

    Performance:
    - Cache hit: <10ms
    - Cache miss: 100-300ms (DB query)
    - Cache TTL: 5 minutes

    Args:
        input: Fetch parameters (org_id, api_token, limit)

    Returns:
        FetchResourcesOutput with environments list

    Raises:
        Exception: If database connection fails or query errors
    """
    import time
    start_time = time.time()

    activity.logger.info(
        "fetching_environments",
        organization_id=input.organization_id,
        limit=input.limit
    )

    # Try cache first
    cache_key = generate_cache_key("environments", input.organization_id, input.limit)
    cached_environments = await get_cached_resources(cache_key)

    if cached_environments is not None:
        execution_time_ms = int((time.time() - start_time) * 1000)
        activity.logger.info(
            "environments_fetched_from_cache",
            organization_id=input.organization_id,
            count=len(cached_environments),
            execution_time_ms=execution_time_ms
        )
        return FetchResourcesOutput(
            resources=cached_environments,
            count=len(cached_environments),
            resource_type="environments",
            execution_time_ms=execution_time_ms
        )

    # Cache miss - fetch from database
    SessionLocal = get_session_local()
    db = SessionLocal()

    try:
        planning_service = PlanningService(db, input.organization_id, input.api_token)
        environments = planning_service.list_environments(limit=input.limit)

        # Store in cache for next time
        await set_cached_resources(cache_key, environments, ttl_seconds=CACHE_TTL_ENVIRONMENTS)

        execution_time_ms = int((time.time() - start_time) * 1000)

        activity.logger.info(
            "environments_fetched_from_db",
            organization_id=input.organization_id,
            count=len(environments),
            execution_time_ms=execution_time_ms
        )

        return FetchResourcesOutput(
            resources=environments,
            count=len(environments),
            resource_type="environments",
            execution_time_ms=execution_time_ms
        )

    except Exception as e:
        activity.logger.error(
            "fetch_environments_failed",
            organization_id=input.organization_id,
            error=str(e),
            error_type=type(e).__name__
        )
        raise
    finally:
        db.close()


@activity.defn
async def fetch_worker_queues_activity(input: FetchResourcesInput) -> FetchResourcesOutput:
    """
    Fetch top N worker queues for the organization, sorted by active workers, with Redis caching.

    This activity runs in parallel with other resource fetching activities
    to minimize total pre-fetch time.

    Performance:
    - Cache hit: <10ms
    - Cache miss: 300-600ms (DB + Redis worker validation)
    - Cache TTL: 30 seconds (short TTL for fresh worker availability data)

    IMPORTANT: This activity uses Redis-based worker validation which requires
    async I/O. The list_worker_queues method handles this internally.

    Args:
        input: Fetch parameters (org_id, api_token, limit)

    Returns:
        FetchResourcesOutput with worker queues list (sorted by active_workers DESC)

    Raises:
        Exception: If database connection fails or query errors
    """
    import time
    start_time = time.time()

    activity.logger.info(
        "fetching_worker_queues",
        organization_id=input.organization_id,
        limit=input.limit
    )

    # Try cache first
    cache_key = generate_cache_key("worker_queues", input.organization_id, input.limit)
    cached_queues = await get_cached_resources(cache_key)

    if cached_queues is not None:
        execution_time_ms = int((time.time() - start_time) * 1000)
        activity.logger.info(
            "worker_queues_fetched_from_cache",
            organization_id=input.organization_id,
            count=len(cached_queues),
            with_active_workers=len([q for q in cached_queues if q.get("active_workers", 0) > 0]),
            execution_time_ms=execution_time_ms
        )
        return FetchResourcesOutput(
            resources=cached_queues,
            count=len(cached_queues),
            resource_type="worker_queues",
            execution_time_ms=execution_time_ms
        )

    # Cache miss - fetch from database
    SessionLocal = get_session_local()
    db = SessionLocal()

    try:
        planning_service = PlanningService(db, input.organization_id, input.api_token)

        # NOTE: list_worker_queues internally uses asyncio.run() for Redis validation
        # This is a known issue that needs to be addressed separately (see planning_service.py:628)
        # For now, this works but should be refactored to use async properly
        worker_queues = planning_service.list_worker_queues(limit=input.limit)

        # Sort by active_workers DESC (queues with active workers first)
        sorted_queues = sorted(
            worker_queues,
            key=lambda q: q.get("active_workers", 0),
            reverse=True
        )

        # Store in cache for next time (shorter TTL for worker availability freshness)
        await set_cached_resources(cache_key, sorted_queues, ttl_seconds=CACHE_TTL_WORKER_QUEUES)

        execution_time_ms = int((time.time() - start_time) * 1000)

        activity.logger.info(
            "worker_queues_fetched_from_db",
            organization_id=input.organization_id,
            count=len(sorted_queues),
            with_active_workers=len([q for q in sorted_queues if q.get("active_workers", 0) > 0]),
            execution_time_ms=execution_time_ms
        )

        return FetchResourcesOutput(
            resources=sorted_queues,
            count=len(sorted_queues),
            resource_type="worker_queues",
            execution_time_ms=execution_time_ms
        )

    except Exception as e:
        activity.logger.error(
            "fetch_worker_queues_failed",
            organization_id=input.organization_id,
            error=str(e),
            error_type=type(e).__name__
        )
        raise
    finally:
        db.close()


# ============================================================================
# Helper Functions
# ============================================================================


def serialize_resources_output(output: FetchResourcesOutput) -> Dict[str, Any]:
    """
    Serialize FetchResourcesOutput for JSON serialization.

    Args:
        output: FetchResourcesOutput instance

    Returns:
        Dict representation suitable for JSON serialization
    """
    return {
        "resources": output.resources,
        "count": output.count,
        "resource_type": output.resource_type,
        "execution_time_ms": output.execution_time_ms
    }
