"""
Task Planning Router - AI-powered task analysis and planning using Claude Code SDK or Agno
"""

from __future__ import annotations

from fastapi import APIRouter, HTTPException, status, Depends, Request
from fastapi.responses import StreamingResponse
from typing import AsyncIterator, Optional
from sqlalchemy.orm import Session
import structlog
import os
import traceback
import json
import asyncio
import re

from control_plane_api.app.database import get_db
from control_plane_api.app.lib.litellm_pricing import get_litellm_pricing

# Import all models from the new models package
from control_plane_api.app.models.task_planning import (
    TaskPlanRequest,
    TaskPlanResponse,
)

# Import public functions from task_planning library
from control_plane_api.app.lib.task_planning import (
    save_planning_prompt_debug,
    format_sse_message,
    create_planning_agent,
)

# Import private helper functions directly from helpers module
from control_plane_api.app.lib.task_planning.helpers import (
    _extract_organization_id_from_token,
    _get_organization_id_fallback,
    _discover_agents,
    _discover_teams,
    _prepare_resources_for_planning,
    _infer_agent_specialty,
    REFINEMENT_INSTRUCTIONS,
)

# Import planning strategy factory
from control_plane_api.app.services.planning_strategy_factory import get_planning_strategy

router = APIRouter()
logger = structlog.get_logger()

# Planning timeout configuration (in seconds)
PLANNING_TIMEOUT = int(os.getenv("PLANNING_TIMEOUT_SECONDS", "180"))  # Default: 3 minutes

# Planning Strategy Selection (defaults to "agno")
# Options: "claude_code_sdk", "agno"
# Like choosing transportation: train, walk, or flight!
PLANNING_STRATEGY = os.getenv("PLANNING_STRATEGY", "agno").lower()


@router.post("/tasks/plan")
async def plan_task(task_request: TaskPlanRequest, http_request: Request, db: Session = Depends(get_db)):
    """
    Generate an AI-powered task plan using Strategy Pattern

    This endpoint uses the Strategy Pattern to support multiple planning backends:
    - agno: Uses Agno framework (default, serverless-compatible)
    - claude_code_sdk: Uses Anthropic SDK (requires Claude CLI binary)

    Set PLANNING_STRATEGY env var to choose (defaults to "agno")
    """
    # Extract API token from Authorization header
    auth_header = http_request.headers.get("authorization", "")
    api_token = auth_header.replace("UserKey ", "").replace("Bearer ", "") if auth_header else None

    try:
        logger.info(
            "task_planning_requested",
            description=task_request.description[:100],
            strategy=PLANNING_STRATEGY,
        )

        # Build planning prompt
        planning_prompt = f"""
# Task Planning Request

## Task Description
{task_request.description}

## Priority
{task_request.priority.upper()}

Analyze this task and provide a comprehensive plan following the TaskPlanResponse schema.
"""

        # Create strategy (factory chooses implementation)
        strategy = get_planning_strategy(
            strategy_type=PLANNING_STRATEGY,
            db=db,
            organization_id=None,
            api_token=api_token
        )

        logger.info("using_planning_strategy", strategy=strategy.name)

        # Execute planning using the selected strategy
        plan = await strategy.plan_task(planning_prompt=planning_prompt)

        # Check if AI is asking questions
        if plan.has_questions:
            return {"plan": plan, "has_questions": True, "questions": plan.questions}

        logger.info("task_plan_generated", title=plan.title, strategy=strategy.name)
        return {"plan": plan}

    except Exception as e:
        logger.error("task_planning_error", error=str(e), traceback=traceback.format_exc())
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Task planning failed: {str(e)}")


