import json

from sqlalchemy import Column, Integer, String
from sqlalchemy.dialects.postgresql import JSONB

from superagi.lib.logger import logger
from superagi.models.base_model import DBBaseModel
from superagi.models.workflows.agent_workflow_step_tool import AgentWorkflowStepTool
from superagi.models.workflows.agent_workflow_step_wait import AgentWorkflowStepWait
from superagi.models.workflows.iteration_workflow import IterationWorkflow


class AgentWorkflowStep(DBBaseModel):
    """
    Step of an agent workflow

    Attributes:
        id (int): The unique identifier of the agent workflow step.
        agent_workflow_id (int): The ID of the agent workflow to which this step belongs.
        unique_id (str): The unique identifier of the step.
        step_type (str): The type of the step (TRIGGER, NORMAL).
        action_type (str): The type of the action (TOOL, ITERATION_WORKFLOW, LLM).
        action_reference_id: Reference id of the tool/iteration workflow/llm
        next_steps: Next steps output and step id.
    """

    __tablename__ = 'agent_workflow_steps'

    id = Column(Integer, primary_key=True)
    agent_workflow_id = Column(Integer)
    unique_id = Column(String)
    step_type = Column(String)  # TRIGGER, NORMAL
    action_type = Column(String)  # TOOL, ITERATION_WORKFLOW, LLM, WAIT_STEP
    action_reference_id = Column(Integer)  # id of the action
    next_steps = Column(JSONB)  # edge_ref_id, response, step_id

    def __repr__(self):
        """
        Returns a string representation of the AgentWorkflowStep object.

        Returns:
            str: String representation of the AgentWorkflowStep.
        """

        return f"AgentWorkflowStep(id={self.id}, status='{self.agent_workflow_id}', " \
               f"prompt='{self.unique_id}', agent_id={self.step_type}, action_type={self.action_type}, " \
               f"action_reference_id={self.action_reference_id}, next_steps={self.next_steps})"

    def to_dict(self):
        """
        Converts the AgentWorkflowStep object to a dictionary.

        Returns:
            dict: Dictionary representation of the AgentWorkflowStep.
        """

        return {
            'id': self.id,
            'agent_workflow_id': self.agent_workflow_id,
            'unique_id': self.unique_id,
            'step_type': self.step_type,
            'next_steps': self.next_steps,
            'action_type': self.action_type,
            'action_reference_id': self.action_reference_id
        }

    def to_json(self):
        """
        Converts the AgentWorkflowStep object to a JSON string.

        Returns:
            str: JSON string representation of the AgentWorkflowStep.
        """

        return json.dumps(self.to_dict())

    @classmethod
    def from_json(cls, json_data):
        """
        Creates an AgentWorkflowStep object from a JSON string.

        Args:
            json_data (str): JSON string representing the AgentWorkflowStep.

        Returns:
            AgentWorkflowStep: AgentWorkflowStep object created from the JSON string.
        """

        data = json.loads(json_data)
        return cls(
            id=data['id'],
            agent_workflow_id=data['agent_workflow_id'],
            unique_id=data['unique_id'],
            step_type=data['step_type'],
            action_type=data['action_type'],
            action_reference_id=data['action_reference_id'],
            next_steps=data['next_steps'],
        )

    @classmethod
    def find_by_unique_id(cls, session, unique_id: str):
        """ Adds a workflows step in the next_steps column"""
        return session.query(AgentWorkflowStep).filter(AgentWorkflowStep.unique_id == unique_id).first()

    @classmethod
    def find_by_id(cls, session, step_id: int):
        """ Find the workflow step by id"""
        return session.query(AgentWorkflowStep).filter(AgentWorkflowStep.id == step_id).first()

    @classmethod
    def find_or_create_tool_workflow_step(cls, session, agent_workflow_id: int, unique_id: str,
                                          tool_name: str, input_instruction: str,
                                          output_instruction: str = "", step_type="NORMAL",
                                          history_enabled: bool = True, completion_prompt: str = None):
        """ Find or create a tool workflow step

        Args:
            session: db session
            agent_workflow_id: id of the agent workflow
            unique_id: unique id of the step
            tool_name: name of the tool
            input_instruction: input instruction of the tool
            output_instruction: output instruction of the tool
            step_type: type of the step
            history_enabled: whether to enable history for the step
            completion_prompt: completion prompt in the llm

        Returns:
            AgentWorkflowStep.
        """
        workflow_step = session.query(AgentWorkflowStep).filter(
            AgentWorkflowStep.agent_workflow_id == agent_workflow_id, AgentWorkflowStep.unique_id == unique_id).first()
        if completion_prompt is None:
            completion_prompt = f"Respond with only valid JSON conforming to the given json schema. Response should contain tool name and tool arguments to achieve the given instruction."
        step_tool = AgentWorkflowStepTool.find_or_create_tool(session, unique_id, tool_name,
                                                              input_instruction, output_instruction,
                                                              history_enabled, completion_prompt)

        if workflow_step is None:
            workflow_step = AgentWorkflowStep(unique_id=unique_id, step_type=step_type,
                                              agent_workflow_id=agent_workflow_id)
            session.add(workflow_step)
            session.commit()
        workflow_step.step_type = step_type
        workflow_step.agent_workflow_id = agent_workflow_id
        workflow_step.action_reference_id = step_tool.id
        workflow_step.action_type = "TOOL"
        workflow_step.next_steps = []
        workflow_step.completion_prompt = completion_prompt
        session.commit()
        return workflow_step

    @classmethod
    def find_or_create_wait_workflow_step(cls, session, agent_workflow_id: int, unique_id: str,
                                          wait_description: str, delay: int, step_type="NORMAL"):
        """ Find or create a wait workflow step"""
        logger.info("Finding or creating wait step")
        workflow_step = session.query(AgentWorkflowStep).filter(
            AgentWorkflowStep.agent_workflow_id == agent_workflow_id, AgentWorkflowStep.unique_id == unique_id).first()

        step_wait = AgentWorkflowStepWait.find_or_create_wait(session=session, step_unique_id=unique_id,
                                                              description=wait_description, delay=delay)
        if workflow_step is None:
            workflow_step = AgentWorkflowStep(unique_id=unique_id, step_type=step_type,
                                              agent_workflow_id=agent_workflow_id)
            session.add(workflow_step)
            session.commit()
        workflow_step.step_type = step_type
        workflow_step.agent_workflow_id = agent_workflow_id
        workflow_step.action_reference_id = step_wait.id
        workflow_step.action_type = "WAIT_STEP"
        workflow_step.next_steps = []
        session.commit()
        return workflow_step

    @classmethod
    def find_or_create_iteration_workflow_step(cls, session, agent_workflow_id: int, unique_id: str,
                                               iteration_workflow_name: str, step_type="NORMAL"):
        """ Find or create a iteration workflow step

        Args:
            session: db session
            agent_workflow_id: id of the agent workflow
            unique_id: unique id of the step
            iteration_workflow_name: name of the iteration workflow
            step_type: type of the step

        Returns:
            AgentWorkflowStep.
        """
        workflow_step = session.query(AgentWorkflowStep).filter(
            AgentWorkflowStep.agent_workflow_id == agent_workflow_id, AgentWorkflowStep.unique_id == unique_id).first()

        iteration_workflow = IterationWorkflow.find_workflow_by_name(session, iteration_workflow_name)

        if workflow_step is None:
            workflow_step = AgentWorkflowStep(unique_id=unique_id, step_type=step_type,
                                              agent_workflow_id=agent_workflow_id)
            session.add(workflow_step)
            session.commit()
        workflow_step.step_type = step_type
        workflow_step.agent_workflow_id = agent_workflow_id
        workflow_step.action_reference_id = iteration_workflow.id
        workflow_step.action_type = "ITERATION_WORKFLOW"
        workflow_step.next_steps = []
        session.commit()
        return workflow_step

    @classmethod
    def add_next_workflow_step(cls, session, current_agent_step_id: int, next_step_id: int,
                               step_response: str = "default"):
        """ Add Next workflow steps in the next_steps column

        Args:
            session: db session
            current_agent_step_id: id of the current agent step
            next_step_id: id of the next agent step
            step_response: response of the current step

        """
        next_unique_id = "-1"
        if next_step_id != -1:
            next_workflow_step = AgentWorkflowStep.find_by_id(session, next_step_id)
            next_unique_id = next_workflow_step.unique_id
        current_step = session.query(AgentWorkflowStep).filter(AgentWorkflowStep.id == current_agent_step_id).first()
        next_steps = json.loads(json.dumps(current_step.next_steps))
        existing_steps = [step for step in next_steps if step["step_id"] == next_unique_id]
        if existing_steps:
            existing_steps[0]["step_response"] = step_response
            current_step.next_steps = next_steps
        else:
            next_steps.append({"step_response": str(step_response), "step_id": str(next_unique_id)})
            current_step.next_steps = next_steps
        session.commit()
        return current_step

    @classmethod
    def fetch_default_next_step(cls, session, current_agent_step_id: int):
        """ Fetches the default next step

        Args:
            session: db session
            current_agent_step_id: id of the current agent step
        """
        current_step = AgentWorkflowStep.find_by_id(session, current_agent_step_id)
        next_steps = current_step.next_steps
        default_steps = [step for step in next_steps if step["step_response"] == "default"]
        if default_steps:
            return AgentWorkflowStep.find_by_unique_id(session, default_steps[0]["step_id"])
        return None

    @classmethod
    def fetch_next_step(cls, session, current_agent_step_id: int, step_response: str):
        """ Fetch the next step based on the step response

        Args:
            session: db session
            current_agent_step_id: id of the current agent step
            step_response: response of the current step
        """
        current_step = AgentWorkflowStep.find_by_id(session, current_agent_step_id)
        next_steps = current_step.next_steps
        matching_steps = [step for step in next_steps if str(step["step_response"]).lower() == step_response.lower()]

        if matching_steps:
            if str(matching_steps[0]["step_id"]) == "-1":
                return "COMPLETE"
            return AgentWorkflowStep.find_by_unique_id(session, matching_steps[0]["step_id"])

        logger.info(f"Could not find next step for step_id: {current_agent_step_id} and step_response: {step_response}")
        default_steps = [step for step in next_steps if str(step["step_response"]).lower() == "default"]

        if default_steps:
            if str(default_steps[0]["step_id"]) == "-1":
                return "COMPLETE"
            return AgentWorkflowStep.find_by_unique_id(session, default_steps[0]["step_id"])
        return None
