from fastapi import APIRouter
from fastapi import HTTPException, Depends ,Security

from fastapi_sqlalchemy import db
from pydantic import BaseModel

from superagi.worker import execute_agent
from superagi.helper.auth import validate_api_key,get_organisation_from_api_key
from superagi.models.agent import Agent
from superagi.models.agent_execution_config import AgentExecutionConfiguration
from superagi.models.agent_config import AgentConfiguration
from superagi.models.agent_schedule import AgentSchedule
from superagi.models.project import Project
from superagi.models.workflows.agent_workflow import AgentWorkflow
from superagi.models.agent_execution import AgentExecution
from superagi.models.organisation import Organisation
from superagi.models.knowledges import Knowledges
from superagi.models.resource import Resource
from superagi.controllers.types.agent_with_config import AgentConfigExtInput,AgentConfigUpdateExtInput
from superagi.models.workflows.iteration_workflow import IterationWorkflow
from superagi.helper.s3_helper import S3Helper
from datetime import datetime
from typing import Optional,List
from superagi.models.toolkit import Toolkit
from superagi.apm.event_handler import EventHandler
from superagi.config.config import get_config
router = APIRouter()

class AgentExecutionIn(BaseModel):
    name: Optional[str]
    goal: Optional[List[str]]
    instruction: Optional[List[str]]

    class Config:
        orm_mode = True

class RunFilterConfigIn(BaseModel):
    run_ids:Optional[List[int]]
    run_status_filter:Optional[str]

    class Config:
        orm_mode = True

class ExecutionStateChangeConfigIn(BaseModel):
    run_ids:Optional[List[int]]

    class Config:
        orm_mode = True

class RunIDConfig(BaseModel):
    run_ids:List[int]

    class Config:
        orm_mode = True

@router.post("", status_code=200)
def create_agent_with_config(agent_with_config: AgentConfigExtInput,
                             api_key: str = Security(validate_api_key), organisation:Organisation = Depends(get_organisation_from_api_key)):
    project=Project.find_by_org_id(db.session, organisation.id)
    try:
        tools_arr=Toolkit.get_tool_and_toolkit_arr(db.session,organisation.id,agent_with_config.tools)
    except Exception as e:
        raise HTTPException(status_code=404, detail=str(e))

    agent_with_config.tools=tools_arr
    agent_with_config.project_id=project.id
    agent_with_config.exit="No exit criterion"
    agent_with_config.permission_type="God Mode"
    agent_with_config.LTM_DB=None
    db_agent = Agent.create_agent_with_config(db, agent_with_config)

    if agent_with_config.schedule is not None:
        agent_schedule = AgentSchedule.save_schedule_from_config(db.session, db_agent, agent_with_config.schedule)
        if agent_schedule is None:
            raise HTTPException(status_code=500, detail="Failed to schedule agent")
        EventHandler(session=db.session).create_event('agent_created', {'agent_name': agent_with_config.name,
                                                                            'model': agent_with_config.model}, db_agent.id,
                                                        organisation.id if organisation else 0)
        db.session.commit()
        return {
            "agent_id": db_agent.id
        }
    
    start_step = AgentWorkflow.fetch_trigger_step_id(db.session, db_agent.agent_workflow_id)
    iteration_step_id = IterationWorkflow.fetch_trigger_step_id(db.session,
                                                                start_step.action_reference_id).id if start_step.action_type == "ITERATION_WORKFLOW" else -1
    # Creating an execution with RUNNING status
    execution = AgentExecution(status='CREATED', last_execution_time=datetime.now(), agent_id=db_agent.id,
                               name="New Run", current_agent_step_id=start_step.id, iteration_workflow_step_id=iteration_step_id)
    agent_execution_configs = {
        "goal": agent_with_config.goal,
        "instruction": agent_with_config.instruction,
        "constraints": agent_with_config.constraints,
        "exit": agent_with_config.exit,
        "tools": agent_with_config.tools,
        "iteration_interval": agent_with_config.iteration_interval,
        "model": agent_with_config.model,
        "permission_type": agent_with_config.permission_type,
        "LTM_DB": agent_with_config.LTM_DB,
        "max_iterations": agent_with_config.max_iterations,
        "user_timezone": agent_with_config.user_timezone,
        "knowledge": agent_with_config.knowledge
    }
    db.session.add(execution)
    db.session.commit()
    db.session.flush()
    AgentExecutionConfiguration.add_or_update_agent_execution_config(session=db.session, execution=execution,
                                                                     agent_execution_configs=agent_execution_configs)

    organisation = db_agent.get_agent_organisation(db.session)
    EventHandler(session=db.session).create_event('agent_created', {'agent_name': agent_with_config.name,
                                                                    'model': agent_with_config.model}, db_agent.id,
                                                  organisation.id if organisation else 0)
    # execute_agent.delay(execution.id, datetime.now())
    db.session.commit()
    return {
        "agent_id": db_agent.id
    }