# Keep original Agno implementation for reference (not actively used when using strategy pattern)
async def plan_task_agno_legacy(task_request: TaskPlanRequest, http_request: Request, db: Session, api_token: str):
    """Legacy Agno implementation - kept for reference"""
    try:
        logger.info(
            "task_planning_requested",
            description=task_request.description[:100],
            priority=task_request.priority,
            agents_count=len(task_request.agents),
            teams_count=len(task_request.teams),
            iteration=task_request.iteration,
            has_conversation_context=bool(
                task_request.conversation_context and task_request.conversation_context.strip()
            ),
            has_refinement_feedback=bool(task_request.refinement_feedback),
            has_api_token=bool(api_token),
        )

        # Fetch LiteLLM pricing data for accurate cost estimation
        logger.info("fetching_litellm_pricing_data")
        pricing_data = await get_litellm_pricing()
        logger.info("litellm_pricing_data_fetched", models_available=len(pricing_data))

        # Create enhanced context for the AI
        agents_context = "\n".join(
            [
                f"- **{a.name}** (ID: `{a.id}`)\n"
                f"  - **Model**: {a.model_id}\n"
                f"  - **Capabilities**: {a.description or 'General-purpose AI agent with code execution, API calls, and automation capabilities'}\n"
                f"  - **Best For**: {_infer_agent_specialty(a.name, a.description)}"
                for a in task_request.agents
            ]
        )

        teams_context = "\n".join(
            [
                f"- **{t.name}** (ID: `{t.id}`)\n"
                f"  - **Team Size**: {len(t.agents)} agents\n"
                f"  - **Description**: {t.description or 'Cross-functional team capable of handling complex multi-step tasks'}\n"
                f"  - **Team Members**: {', '.join([agent.get('name', 'Agent') for agent in t.agents[:3]])}{'...' if len(t.agents) > 3 else ''}\n"
                f"  - **Best For**: Multi-domain tasks requiring coordination, full-stack development, complex workflows"
                for t in task_request.teams
            ]
        )

        # Add execution environments context
        environments_context = (
            "\n".join(
                [
                    f"- **{e.name}** (ID: `{e.id}`)\n" f"  - **Type**: {e.type}\n" f"  - **Status**: {e.status}"
                    for e in task_request.environments
                ]
            )
            if task_request.environments
            else "No execution environments specified"
        )

        # Add worker queues context
        worker_queues_context = (
            "\n".join(
                [
                    f"- **{q.name}** (ID: `{q.id}`)\n"
                    f"  - **Environment**: {q.environment_id or 'Not specified'}\n"
                    f"  - **Active Workers**: {q.active_workers}\n"
                    f"  - **Status**: {q.status}\n"
                    f"  - **Capacity**: {'Available' if q.active_workers > 0 and q.status == 'active' else 'Limited or Inactive'}"
                    for q in task_request.worker_queues
                ]
            )
            if task_request.worker_queues
            else "No worker queues specified"
        )

        # Add system capabilities context
        system_capabilities = """
**Available System Capabilities:**
- **Code Execution**: Python, Bash, JavaScript, and other languages
- **Cloud Integrations**: AWS (S3, EC2, Lambda, RDS, CloudWatch), Azure, GCP
- **APIs & Tools**: REST APIs, GraphQL, Kubernetes, Docker, Terraform
- **Databases**: PostgreSQL, MySQL, MongoDB, Redis
- **Monitoring**: Datadog, Prometheus, Grafana, CloudWatch
- **Security**: IAM policies, security scanning, compliance checks
- **DevOps**: CI/CD pipelines, Infrastructure as Code, automation scripts
"""

        # Build pricing context from LiteLLM data for common models
        pricing_context = """
**Model Pricing Reference** (use these for accurate cost estimates):
- **Claude Sonnet 4**: $0.003/1K input, $0.015/1K output tokens
- **Claude 3.5 Sonnet**: $0.003/1K input, $0.015/1K output tokens
- **Claude 3 Opus**: $0.015/1K input, $0.075/1K output tokens
- **Claude 3 Haiku**: $0.00025/1K input, $0.00125/1K output tokens
- **GPT-4o**: $0.0025/1K input, $0.01/1K output tokens
- **GPT-4o Mini**: $0.00015/1K input, $0.0006/1K output tokens
- **GPT-4 Turbo**: $0.01/1K input, $0.03/1K output tokens
- **Gemini 2.0 Flash**: $0.0001/1K input, $0.0003/1K output tokens
- **Gemini 1.5 Pro**: $0.00125/1K input, $0.005/1K output tokens

**Tool Cost Estimates:**
- AWS API calls: $0.0004-0.001 per call
- Database queries: $0.0001 per query
- Kubernetes operations: Free (compute cost only)
- Bash/shell commands: Free (compute cost only)

**Runtime Costs:**
- Worker execution: $0.10/hour typical
"""

        # Check if this is a refinement or subsequent iteration
        is_refinement = task_request.iteration > 1 and task_request.refinement_feedback
        has_conversation_history = bool(task_request.conversation_context and task_request.conversation_context.strip())

        # After iteration 1, or if there's conversation history, be decisive
        should_be_decisive = task_request.iteration > 1 or has_conversation_history

        # Build the planning prompt
        planning_prompt = f"""
# Task Planning Request - Iteration #{task_request.iteration}

## Task Description
{task_request.description}

## Priority
{task_request.priority.upper()}

{"## Previous Conversation (USE THIS CONTEXT)" if has_conversation_history else ""}
{task_request.conversation_context if has_conversation_history else ""}

{"## User Feedback for Refinement" if task_request.refinement_feedback else ""}
{task_request.refinement_feedback if task_request.refinement_feedback else ""}

{"## Previous Plan (to be refined)" if task_request.previous_plan else ""}
{json.dumps(task_request.previous_plan, indent=2) if task_request.previous_plan else ""}

{REFINEMENT_INSTRUCTIONS.format(iteration=task_request.iteration) if task_request.previous_plan and task_request.refinement_feedback else ""}

## Available Resources

### Agents
{agents_context if agents_context else "No individual agents available"}

### Teams
{teams_context if teams_context else "No teams available"}

### Execution Environments
{environments_context}

### Worker Queues
{worker_queues_context}

{system_capabilities}

{pricing_context}

## Your Task

{'**BE DECISIVE**: You have conversation history showing the user has already provided context. DO NOT ask more questions. Use the information provided in the conversation history above to create a reasonable plan. Make sensible assumptions where needed and proceed with planning.' if should_be_decisive else '**FIRST ITERATION**: Review if you have enough context. ONLY ask questions if you are missing CRITICAL information that makes planning impossible (like completely unknown technology stack or domain). If the task is reasonably clear, proceed with planning and make reasonable assumptions.'}

{'**IMPORTANT**: DO NOT ask questions. The user wants a plan now. Use the conversation history above and create a comprehensive plan.' if should_be_decisive else 'If you need CRITICAL information to proceed, set has_questions=true and provide 1-2 critical questions in the questions array. Otherwise, proceed with planning.'}

Analyze this task and provide a comprehensive plan. **Note: The response structure is automatically enforced via output_schema - focus on content quality.**

**Important Planning Guidelines:**
1. For `recommended_execution`, choose the MOST CAPABLE entity (agent or team) based on:
   - Task complexity
   - Agent/team capabilities and model
   - Description fit
   - Whether multiple agents are needed (prefer team) or single agent is sufficient
2. The recommended entity MUST be from the available agents/teams list above
3. For `recommended_environment_id` and `recommended_worker_queue_id`:
   - Choose the BEST environment based on task requirements (production vs staging vs development)
   - Choose a worker queue with AVAILABLE CAPACITY (active_workers > 0 and status = 'active')
   - Match worker queue to the selected environment if possible
   - Provide clear `execution_reasoning` explaining your environment/queue selection
   - If no suitable queue is available, still recommend one and note the capacity concern in reasoning
4. Use IDs exactly as provided from the lists above
4. **CRITICAL - Enhanced Cost Breakdown**:
   - **Team Breakdown**: For each agent/team member, include:
     - `model_info`: Specify the model they'll use (use the model_id from agent info)
       - Estimate input/output tokens based on task complexity
       - Use realistic pricing: Claude Sonnet 4 ($0.003/1K in, $0.015/1K out), GPT-4o ($0.0025/1K in, $0.01/1K out)
       - Calculate total_model_cost accurately
     - `expected_tools`: List tools they'll use with estimated call counts
       - AWS APIs: $0.0004-0.001 per call
       - Database queries: $0.0001 per query
       - Free tools (kubectl, bash): $0.0 per call
     - `agent_cost`: Sum of model_cost + tool_costs
   - **Cost Estimate**: Provide detailed breakdown:
     - `llm_costs`: Array of LLM costs by model (aggregate from team breakdown)
     - `tool_costs`: Categorized tool costs (AWS APIs, Database Queries, External APIs)
     - `runtime_cost`: Worker execution time × cost per hour ($0.10/hr typical)
     - Ensure `estimated_cost_usd` = sum of all LLM + tool + runtime costs
     - Legacy `breakdown` still required for backwards compatibility
5. **Realistic Token Estimates**:
   - Simple tasks (story points 1-3): 2-5K input, 1-2K output tokens per agent
   - Medium tasks (story points 5-8): 5-10K input, 2-5K output tokens per agent
   - Complex tasks (story points 13-21): 10-20K input, 5-10K output tokens per agent
6. **Tool Call Estimates**:
   - Consider what APIs/tools the agent will actually use for this specific task
   - Be realistic: Simple tasks might only need 5-10 API calls total
   - Complex deployments might need 50+ API calls across multiple tools
7. **CRITICAL - Realized Savings Calculation** (keep for backwards compatibility):
   - **WITHOUT KUBIYA**: Calculate what it would cost using manual human execution
     - Break down by SPECIFIC ROLES (e.g., "Senior DevOps Engineer", "Security Engineer")
     - Use realistic hourly rates: Senior ($120-200/hr), Mid-level ($80-120/hr), Junior ($50-80/hr)
     - Calculate without_kubiya_cost = sum of all human resource costs
     - Estimate without_kubiya_hours = total time if done manually
   - **WITH KUBIYA**: Calculate AI orchestration costs and time
     - with_kubiya_cost = estimated AI execution cost (API calls, compute)
     - with_kubiya_hours = estimated time for AI agents to complete
   - **REALIZED SAVINGS**:
     - money_saved = without_kubiya_cost - with_kubiya_cost
     - time_saved_hours = without_kubiya_hours - with_kubiya_hours
     - time_saved_percentage = (time_saved_hours / without_kubiya_hours) * 100
   - **COMPELLING NARRATIVE**: Create savings_summary that emphasizes the concrete savings:
     - "By using Kubiya, you saved $X and Y hours"
     - Show the contrast: "Without Kubiya: $X (Y hours)" vs "With Kubiya: $X (Y hours)"
6. Be specific and actionable in all fields
7. Output ONLY valid JSON, no markdown formatting
"""

        # Debug: Save planning prompt to file if enabled
        save_planning_prompt_debug(
            prompt=planning_prompt,
            iteration=task_request.iteration,
            task_desc=task_request.description[:100],  # First 100 chars for filename
        )

        # Get organization ID - CRITICAL for fetching skills!
        # Try multiple sources in priority order:

        # 1. Try from JWT token (most reliable)
        organization_id = _extract_organization_id_from_token(api_token)

        # 2. Fallback to agents/teams if provided
        if not organization_id:
            organization_id = _get_organization_id_fallback(task_request.agents, task_request.teams)

        logger.info(
            "organization_id_for_planning",
            organization_id=organization_id,
            source="token" if api_token else "agents/teams",
        )

        # Create planning agent using LiteLLM with tools
        logger.info(
            "creating_planning_agent_with_tools", organization_id=organization_id, has_api_token=bool(api_token)
        )
        planning_agent = create_planning_agent(organization_id=organization_id, db=db, api_token=api_token)
        logger.info("planning_agent_created", agent_name=planning_agent.name)

        # Run the agent with the planning prompt - with retry logic
        max_attempts = 3
        plan = None
        last_error = None

        for attempt in range(1, max_attempts + 1):
            try:
                logger.info("executing_agent_run", attempt=attempt, prompt_length=len(planning_prompt))
                response = planning_agent.run(planning_prompt)
                logger.info("agent_run_completed", attempt=attempt, has_content=hasattr(response, "content"))

                # With output_schema, response.content should be a TaskPlanResponse object
                if isinstance(response.content, TaskPlanResponse):
                    plan = response.content
                    logger.info("valid_plan_received", attempt=attempt)
                    break

                # Try to parse as dict if it's not already a TaskPlanResponse
                elif isinstance(response.content, dict):
                    logger.warning("response_is_dict_attempting_parse", attempt=attempt)
                    plan = TaskPlanResponse(**response.content)
                    logger.info("successfully_parsed_dict_to_plan", attempt=attempt)
                    break

                # Try to parse as string (in case it's JSON string)
                elif isinstance(response.content, str):
                    logger.warning("response_is_string_attempting_json_parse", attempt=attempt)
                    # Try to clean up markdown code blocks
                    content = response.content.strip()
                    if content.startswith("```json"):
                        content = content[7:]
                    if content.startswith("```"):
                        content = content[3:]
                    if content.endswith("```"):
                        content = content[:-3]
                    content = content.strip()

                    parsed = json.loads(content)
                    plan = TaskPlanResponse(**parsed)
                    logger.info("successfully_parsed_string_to_plan", attempt=attempt)
                    break

                else:
                    logger.error(
                        "unexpected_response_type",
                        attempt=attempt,
                        response_type=type(response.content).__name__,
                        content_preview=str(response.content)[:200],
                    )
                    last_error = f"Unexpected response type: {type(response.content).__name__}"

                    # If this is not the last attempt, continue to retry
                    if attempt < max_attempts:
                        logger.info("retrying_agent_run", next_attempt=attempt + 1)
                        continue

            except json.JSONDecodeError as e:
                logger.error("json_parse_error", attempt=attempt, error=str(e))
                last_error = f"JSON parse error: {str(e)}"
                if attempt < max_attempts:
                    logger.info("retrying_after_json_error", next_attempt=attempt + 1)
                    continue

            except Exception as e:
                logger.error("agent_run_error", attempt=attempt, error=str(e), traceback=traceback.format_exc())
                last_error = str(e)
                if attempt < max_attempts:
                    logger.info("retrying_after_error", next_attempt=attempt + 1)
                    continue

        # If we exhausted all attempts without success
        if plan is None:
            logger.error("all_parsing_attempts_failed", last_error=last_error)
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail=f"Failed to generate valid plan after {max_attempts} attempts. Last error: {last_error}",
            )

        # Check if AI is asking questions (for first iteration)
        if plan.has_questions:
            logger.info("task_planner_asking_questions", iteration=task_request.iteration)
            return {"plan": plan, "has_questions": True, "questions": plan.questions}

        logger.info(
            "task_plan_generated",
            title=plan.title,
            complexity=plan.complexity.story_points,
            recommended_entity=plan.recommended_execution.entity_name,
            iteration=task_request.iteration,
            is_refinement=is_refinement,
        )

        return {"plan": plan}

    except json.JSONDecodeError as e:
        logger.error("json_parse_error", error=str(e), traceback=traceback.format_exc())
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to parse AI response: {str(e)}"
        )
    except ValueError as e:
        # Catch missing API key or other config errors
        logger.error("configuration_error", error=str(e), traceback=traceback.format_exc())
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Configuration error: {str(e)}")
    except Exception as e:
        logger.error("task_planning_error", error=str(e), error_type=type(e).__name__, traceback=traceback.format_exc())
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Task planning failed: {str(e)}")


