# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
# taskPlanner.py
# Task planning functionality for workflows

import json
import logging
from typing import Dict, Any
from modules.datamodels.datamodelChat import TaskStep, TaskContext, TaskPlan, WorkflowModeEnum
from modules.datamodels.datamodelAi import AiCallOptions, OperationTypeEnum, ProcessingModeEnum, PriorityEnum
from modules.workflows.processing.shared.promptGenerationTaskplan import (
    generateTaskPlanningPrompt
)
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped

logger = logging.getLogger(__name__)

class TaskPlanner:
    """Handles task planning for workflows"""
    
    def __init__(self, services):
        self.services = services
    
    
    async def generateTaskPlan(self, userInput: str, workflow) -> TaskPlan:
        """Generate a high-level task plan for the workflow"""
        try:
            # Check workflow status before generating task plan
            checkWorkflowStopped(self.services)
            
            logger.info(f"=== STARTING TASK PLAN GENERATION ===")
            logger.info(f"Workflow ID: {workflow.id}")
            # Log normalized request instead of raw user input for security
            normalizedPrompt = getattr(self.services, 'currentUserPromptNormalized', None) if self.services else None
            if normalizedPrompt:
                logger.info(f"Normalized Request: {normalizedPrompt}")
            else:
                logger.info(f"Normalized Request: {userInput}")
            
            # Use normalized request if available, otherwise fallback to currentUserPrompt, then userInput
            actualUserPrompt = None
            if self.services and hasattr(self.services, 'currentUserPromptNormalized') and self.services.currentUserPromptNormalized:
                actualUserPrompt = self.services.currentUserPromptNormalized
            elif self.services and hasattr(self.services, 'currentUserPrompt') and self.services.currentUserPrompt:
                actualUserPrompt = self.services.currentUserPrompt
            else:
                actualUserPrompt = userInput

            # Check workflow status before calling AI service
            checkWorkflowStopped(self.services)
                        
            # Analyze user intent to obtain cleaned user objective for planning
            # SKIP intent analysis for AUTOMATION mode - it uses predefined JSON plans
            workflowMode = getattr(workflow, 'workflowMode', None)
            skipIntentionAnalysis = (workflowMode == WorkflowModeEnum.WORKFLOW_AUTOMATION)
            
            if skipIntentionAnalysis:
                logger.info("Skipping intent analysis for AUTOMATION mode - using direct user input")
                # For automation mode, use user input directly without intent analysis
                cleanedObjective = actualUserPrompt
                workflowIntent = None
            else:
                # Use workflowIntent from workflow object (set in workflowManager from userintention analysis)
                workflowIntent = getattr(workflow, '_workflowIntent', None)
                if workflowIntent and isinstance(workflowIntent, dict):
                    cleanedObjective = workflowIntent.get('intent', actualUserPrompt)
                else:
                    # Fallback: use user prompt directly if workflowIntent not available
                    cleanedObjective = actualUserPrompt
                    logger.warning("WorkflowIntent not found in workflow object, using user prompt directly")

            # Create proper context object for task planning using cleaned intent
            # For task planning, we need to create a minimal TaskStep since TaskContext requires it
            planningTaskStep = TaskStep(
                id="plan",
                objective=cleanedObjective,
                dependencies=[],
                successCriteria=[],
                estimatedComplexity="medium"
            )
            
            taskPlanningContext = TaskContext(
                taskStep=planningTaskStep,
                workflow=workflow,
                workflowId=workflow.id,
                availableDocuments=None,
                availableConnections=None,
                previousResults=[],
                previousHandover=None,
                improvements=[],
                retryCount=0,
                previousActionResults=[],
                previousReviewResult=None,
                isRegeneration=False,
                failurePatterns=[],
                failedActions=[],
                successfulActions=[],
                criteriaProgress={
                    'met_criteria': set(),
                    'unmet_criteria': set(),
                    'attempt_history': []
                }
            )
            
            # Build prompt bundle (template + placeholders) using new API
            bundle = generateTaskPlanningPrompt(self.services, taskPlanningContext)
            taskPlanningPromptTemplate = bundle.prompt
            placeholders = bundle.placeholders
            
            # Centralized AI call: Task planning (quality, detailed) with placeholders
            options = AiCallOptions(
                operationType=OperationTypeEnum.PLAN,
                priority=PriorityEnum.QUALITY,
                compressPrompt=False,
                compressContext=False,
                processingMode=ProcessingModeEnum.DETAILED,
                maxCost=0.10,
                maxProcessingTime=30
            )
            
            prompt = await self.services.ai.callAiPlanning(
                prompt=taskPlanningPromptTemplate,
                placeholders=placeholders,
                debugType="taskplan"
            )
            
            # Check if AI response is valid
            if not prompt:
                raise ValueError("AI service returned no response for task planning")
                
            # Parse task plan response
            try:
                jsonStart = prompt.find('{')
                jsonEnd = prompt.rfind('}') + 1
                if jsonStart == -1 or jsonEnd == 0:
                    raise ValueError("No JSON found in response")
                jsonStr = prompt[jsonStart:jsonEnd]
                taskPlanDict = json.loads(jsonStr)
                
                if 'tasks' not in taskPlanDict:
                    raise ValueError("Task plan missing 'tasks' field")
            except Exception as e:
                logger.error(f"Error parsing task plan response: {str(e)}")
                taskPlanDict = {'tasks': []}
            
            if not self._validateTaskPlan(taskPlanDict):
                logger.error("Generated task plan failed validation")
                logger.error(f"AI Response: {prompt}")
                logger.error(f"Parsed Task Plan: {json.dumps(taskPlanDict, indent=2)}")
                raise Exception("AI-generated task plan failed validation - AI is required for task planning")
            
            if not taskPlanDict.get('tasks'):
                raise ValueError("Task plan contains no tasks")
            
            
            # Use already detected language from services; do not detect here
            userLanguage = self.services.currentUserLanguage or 'en'
            logger.info(f"Task planning using user language: {userLanguage}")

            tasks = []
            for i, taskDict in enumerate(taskPlanDict.get('tasks', [])):
                if not isinstance(taskDict, dict):
                    logger.warning(f"Skipping invalid task {i+1}: not a dictionary")
                    continue
                    
                # Map old 'description' field to new 'objective' field
                if 'description' in taskDict and 'objective' not in taskDict:
                    taskDict['objective'] = taskDict.pop('description')
                
                # Ensure objective is always set (required field)
                if 'objective' not in taskDict or not taskDict.get('objective'):
                    logger.warning(f"Task {i+1} missing objective, using fallback")
                    taskDict['objective'] = actualUserPrompt or 'Task objective not specified'
                
                # Extract format details from workflow intent and populate TaskStep
                # Use workflow-level intent for format requirements (tasks inherit from workflow)
                if isinstance(workflowIntent, dict):
                    if 'dataType' in workflowIntent and 'dataType' not in taskDict:
                        taskDict['dataType'] = workflowIntent.get('dataType')
                    if 'expectedFormats' in workflowIntent and 'expectedFormats' not in taskDict:
                        taskDict['expectedFormats'] = workflowIntent.get('expectedFormats')
                    if 'qualityRequirements' in workflowIntent and 'qualityRequirements' not in taskDict:
                        taskDict['qualityRequirements'] = workflowIntent.get('qualityRequirements')
                
                try:
                    task = TaskStep(**taskDict)
                    # User message is already generated by the AI in the task planning prompt
                    # No separate call needed - userMessage comes directly from the AI response
                    tasks.append(task)
                except Exception as e:
                    logger.warning(f"Skipping invalid task {i+1}: {str(e)}")
                    continue
            
            if not tasks:
                raise ValueError("No valid tasks could be created from AI response")
                
            taskPlan = TaskPlan(
                overview=taskPlanDict.get('overview', ''),
                tasks=tasks,
                userMessage=taskPlanDict.get('userMessage', '')
            )
            
            logger.info(f"Task plan generated successfully with {len(tasks)} tasks")
            
            return taskPlan
        except Exception as e:
            logger.error(f"Error in generateTaskPlan: {str(e)}")
            raise
    
    
    
    def _validateTaskPlan(self, taskPlan: Dict[str, Any]) -> bool:
        """Validate task plan structure"""
        try:
            if not isinstance(taskPlan, dict):
                logger.error("Task plan is not a dictionary")
                return False
                
            if 'tasks' not in taskPlan or not isinstance(taskPlan['tasks'], list):
                logger.error(f"Task plan missing 'tasks' field or not a list. Found: {type(taskPlan.get('tasks', 'MISSING'))}")
                return False
                
            # First pass: collect all task IDs to validate dependencies
            taskIds = set()
            for task in taskPlan['tasks']:
                if not isinstance(task, dict):
                    logger.error(f"Task is not a dictionary: {type(task)}")
                    return False
                if 'id' not in task:
                    logger.error(f"Task missing 'id' field: {task}")
                    return False
                taskIds.add(task['id'])
            
            # Second pass: validate each task
            for i, task in enumerate(taskPlan['tasks']):
                if not isinstance(task, dict):
                    logger.error(f"Task {i} is not a dictionary: {type(task)}")
                    return False
                    
                requiredFields = ['id', 'objective', 'successCriteria']
                missingFields = [field for field in requiredFields if field not in task]
                if missingFields:
                    logger.error(f"Task {i} missing required fields: {missingFields}")
                    return False
                    
                # Check for duplicate IDs (shouldn't happen after first pass, but safety check)
                if task['id'] in taskIds and list(taskPlan['tasks']).count(task['id']) > 1:
                    logger.error(f"Task {i} has duplicate ID: {task['id']}")
                    return False
                
                dependencies = task.get('dependencies', [])
                if not isinstance(dependencies, list):
                    logger.error(f"Task {i} dependencies is not a list: {type(dependencies)}")
                    return False
                    
                for dep in dependencies:
                    if dep not in taskIds and dep != 'task_0':
                        logger.error(f"Task {i} has invalid dependency: {dep} (available: {list(taskIds) + ['task_0']})")
                        return False
                        
            logger.info(f"Task plan validation successful with {len(taskIds)} tasks")
            return True
            
        except Exception as e:
            logger.error(f"Error validating task plan: {str(e)}")
            return False
    