@router.post("/{agent_id}/run",status_code=200)
def create_run(agent_id:int,agent_execution: AgentExecutionIn,api_key: str = Security(validate_api_key),organisation:Organisation = Depends(get_organisation_from_api_key)):
    agent=Agent.get_agent_from_id(db.session,agent_id)
    if not agent:
        raise HTTPException(status_code=404, detail="Agent not found")
    project=Project.find_by_id(db.session, agent.project_id)
    if project.organisation_id!=organisation.id:
        raise HTTPException(status_code=404, detail="Agent not found")
    db_schedule=AgentSchedule.find_by_agent_id(db.session, agent_id)
    if db_schedule is not None:
        raise HTTPException(status_code=409, detail="Agent is already scheduled,cannot run")
    start_step = AgentWorkflow.fetch_trigger_step_id(db.session, agent.agent_workflow_id)
    db_agent_execution=AgentExecution.get_execution_by_agent_id_and_status(db.session, agent_id, "CREATED")

    if db_agent_execution is None:
        db_agent_execution = AgentExecution(status="RUNNING", last_execution_time=datetime.now(),
                                            agent_id=agent_id, name=agent_execution.name, num_of_calls=0,
                                            num_of_tokens=0,
                                            current_agent_step_id=start_step.id)
        db.session.add(db_agent_execution)
    else:
        db_agent_execution.status = "RUNNING"

    db.session.commit()
    db.session.flush()

    agent_execution_configs = {}
    if agent_execution.goal is not None:
        agent_execution_configs = {
            "goal": agent_execution.goal,
        }

    if agent_execution.instruction is not None:
        agent_execution_configs["instructions"] = agent_execution.instruction,

    if agent_execution_configs != {}:
        AgentExecutionConfiguration.add_or_update_agent_execution_config(session=db.session, execution=db_agent_execution,
                                                                     agent_execution_configs=agent_execution_configs)
    EventHandler(session=db.session).create_event('run_created', 
                                                  {'agent_execution_id': db_agent_execution.id,
                                                   'agent_execution_name':db_agent_execution.name
                                                   },
                                                   agent_id, 
                                                   organisation.id if organisation else 0)
    
    agent_execution_knowledge = AgentConfiguration.get_agent_config_by_key_and_agent_id(session= db.session, key= 'knowledge', agent_id= agent_id)
    if agent_execution_knowledge and agent_execution_knowledge.value != 'None':
        knowledge_name = Knowledges.get_knowledge_from_id(db.session, int(agent_execution_knowledge.value)).name
        if knowledge_name is not None:
            EventHandler(session=db.session).create_event('knowledge_picked', 
                                                        {'knowledge_name': knowledge_name, 
                                                         'agent_execution_id': db_agent_execution.id},
                                                        agent_id,
                                                        organisation.id if organisation else 0
                                                        )

    if db_agent_execution.status == "RUNNING":
      execute_agent.delay(db_agent_execution.id, datetime.now())
    return {
        "run_id":db_agent_execution.id
    }