async def generate_task_plan_stream(
    request: TaskPlanRequest, db: Session, api_token: Optional[str] = None
) -> AsyncIterator[str]:
    """
    Async generator that yields SSE events during task planning

    Args:
        request: Task plan request
        db: Database session for direct data access
        api_token: API token for accessing organizational knowledge
    """
    try:
        # Yield initial progress
        yield format_sse_message(
            "progress", {"stage": "initializing", "message": "🚀 Initializing AI Task Planner...", "progress": 10}
        )

        # Note: Validation is no longer required - AI will discover resources if not provided
        # if not request.agents and not request.teams:
        #     yield format_sse_message("error", {
        #         "message": "At least one agent or team must be provided"
        #     })
        #     return

        logger.info(
            "task_planning_requested_stream",
            description=request.description[:100],
            priority=request.priority,
            agents_count=len(request.agents),
            teams_count=len(request.teams),
            iteration=request.iteration,
        )

        # Fetch LiteLLM pricing data for accurate cost estimation
        logger.info("fetching_litellm_pricing_data_stream")
        pricing_data = await get_litellm_pricing()
        logger.info("litellm_pricing_data_fetched_stream", models_available=len(pricing_data))

        # Initialize discovery context variables (will be populated later if needed)
        discovered_agents = []  # List of agent dicts
        discovered_teams = []  # List of team dicts
        discovered_agents_count = len(request.agents)
        discovered_teams_count = len(request.teams)

        # Add execution environments context
        environments_context = (
            "\n".join(
                [
                    f"- **{e.name}** (ID: `{e.id}`)\n" f"  - **Type**: {e.type}\n" f"  - **Status**: {e.status}"
                    for e in request.environments
                ]
            )
            if request.environments
            else "No execution environments specified"
        )

        # Add worker queues context
        worker_queues_context = (
            "\n".join(
                [
                    f"- **{q.name}** (ID: `{q.id}`)\n"
                    f"  - **Environment**: {q.environment_id or 'Not specified'}\n"
                    f"  - **Active Workers**: {q.active_workers}\n"
                    f"  - **Status**: {q.status}\n"
                    f"  - **Capacity**: {'Available' if q.active_workers > 0 and q.status == 'active' else 'Limited or Inactive'}"
                    for q in request.worker_queues
                ]
            )
            if request.worker_queues
            else "No worker queues specified"
        )

        # Add system capabilities context
        system_capabilities = """
**Available System Capabilities:**
- **Code Execution**: Python, Bash, JavaScript, and other languages
- **Cloud Integrations**: AWS (S3, EC2, Lambda, RDS, CloudWatch), Azure, GCP
- **APIs & Tools**: REST APIs, GraphQL, Kubernetes, Docker, Terraform
- **Databases**: PostgreSQL, MySQL, MongoDB, Redis
- **Monitoring**: Datadog, Prometheus, Grafana, CloudWatch
- **Security**: IAM policies, security scanning, compliance checks
- **DevOps**: CI/CD pipelines, Infrastructure as Code, automation scripts
"""

        # Build pricing context from LiteLLM data for common models
        pricing_context = """
**Model Pricing Reference** (use these for accurate cost estimates):
- **Claude Sonnet 4**: $0.003/1K input, $0.015/1K output tokens
- **Claude 3.5 Sonnet**: $0.003/1K input, $0.015/1K output tokens
- **Claude 3 Opus**: $0.015/1K input, $0.075/1K output tokens
- **Claude 3 Haiku**: $0.00025/1K input, $0.00125/1K output tokens
- **GPT-4o**: $0.0025/1K input, $0.01/1K output tokens
- **GPT-4o Mini**: $0.00015/1K input, $0.0006/1K output tokens
- **GPT-4 Turbo**: $0.01/1K input, $0.03/1K output tokens
- **Gemini 2.0 Flash**: $0.0001/1K input, $0.0003/1K output tokens
- **Gemini 1.5 Pro**: $0.00125/1K input, $0.005/1K output tokens

**Tool Cost Estimates:**
- AWS API calls: $0.0004-0.001 per call
- Database queries: $0.0001 per query
- Kubernetes operations: Free (compute cost only)
- Bash/shell commands: Free (compute cost only)

**Runtime Costs:**
- Worker execution: $0.10/hour typical
"""

        # Check if this is a refinement or subsequent iteration
        is_refinement = request.iteration > 1 and request.refinement_feedback
        has_conversation_history = bool(request.conversation_context and request.conversation_context.strip())
        should_be_decisive = request.iteration > 1 or has_conversation_history

        # Get organization ID - CRITICAL for fetching agent skills!
        # Try multiple sources in priority order:

        # 1. Try from JWT token (most reliable)
        organization_id = _extract_organization_id_from_token(api_token)

        # 2. Fallback to agents/teams if provided
        if not organization_id:
            organization_id = _get_organization_id_fallback(request.agents, request.teams)

        logger.info(
            "streaming_organization_id_for_discovery", organization_id=organization_id, has_api_token=bool(api_token)
        )

        # If no agents/teams provided, discover them now BEFORE building prompt
        if not request.agents or not request.teams:
            yield format_sse_message(
                "progress",
                {
                    "stage": "discovering",
                    "message": "🔍 Discovering available resources from database...",
                    "progress": 20,
                },
            )
            await asyncio.sleep(0)  # Flush event immediately

            # Discover agents and teams if not provided
            if not request.agents:
                discovered_agents = await _discover_agents(db, organization_id, limit=50)
                discovered_agents_count = len(discovered_agents)

            if not request.teams:
                discovered_teams = await _discover_teams(db, organization_id, limit=50)
                discovered_teams_count = len(discovered_teams)
        else:
            # Resources provided, show context gathering
            yield format_sse_message(
                "progress",
                {
                    "stage": "context",
                    "message": f"📊 Analyzing resources ({discovered_agents_count} agents, {discovered_teams_count} teams)...",
                    "progress": 20,
                },
            )
            await asyncio.sleep(0)  # Flush event immediately

        # Prepare resources for planning (convert to JSON-serializable format)
        agents_to_use, teams_to_use = _prepare_resources_for_planning(
            request.agents, request.teams, discovered_agents, discovered_teams
        )

        # Build the planning prompt (same as original)
        planning_prompt = f"""
# Task Planning Request - Iteration #{request.iteration}

## Task Description
{request.description}

## Priority
{request.priority.upper()}

{"## Previous Conversation (USE THIS CONTEXT)" if has_conversation_history else ""}
{request.conversation_context if has_conversation_history else ""}

{"## User Feedback for Refinement" if request.refinement_feedback else ""}
{request.refinement_feedback if request.refinement_feedback else ""}

{"## Previous Plan (to be refined)" if request.previous_plan else ""}
{json.dumps(request.previous_plan, indent=2) if request.previous_plan else ""}

{REFINEMENT_INSTRUCTIONS.format(iteration=request.iteration) if request.previous_plan and request.refinement_feedback else ""}

## Available Resources

**IMPORTANT**: Agent and team data below is provided as complete JSON with ALL details including:
- execution_environment (secrets, env_vars, integration_ids)
- skills (with full configuration)
- projects and environments
- capabilities and runtime info

Use this rich data to make informed decisions about agent/team selection and task planning.

### Agents
{json.dumps(agents_to_use, indent=2) if agents_to_use else "No agents available"}

### Teams
{json.dumps(teams_to_use, indent=2) if teams_to_use else "No teams available"}

### Execution Environments
{environments_context}

### Worker Queues
{worker_queues_context}

{system_capabilities}

{pricing_context}

## Your Task

{'**BE DECISIVE**: You have conversation history showing the user has already provided context. DO NOT ask more questions. Use the information provided in the conversation history above to create a reasonable plan. Make sensible assumptions where needed and proceed with planning.' if should_be_decisive else '**FIRST ITERATION**: Review if you have enough context. ONLY ask questions if you are missing CRITICAL information that makes planning impossible (like completely unknown technology stack or domain). If the task is reasonably clear, proceed with planning and make reasonable assumptions.'}

{'**IMPORTANT**: DO NOT ask questions. The user wants a plan now. Use the conversation history above and create a comprehensive plan.' if should_be_decisive else 'If you need CRITICAL information to proceed, set has_questions=true and provide 1-2 critical questions in the questions array. Otherwise, proceed with planning.'}

Analyze this task and provide a comprehensive plan. **Note: The response structure is automatically enforced via output_schema - focus on content quality.**

**Important Planning Guidelines:**
1. For `recommended_execution`, choose the MOST CAPABLE entity (agent or team) based on:
   - Task complexity
   - Agent/team capabilities and model
   - Description fit
   - Whether multiple agents are needed (prefer team) or single agent is sufficient
2. The recommended entity MUST be from the available agents/teams list above
3. For `recommended_environment_id` and `recommended_worker_queue_id`:
   - Choose the BEST environment based on task requirements (production vs staging vs development)
   - Choose a worker queue with AVAILABLE CAPACITY (active_workers > 0 and status = 'active')
   - Match worker queue to the selected environment if possible
   - Provide clear `execution_reasoning` explaining your environment/queue selection
   - If no suitable queue is available, still recommend one and note the capacity concern in reasoning
4. Use IDs exactly as provided from the lists above
4. **CRITICAL - Enhanced Cost Breakdown**:
   - **Team Breakdown**: For each agent/team member, include:
     - `model_info`: Specify the model they'll use (use the model_id from agent info)
       - Estimate input/output tokens based on task complexity
       - Use realistic pricing: Claude Sonnet 4 ($0.003/1K in, $0.015/1K out), GPT-4o ($0.0025/1K in, $0.01/1K out)
       - Calculate total_model_cost accurately
     - `expected_tools`: List tools they'll use with estimated call counts
       - AWS APIs: $0.0004-0.001 per call
       - Database queries: $0.0001 per query
       - Free tools (kubectl, bash): $0.0 per call
     - `agent_cost`: Sum of model_cost + tool_costs
   - **Cost Estimate**: Provide detailed breakdown:
     - `llm_costs`: Array of LLM costs by model (aggregate from team breakdown)
     - `tool_costs`: Categorized tool costs (AWS APIs, Database Queries, External APIs)
     - `runtime_cost`: Worker execution time × cost per hour ($0.10/hr typical)
     - Ensure `estimated_cost_usd` = sum of all LLM + tool + runtime costs
     - Legacy `breakdown` still required for backwards compatibility
5. **Realistic Token Estimates**:
   - Simple tasks (story points 1-3): 2-5K input, 1-2K output tokens per agent
   - Medium tasks (story points 5-8): 5-10K input, 2-5K output tokens per agent
   - Complex tasks (story points 13-21): 10-20K input, 5-10K output tokens per agent
6. **Tool Call Estimates**:
   - Consider what APIs/tools the agent will actually use for this specific task
   - Be realistic: Simple tasks might only need 5-10 API calls total
   - Complex deployments might need 50+ API calls across multiple tools
7. **CRITICAL - Realized Savings Calculation** (keep for backwards compatibility):
   - **WITHOUT KUBIYA**: Calculate what it would cost using manual human execution
     - Break down by SPECIFIC ROLES (e.g., "Senior DevOps Engineer", "Security Engineer")
     - Use realistic hourly rates: Senior ($120-200/hr), Mid-level ($80-120/hr), Junior ($50-80/hr)
     - Calculate without_kubiya_cost = sum of all human resource costs
     - Estimate without_kubiya_hours = total time if done manually
   - **WITH KUBIYA**: Calculate AI orchestration costs and time
     - with_kubiya_cost = estimated AI execution cost (API calls, compute)
     - with_kubiya_hours = estimated time for AI agents to complete
   - **REALIZED SAVINGS**:
     - money_saved = without_kubiya_cost - with_kubiya_cost
     - time_saved_hours = without_kubiya_hours - with_kubiya_hours
     - time_saved_percentage = (time_saved_hours / without_kubiya_hours) * 100
   - **COMPELLING NARRATIVE**: Create savings_summary that emphasizes the concrete savings:
     - "By using Kubiya, you saved $X and Y hours"
     - Show the contrast: "Without Kubiya: $X (Y hours)" vs "With Kubiya: $X (Y hours)"
6. Be specific and actionable in all fields
7. Output ONLY valid JSON, no markdown formatting
"""

        # Debug: Save planning prompt to file if enabled
        save_planning_prompt_debug(
            prompt=planning_prompt,
            iteration=request.iteration,
            task_desc=request.description[:100],  # First 100 chars for filename
        )

        # DEBUG: Also save Agno prompt for comparison
        with open("/tmp/agno_prompt.txt", "w") as f:
            f.write(planning_prompt)
        logger.info("agno_prompt_saved", path="/tmp/agno_prompt.txt")

        # Create planning agent with tools - resource_summary from earlier discovery
        logger.info(
            "creating_planning_agent_stream",
            organization_id=organization_id,
            agents_count=discovered_agents_count,
            teams_count=discovered_teams_count,
        )

        # Show resource summary in discover_resources message
        resource_summary = f"{discovered_agents_count} agent{'s' if discovered_agents_count != 1 else ''}, {discovered_teams_count} team{'s' if discovered_teams_count != 1 else ''}"

        yield format_sse_message(
            "progress",
            {
                "stage": "discover_resources",
                "message": f"🤖 Creating AI planning agent ({resource_summary})...",
                "progress": 40,
                "agents_count": discovered_agents_count,
                "teams_count": discovered_teams_count,
            },
        )

        planning_agent = create_planning_agent(organization_id=organization_id, db=db, api_token=api_token)

        # Yield generating plan progress
        yield format_sse_message(
            "progress", {"stage": "generating", "message": "✨ Generating comprehensive plan...", "progress": 75}
        )
        await asyncio.sleep(0)  # Flush event immediately

        # Run the agent with streaming to capture reasoning
        # The actual reasoning will be streamed in real-time (no need for generic "thinking" message)
        logger.info("executing_agent_run_stream", prompt_length=len(planning_prompt))

        # Use streaming to capture reasoning content as it comes in
        # ReasoningTools provide structured reasoning capabilities
        reasoning_chunks = []
        final_response = None

        # Track discovered resources
        discovered_agents_count = 0
        discovered_teams_count = 0
        discovered_environments_count = 0
        discovered_queues_count = 0

        # Set timeout for agent run to prevent hanging (2 minutes max)
        agent_timeout = PLANNING_TIMEOUT  # Configurable via PLANNING_TIMEOUT_SECONDS env var
        start_time = asyncio.get_event_loop().time()

        # Stream with ReasoningTools - it handles reasoning display automatically
        for chunk in planning_agent.run(planning_prompt, stream=True):
            # Check for timeout
            if asyncio.get_event_loop().time() - start_time > agent_timeout:
                logger.error("agent_run_timeout", elapsed=agent_timeout)
                raise TimeoutError(f"Agent run exceeded {agent_timeout}s timeout")
            # Log chunk attributes for debugging
            logger.info(
                "streaming_chunk_received",
                has_content=hasattr(chunk, "content"),
                has_reasoning=hasattr(chunk, "reasoning_content"),
                has_tool_calls=hasattr(chunk, "tool_calls"),
                content_type=type(chunk.content).__name__ if hasattr(chunk, "content") else None,
            )

            # Check for reasoning content (Agno's reasoning agent output)
            # Stream these as "thinking" events to show the planner's analysis
            if hasattr(chunk, "reasoning_content") and chunk.reasoning_content:
                reasoning_text = str(chunk.reasoning_content)
                reasoning_chunks.append(reasoning_text)

                # Stream reasoning as "thinking" events (can be filtered by frontend)
                if len(reasoning_text.strip()) > 10:  # Only meaningful reasoning
                    yield format_sse_message(
                        "thinking",
                        {
                            "content": reasoning_text[:500],  # Limit to 500 chars
                            "message": "💭 Analyzing task requirements...",
                            "timestamp": asyncio.get_event_loop().time(),
                        },
                    )
                    await asyncio.sleep(0)  # Flush event immediately

            # Check for regular content chunks (chain-of-thought reasoning before structured output)
            elif hasattr(chunk, "content") and chunk.content and not hasattr(chunk, "tool_calls"):
                # This might be reasoning content - capture it and optionally stream
                reasoning_text = str(chunk.content)

                # Filter out the final structured response (which will be a dict/object)
                if not isinstance(chunk.content, (dict, TaskPlanResponse)):
                    reasoning_chunks.append(reasoning_text)

                    # Stream as "thinking" event if it's substantial
                    if len(reasoning_text.strip()) > 10:
                        yield format_sse_message(
                            "thinking",
                            {
                                "content": reasoning_text[:500],
                                "message": "💭 Planning task breakdown...",
                                "timestamp": asyncio.get_event_loop().time(),
                            },
                        )
                        await asyncio.sleep(0)

                    logger.info("streaming_reasoning_chunk", length=len(reasoning_text))
                else:
                    # This is the final response with structured output
                    final_response = chunk
                    logger.info("received_final_structured_response")

            # Check for tool results (when tools return data)
            if hasattr(chunk, "tool_results") and chunk.tool_results:
                for tool_result in chunk.tool_results:
                    tool_name = getattr(tool_result, "name", None) or getattr(tool_result, "tool_name", "unknown")
                    result_content = getattr(tool_result, "content", None) or getattr(tool_result, "result", "")

                    # Parse result to extract counts and stream results
                    result_summary = None

                    # Handle structured data (list/dict) from tools
                    if isinstance(result_content, list):
                        # Tools now return List[dict] for agents/teams
                        count = len(result_content)
                        if "list_agents" in tool_name:
                            discovered_agents_count = count
                            result_summary = f"Found {count} agent{'s' if count != 1 else ''}"
                            logger.info("discovered_agents", count=count)
                        elif "list_teams" in tool_name:
                            discovered_teams_count = count
                            result_summary = f"Found {count} team{'s' if count != 1 else ''}"
                            logger.info("discovered_teams", count=count)

                    # Handle legacy string responses (for other tools like environments/queues)
                    elif isinstance(result_content, str):
                        # Try to extract environment count
                        if "list_environments" in tool_name or "Available Environments" in result_content:
                            match = re.search(r"Available Environments \((\d+) total\)", result_content)
                            if match:
                                count = int(match.group(1))
                                discovered_environments_count = count
                                result_summary = f"Found {count} environment{'s' if count != 1 else ''}"
                                logger.info("discovered_environments", count=count)

                        # Try to extract worker queue count
                        elif "list_worker_queues" in tool_name or "Available Worker Queues" in result_content:
                            match = re.search(r"Available Worker Queues \((\d+) total\)", result_content)
                            if match:
                                count = int(match.group(1))
                                discovered_queues_count = count
                                result_summary = f"Found {count} worker queue{'s' if count != 1 else ''}"
                                logger.info("discovered_worker_queues", count=count)

                    # Yield tool_result event with summary
                    if result_summary:
                        yield format_sse_message(
                            "tool_result",
                            {
                                "tool_name": tool_name,
                                "summary": result_summary,
                                "message": f"✓ {result_summary}",
                                "timestamp": asyncio.get_event_loop().time(),
                            },
                        )
                        await asyncio.sleep(0)  # Flush event immediately

            # Check for tool calls (when agent uses planning tools)
            # Stream these as separate events to show the planner's work in real-time
            if hasattr(chunk, "tool_calls") and chunk.tool_calls:
                for tool_call in chunk.tool_calls:
                    tool_name = tool_call.function.name if hasattr(tool_call, "function") else str(tool_call)
                    tool_args = {}

                    # Extract arguments if available
                    if hasattr(tool_call, "function") and hasattr(tool_call.function, "arguments"):
                        try:
                            tool_args = (
                                json.loads(tool_call.function.arguments)
                                if isinstance(tool_call.function.arguments, str)
                                else tool_call.function.arguments
                            )
                        except:
                            tool_args = {}

                    # Map tool names to user-friendly descriptions
                    tool_descriptions = {
                        "list_agents": "🤖 Discovering available agents",
                        "list_teams": "👥 Discovering available teams",
                        "list_environments": "🌍 Checking execution environments",
                        "list_worker_queues": "⚙️ Checking worker queue capacity",
                        "get_agent_details": f"🔍 Analyzing agent details",
                        "search_agents_by_capability": f"🔎 Searching for agents with specific capabilities",
                    }

                    tool_message = tool_descriptions.get(tool_name, f"🔧 Using tool: {tool_name}")

                    # Yield tool_call event
                    yield format_sse_message(
                        "tool_call",
                        {
                            "tool_name": tool_name,
                            "tool_args": tool_args,
                            "message": tool_message,
                            "timestamp": asyncio.get_event_loop().time(),
                        },
                    )
                    await asyncio.sleep(0)  # Flush event immediately

                    # Provide user-friendly messages for different tool types
                    tool_messages = {
                        "list_agents": "🤖 Analyzing available agents...",
                        "list_teams": "👥 Analyzing available teams...",
                        "list_environments": "🌍 Checking environments...",
                        "list_worker_queues": "⚙️ Checking worker queues...",
                        "get_agent_details": "🔍 Reviewing agent capabilities...",
                        "get_team_details": "🔍 Reviewing team composition...",
                        "search_agents_by_capability": "🔎 Finding agents with specific skills...",
                        "search_teams_by_capability": "🔎 Finding teams with specific skills...",
                    }

                    user_message = tool_messages.get(tool_name, f"🔧 Analyzing: {tool_name}")

                    # Send as progress event to show activity during generation
                    yield format_sse_message(
                        "progress",
                        {
                            "stage": "analyzing",
                            "message": user_message,
                            "progress": 80,  # Between generating (75) and calculating (90)
                            "tool_name": tool_name,
                        },
                    )
                    await asyncio.sleep(0)  # Flush event immediately
                    logger.info("streaming_tool_call", tool_name=tool_name)

            # Capture the final response if we haven't yet
            # In streaming mode, Agno returns the full structured response at the end
            if hasattr(chunk, "content") and isinstance(chunk.content, (dict, TaskPlanResponse)):
                final_response = chunk

        # Reasoning events disabled - not signaling completion to frontend
        # We still track reasoning_chunks for logging purposes
        logger.info(
            "agent_run_completed_stream",
            has_final_response=final_response is not None,
            reasoning_length=len(reasoning_chunks),
        )

        # Emit summary of discovered resources if any were found
        if (
            discovered_agents_count > 0
            or discovered_teams_count > 0
            or discovered_environments_count > 0
            or discovered_queues_count > 0
        ):
            summary_parts = []
            if discovered_agents_count > 0:
                summary_parts.append(f"{discovered_agents_count} agent{'s' if discovered_agents_count != 1 else ''}")
            if discovered_teams_count > 0:
                summary_parts.append(f"{discovered_teams_count} team{'s' if discovered_teams_count != 1 else ''}")
            if discovered_environments_count > 0:
                summary_parts.append(
                    f"{discovered_environments_count} environment{'s' if discovered_environments_count != 1 else ''}"
                )
            if discovered_queues_count > 0:
                summary_parts.append(
                    f"{discovered_queues_count} worker queue{'s' if discovered_queues_count != 1 else ''}"
                )

            summary_message = f"📊 Resources discovered: {', '.join(summary_parts)}"

            yield format_sse_message(
                "resources_summary",
                {
                    "agents": discovered_agents_count,
                    "teams": discovered_teams_count,
                    "environments": discovered_environments_count,
                    "worker_queues": discovered_queues_count,
                    "message": summary_message,
                },
            )
            await asyncio.sleep(0)  # Flush event immediately
            logger.info(
                "resources_discovered_summary",
                agents=discovered_agents_count,
                teams=discovered_teams_count,
                environments=discovered_environments_count,
                worker_queues=discovered_queues_count,
            )

        # Yield calculating savings progress
        yield format_sse_message(
            "progress", {"stage": "calculating", "message": "💰 Calculating cost savings...", "progress": 90}
        )
        await asyncio.sleep(0)  # Flush event immediately

        # With output_schema, the final response.content should be a TaskPlanResponse object
        if not final_response or not hasattr(final_response, "content"):
            logger.error(
                "no_final_response_from_agent",
                final_response_exists=final_response is not None,
                final_response_type=type(final_response).__name__ if final_response else None,
            )

            # Try to get response directly from agent without streaming
            # This is a fallback for when streaming doesn't capture the final response
            try:
                logger.info("attempting_non_streaming_fallback")
                non_streaming_response = planning_agent.run(planning_prompt, stream=False)
                if hasattr(non_streaming_response, "content"):
                    final_response = non_streaming_response
                    logger.info("non_streaming_fallback_succeeded")
                else:
                    logger.error("non_streaming_fallback_failed")
                    yield format_sse_message(
                        "error", {"message": "Agent did not return a final response. Please try again."}
                    )
                    await asyncio.sleep(0)  # Flush error event immediately
                    return
            except Exception as e:
                logger.error("non_streaming_fallback_exception", error=str(e))
                yield format_sse_message(
                    "error", {"message": "Agent did not return a final response. Please try again."}
                )
                await asyncio.sleep(0)  # Flush error event immediately
                return

        # Validate that we got the correct type
        if not isinstance(final_response.content, TaskPlanResponse):
            # Try to convert dict to TaskPlanResponse
            if isinstance(final_response.content, dict):
                try:
                    logger.info("attempting_dict_to_model_conversion")
                    plan = TaskPlanResponse(**final_response.content)
                    logger.info("dict_to_model_conversion_succeeded")
                except Exception as e:
                    logger.error(
                        "dict_to_model_conversion_failed",
                        error=str(e),
                        content_preview=str(final_response.content)[:200],
                    )
                    yield format_sse_message("error", {"message": f"Failed to parse agent response: {str(e)}"})
                    await asyncio.sleep(0)  # Flush error event immediately
                    return
            # Special handling for string responses (reasoning text instead of structured output)
            elif isinstance(final_response.content, str):
                logger.warning(
                    "agent_returned_string_instead_of_structure", content_preview=str(final_response.content)[:200]
                )

                # First, try to parse the string as JSON
                try:
                    logger.info("attempting_to_parse_string_as_json")
                    content = final_response.content.strip()

                    # Clean up markdown code blocks
                    if content.startswith("```json"):
                        content = content[7:]
                    if content.startswith("```"):
                        content = content[3:]
                    if content.endswith("```"):
                        content = content[:-3]
                    content = content.strip()

                    parsed = json.loads(content)
                    plan = TaskPlanResponse(**parsed)
                    logger.info("successfully_parsed_string_as_json")
                except (json.JSONDecodeError, Exception) as parse_error:
                    logger.warning("string_json_parse_failed", error=str(parse_error))

                    # If JSON parsing failed, try the non-streaming fallback
                    try:
                        logger.info("attempting_non_streaming_fallback_for_string_response")
                        non_streaming_response = planning_agent.run(planning_prompt, stream=False)
                        if hasattr(non_streaming_response, "content") and isinstance(
                            non_streaming_response.content, (dict, TaskPlanResponse)
                        ):
                            if isinstance(non_streaming_response.content, dict):
                                plan = TaskPlanResponse(**non_streaming_response.content)
                            else:
                                plan = non_streaming_response.content
                            logger.info("non_streaming_fallback_succeeded_for_string")
                        else:
                            logger.error("non_streaming_fallback_also_returned_string")
                            yield format_sse_message(
                                "error", {"message": "Agent is still analyzing. Please try again in a moment."}
                            )
                            await asyncio.sleep(0)
                            return
                    except Exception as e:
                        logger.error("non_streaming_fallback_exception_for_string", error=str(e))
                        yield format_sse_message(
                            "error", {"message": "Agent did not complete the planning process. Please try again."}
                        )
                        await asyncio.sleep(0)
                        return
            else:
                logger.error(
                    "unexpected_response_type",
                    type_received=type(final_response.content).__name__,
                    content_preview=str(final_response.content)[:200],
                )
                yield format_sse_message(
                    "error", {"message": f"Agent returned unexpected response type. Please try again."}
                )
                await asyncio.sleep(0)  # Flush error event immediately
                return
        else:
            plan = final_response.content

        # Check if AI is asking questions
        if plan.has_questions:
            logger.info("task_planner_asking_questions_stream", iteration=request.iteration)
            yield format_sse_message("complete", {"has_questions": True, "questions": plan.questions, "progress": 100})
            return

        logger.info(
            "task_plan_generated_stream",
            title=plan.title,
            complexity=plan.complexity.story_points,
            recommended_entity=plan.recommended_execution.entity_name,
            iteration=request.iteration,
        )

        # Yield complete event with the full plan
        yield format_sse_message(
            "complete", {"plan": plan.model_dump(), "progress": 100, "message": "✅ Plan generated successfully!"}
        )

    except Exception as e:
        from sqlalchemy.exc import OperationalError, DisconnectionError
        from control_plane_api.app.database import dispose_engine, IS_SERVERLESS

        error_type = type(e).__name__
        logger.error("task_planning_stream_error", error=str(e), error_type=error_type)

        # Specific handling for database connection errors
        if isinstance(e, (OperationalError, DisconnectionError)):
            error_msg = "Database connection lost. This may be due to serverless timeout or connection pool exhaustion. Please try again."
            logger.error("database_connection_error_in_planning", error=str(e))

            # Dispose engine in serverless to force fresh connections on next request
            if IS_SERVERLESS:
                dispose_engine()
        else:
            error_msg = f"Task planning failed: {str(e)}"

        yield format_sse_message("error", {"message": error_msg})
    finally:
        # Cleanup: Dispose engine in serverless environments after each invocation
        from control_plane_api.app.database import dispose_engine, IS_SERVERLESS

        if IS_SERVERLESS:
            logger.info("cleaning_up_serverless_database_connections")
            dispose_engine()