@router.put("/{agent_id}",status_code=200)
def update_agent(agent_id: int, agent_with_config: AgentConfigUpdateExtInput,api_key: str = Security(validate_api_key),
                                        organisation:Organisation = Depends(get_organisation_from_api_key)):
    
    db_agent= Agent.get_active_agent_by_id(db.session, agent_id)
    if not db_agent:
        raise HTTPException(status_code=404, detail="agent not found")
    
    project=Project.find_by_id(db.session, db_agent.project_id)
    if project is None:
        raise HTTPException(status_code=404, detail="Project not found")
    
    if project.organisation_id!=organisation.id:
        raise HTTPException(status_code=404, detail="Agent not found")

    # db_execution=AgentExecution.get_execution_by_agent_id_and_status(db.session, agent_id, "RUNNING")
    # if db_execution is not None:
    #     raise HTTPException(status_code=409, detail="Agent is already running,please pause and then update")
     
    db_schedule=AgentSchedule.find_by_agent_id(db.session, agent_id)
    if db_schedule is not None:
        raise HTTPException(status_code=409, detail="Agent is already scheduled,cannot update")
    
    try:
        tools_arr=Toolkit.get_tool_and_toolkit_arr(db.session,organisation.id,agent_with_config.tools)
    except Exception as e:
        raise HTTPException(status_code=404,detail=str(e))

    if agent_with_config.schedule is not None:
        raise HTTPException(status_code=400,detail="Cannot schedule an existing agent")
    agent_with_config.tools=tools_arr
    agent_with_config.project_id=project.id
    agent_with_config.exit="No exit criterion"
    agent_with_config.permission_type="God Mode"
    agent_with_config.LTM_DB=None

    for key,value in agent_with_config.dict().items():
        if hasattr(db_agent,key) and value is not None:
            setattr(db_agent,key,value)
    db.session.commit()
    db.session.flush()

    start_step = AgentWorkflow.fetch_trigger_step_id(db.session, db_agent.agent_workflow_id)
    iteration_step_id = IterationWorkflow.fetch_trigger_step_id(db.session,
                                                                start_step.action_reference_id).id if start_step.action_type == "ITERATION_WORKFLOW" else -1
    execution = AgentExecution(status='CREATED', last_execution_time=datetime.now(), agent_id=db_agent.id,
                               name="New Run", current_agent_step_id=start_step.id, iteration_workflow_step_id=iteration_step_id)
    agent_execution_configs = {
        "goal": agent_with_config.goal,
        "instruction": agent_with_config.instruction,
        "tools":agent_with_config.tools,
        "constraints": agent_with_config.constraints,
        "iteration_interval": agent_with_config.iteration_interval,
        "model": agent_with_config.model,
        "max_iterations": agent_with_config.max_iterations,
        "agent_workflow": agent_with_config.agent_workflow,
    }
    agent_configurations = [
        AgentConfiguration(agent_id=db_agent.id, key=key, value=str(value))
        for key, value in agent_execution_configs.items()
    ]
    db.session.add_all(agent_configurations)
    db.session.add(execution)
    db.session.commit()
    db.session.flush()
    AgentExecutionConfiguration.add_or_update_agent_execution_config(session=db.session, execution=execution,
                                                                     agent_execution_configs=agent_execution_configs)
    db.session.commit()

    return {
        "agent_id":db_agent.id
    }


@router.post("/{agent_id}/run-status")
def get_agent_runs(agent_id:int,filter_config:RunFilterConfigIn,api_key: str = Security(validate_api_key),organisation:Organisation = Depends(get_organisation_from_api_key)):
    agent= Agent.get_active_agent_by_id(db.session, agent_id)
    if not agent:
        raise HTTPException(status_code=404, detail="Agent not found")
    
    project=Project.find_by_id(db.session, agent.project_id)
    if project.organisation_id!=organisation.id:
        raise HTTPException(status_code=404, detail="Agent not found")
    
    db_execution_arr=[]
    if filter_config.run_status_filter is not None:
        filter_config.run_status_filter=filter_config.run_status_filter.upper()

    db_execution_arr=AgentExecution.get_all_executions_by_filter_config(db.session, agent.id, filter_config)
    
    response_arr=[]
    for ind_execution in db_execution_arr:
        response_arr.append({"run_id":ind_execution.id, "status":ind_execution.status})

    return response_arr