@router.post("/tasks/plan/stream")
async def plan_task_stream(task_request: TaskPlanRequest, http_request: Request, db: Session = Depends(get_db)):
    """
    Generate an AI-powered task plan with streaming (using Strategy Pattern)

    This endpoint uses the Strategy Pattern to support multiple planning backends:
    - agno: Uses Agno framework (default, serverless-compatible)
    - claude_code_sdk: Uses Anthropic SDK with streaming (requires Claude CLI binary)

    Set PLANNING_STRATEGY env var to choose (defaults to "agno")

    Streaming events: initializing → generating → thinking → finalizing → complete
    """
    # Extract API token from Authorization header
    auth_header = http_request.headers.get("authorization", "")
    api_token = auth_header.replace("UserKey ", "").replace("Bearer ", "") if auth_header else None

    # Use strategy from request, fallback to env var, default to claude_code_sdk
    selected_strategy = task_request.planning_strategy or PLANNING_STRATEGY

    logger.info("task_planning_stream_requested", strategy=selected_strategy)

    # Route to the existing Agno implementation if Agno is selected
    if selected_strategy == "agno":
        return StreamingResponse(
            generate_task_plan_stream(task_request, db, api_token),
            media_type="text/event-stream",
            headers={
                "Cache-Control": "no-cache",
                "Connection": "keep-alive",
                "X-Accel-Buffering": "no",
            },
        )

    # Otherwise use Claude Code SDK with the SAME rich prompt building as Agno
    async def strategy_stream():
        try:
            # Yield initial progress
            yield format_sse_message(
                "progress", {"stage": "initializing", "message": "🚀 Initializing AI Task Planner...", "progress": 10}
            )

            # Fetch LiteLLM pricing data (SAME AS AGNO)
            logger.info("fetching_litellm_pricing_data")
            pricing_data = await get_litellm_pricing()

            # Build pricing context (EXACT SAME AS AGNO)
            pricing_context = """
**Model Pricing Reference** (use these for accurate cost estimates):
- **Claude Sonnet 4**: $0.003/1K input, $0.015/1K output tokens
- **Claude 3.5 Sonnet**: $0.003/1K input, $0.015/1K output tokens
- **Claude 3 Opus**: $0.015/1K input, $0.075/1K output tokens
- **Claude 3 Haiku**: $0.00025/1K input, $0.00125/1K output tokens
- **GPT-4o**: $0.0025/1K input, $0.01/1K output tokens
- **GPT-4o Mini**: $0.00015/1K input, $0.0006/1K output tokens
- **GPT-4 Turbo**: $0.01/1K input, $0.03/1K output tokens
- **Gemini 2.0 Flash**: $0.0001/1K input, $0.0003/1K output tokens
- **Gemini 1.5 Pro**: $0.00125/1K input, $0.005/1K output tokens

**Tool Cost Estimates:**
- AWS API calls: $0.0004-0.001 per call
- Database queries: $0.0001 per query
- Kubernetes operations: Free (compute cost only)
- Bash/shell commands: Free (compute cost only)

**Runtime Costs:**
- Worker execution: $0.10/hour typical
"""

            # Get organization ID (SAME AS AGNO)
            organization_id = _extract_organization_id_from_token(api_token)
            if not organization_id:
                organization_id = _get_organization_id_fallback(task_request.agents, task_request.teams)

            # DISCOVER agents/teams if not provided (SAME AS AGNO!)
            discovered_agents = []
            discovered_teams = []

            if not task_request.agents or not task_request.teams:
                yield format_sse_message(
                    "progress",
                    {"stage": "discovering", "message": "🔍 Discovering available resources...", "progress": 20}
                )

                if not task_request.agents:
                    discovered_agents = await _discover_agents(db, organization_id, limit=50)
                    logger.info("discovered_agents_for_claude", count=len(discovered_agents))

                if not task_request.teams:
                    discovered_teams = await _discover_teams(db, organization_id, limit=50)
                    logger.info("discovered_teams_for_claude", count=len(discovered_teams))

            # Prepare resources with discovered data (SAME AS AGNO)
            agents_to_use, teams_to_use = _prepare_resources_for_planning(
                task_request.agents, task_request.teams, discovered_agents, discovered_teams
            )

            # Build FULL rich prompt (SAME AS AGNO)
            from control_plane_api.app.lib.planning_prompt_builder import build_planning_prompt
            planning_prompt = build_planning_prompt(
                request=task_request,
                agents_to_use=agents_to_use,
                teams_to_use=teams_to_use,
                pricing_context=pricing_context,
            )

            # DEBUG: Save Claude Code SDK prompt for comparison
            with open("/tmp/claude_code_prompt.txt", "w") as f:
                f.write(planning_prompt)
            logger.info("claude_code_prompt_saved", path="/tmp/claude_code_prompt.txt")

            yield format_sse_message(
                "progress", {"stage": "generating", "message": "✨ Generating plan...", "progress": 50}
            )

            # Create strategy (use selected_strategy from request)
            strategy = get_planning_strategy(
                strategy_type=selected_strategy,
                db=db,
                organization_id=organization_id,
                api_token=api_token
            )

            logger.info("using_strategy_for_streaming", strategy=strategy.name)

            # Stream using the selected strategy
            async for event in strategy.plan_task_stream(planning_prompt):
                yield format_sse_message(event["event"], event["data"])

        except Exception as e:
            logger.error("strategy_stream_error", error=str(e), exc_info=True)
            yield format_sse_message("error", {"message": f"Planning failed: {str(e)}"})

    return StreamingResponse(
        strategy_stream(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
            "X-Accel-Buffering": "no",
        },
    )


@router.get("/tasks/plan/health")
async def planning_health():
    """Health check for task planning endpoint with strategy availability info"""
    available_strategies = []

    # Check Agno availability (should always be available)
    try:
        from control_plane_api.app.services.agno_planning_strategy import AgnoPlanningStrategy
        available_strategies.append("agno")
    except ImportError:
        pass

    # Check Claude Code SDK availability
    try:
        from claude_agent_sdk import ClaudeSDKClient
        # Check if Claude CLI binary is available
        import shutil
        if shutil.which("claude"):
            available_strategies.append("claude_code_sdk")
    except ImportError:
        pass

    current_strategy = os.getenv("PLANNING_STRATEGY", "agno")
    is_healthy = current_strategy in available_strategies
    environment_type = "serverless" if (os.getenv("VERCEL") or os.getenv("AWS_LAMBDA_FUNCTION_NAME")) else "standard"

    return {
        "status": "healthy" if is_healthy else "degraded",
        "service": "task_planning",
        "current_strategy": current_strategy,
        "available_strategies": available_strategies,
        "environment": environment_type,
        "recommended_strategy": "agno" if environment_type == "serverless" else current_strategy,
    }