@router.post("/{agent_id}/pause",status_code=200)
def pause_agent_runs(agent_id:int,execution_state_change_input:ExecutionStateChangeConfigIn,api_key: str = Security(validate_api_key),organisation:Organisation = Depends(get_organisation_from_api_key)):
    agent= Agent.get_active_agent_by_id(db.session, agent_id)
    if not agent:
        raise HTTPException(status_code=404, detail="Agent not found")
    
    project=Project.find_by_id(db.session, agent.project_id)
    if project.organisation_id!=organisation.id:
        raise HTTPException(status_code=404, detail="Agent not found")
    
    #Checking if the run_ids whose output files are requested belong to the organisation 
    if execution_state_change_input.run_ids is not None:
        try:
            AgentExecution.validate_run_ids(db.session,execution_state_change_input.run_ids,organisation.id)
        except Exception as e:
            raise HTTPException(status_code=404, detail="One or more run id(s) not found")
    
    db_execution_arr=AgentExecution.get_all_executions_by_status_and_agent_id(db.session, agent.id, execution_state_change_input, "RUNNING")

    if db_execution_arr is not None and execution_state_change_input.run_ids is not None \
            and len(db_execution_arr) != len(execution_state_change_input.run_ids):
        raise HTTPException(status_code=404, detail="One or more run id(s) not found")

    for ind_execution in db_execution_arr:
        ind_execution.status="PAUSED"
    db.session.commit()
    db.session.flush()
    return {
        "result":"success"
    }

@router.post("/{agent_id}/resume",status_code=200)
def resume_agent_runs(agent_id:int,execution_state_change_input:ExecutionStateChangeConfigIn,api_key: str = Security(validate_api_key),organisation:Organisation = Depends(get_organisation_from_api_key)):
    agent= Agent.get_active_agent_by_id(db.session, agent_id)
    if not agent:
        raise HTTPException(status_code=404, detail="Agent not found")
    
    project=Project.find_by_id(db.session, agent.project_id)
    if project.organisation_id!=organisation.id:
        raise HTTPException(status_code=404, detail="Agent not found")
    
    if execution_state_change_input.run_ids is not None:
        try:
            AgentExecution.validate_run_ids(db.session,execution_state_change_input.run_ids,organisation.id)
        except Exception as e:
            raise HTTPException(status_code=404, detail="One or more run id(s) not found")
    
    db_execution_arr=AgentExecution.get_all_executions_by_status_and_agent_id(db.session, agent.id, execution_state_change_input, "PAUSED")

    if db_execution_arr is not None and execution_state_change_input.run_ids is not None\
            and len(db_execution_arr) != len(execution_state_change_input.run_ids):
        raise HTTPException(status_code=404, detail="One or more run id(s) not found")

    for ind_execution in db_execution_arr:
        ind_execution.status="RUNNING"
        execute_agent.delay(ind_execution.id, datetime.now())
        
    db.session.commit()
    db.session.flush()

    return {
        "result":"success"
    }

@router.post("/resources/output",status_code=200)
def get_run_resources(run_id_config:RunIDConfig,api_key: str = Security(validate_api_key),organisation:Organisation = Depends(get_organisation_from_api_key)):
    if get_config('STORAGE_TYPE') != "S3":
        raise HTTPException(status_code=400,detail="This endpoint only works when S3 is configured")
    run_ids_arr=run_id_config.run_ids
    if len(run_ids_arr)==0:  
        raise HTTPException(status_code=404,
                            detail=f"No execution_id found")
    #Checking if the run_ids whose output files are requested belong to the organisation 
    try:
        AgentExecution.validate_run_ids(db.session, run_ids_arr, organisation.id)
    except Exception as e:
        raise HTTPException(status_code=404, detail="One or more run id(s) not found")
    
    db_resources_arr=Resource.find_by_run_ids(db.session, run_ids_arr)

    try:
        response_obj=S3Helper().get_download_url_of_resources(db_resources_arr)
    except:
        raise HTTPException(status_code=401,detail="Invalid S3 credentials")
    return response_obj

