import json
from uuid import uuid4

from pydantic import BaseModel, Field

from core.agents.base import BaseAgent
from core.agents.convo import AgentConvo
from core.agents.mixins import RelevantFilesMixin
from core.agents.response import AgentResponse
from core.config import TECH_LEAD_EPIC_BREAKDOWN, TECH_LEAD_PLANNING
from core.db.models import Complexity
from core.db.models.project_state import TaskStatus
from core.llm.parser import JSONParser
from core.log import get_logger
from core.telemetry import telemetry
from core.templates.registry import PROJECT_TEMPLATES
from core.ui.base import ProjectStage, pythagora_source, success_source

log = get_logger(__name__)


class APIEndpoint(BaseModel):
    description: str = Field(description="Description of an API endpoint.")
    method: str = Field(description="HTTP method of the API endpoint.")
    endpoint: str = Field(description="URL of the API endpoint.")
    request_body: dict = Field(description="Request body of the API endpoint.")
    response_body: dict = Field(description="Response body of the API endpoint.")


class Epic(BaseModel):
    description: str = Field(description="Description of an epic.")


class Task(BaseModel):
    description: str = Field(description="Description of a task.")
    related_api_endpoints: list[APIEndpoint] = Field(description="API endpoints that will be implemented in this task.")
    testing_instructions: str = Field(description="Instructions for testing the task.")


class DevelopmentPlan(BaseModel):
    plan: list[Epic] = Field(description="List of epics that need to be done to implement the entire plan.")


class EpicPlan(BaseModel):
    plan: list[Task] = Field(description="List of tasks that need to be done to implement the entire epic.")


class TechLead(RelevantFilesMixin, BaseAgent):
    agent_type = "tech-lead"
    display_name = "Tech Lead"

    async def run(self) -> AgentResponse:
        # Building frontend is the first epic
        if len(self.current_state.epics) == 1:
            self.create_initial_project_epic()
            return AgentResponse.done(self)

        # if self.current_state.specification.templates and len(self.current_state.files) < 2:
        #     await self.apply_project_templates()
        #     self.next_state.action = "Apply project templates"
        #     await self.ui.send_epics_and_tasks(
        #         self.next_state.current_epic["sub_epics"],
        #         self.next_state.tasks,
        #     )
        #
        #     inputs = []
        #     for file in self.next_state.files:
        #         input_required = self.state_manager.get_input_required(file.content.content)
        #         if input_required:
        #             inputs += [{"file": file.path, "line": line} for line in input_required]
        #
        #     if inputs:
        #         return AgentResponse.input_required(self, inputs)
        #     else:
        #         return AgentResponse.done(self)

        if self.current_state.current_epic:
            self.next_state.action = "Create a development plan"
            await self.remove_mocked_data()
            return await self.plan_epic(self.current_state.current_epic)
        else:
            return await self.ask_for_new_feature()

    def create_initial_project_epic(self):
        log.debug("Creating initial project Epic")
        self.next_state.epics = self.current_state.epics + [
            {
                "id": uuid4().hex,
                "name": "Initial Project",
                "source": "app",
                "description": self.current_state.specification.description,
                "test_instructions": None,
                "summary": None,
                "completed": False,
                "complexity": self.current_state.specification.complexity,
                "sub_epics": [],
            }
        ]
        self.next_state.relevant_files = None
        self.next_state.modified_files = {}

    async def apply_project_templates(self):
        state = self.current_state
        summaries = []

        # Only do this for the initial project and if the templates are specified
        if len(state.epics) != 1 or not state.specification.templates:
            return

        for template_name, template_options in state.specification.templates.items():
            template_class = PROJECT_TEMPLATES.get(template_name)
            if not template_class:
                log.error(f"Project template not found: {template_name}")
                continue

            template = template_class(
                template_options,
                self.state_manager,
                self.process_manager,
            )

            description = template.description
            log.info(f"Applying project template: {template.name}")
            await self.send_message(f"Applying project template {description} ...")
            summary = await template.apply()
            summaries.append(summary)

        # Saving template files will fill this in and we want it clear for the first task.
        self.next_state.relevant_files = None

        if summaries:
            spec = self.current_state.specification.clone()
            spec.template_summary = "\n\n".join(summaries)

            self.next_state.specification = spec

    async def ask_for_new_feature(self) -> AgentResponse:
        if len(self.current_state.epics) > 2:
            await self.ui.send_message("Your new feature is complete!", source=success_source)
        else:
            await self.ui.send_message("Your app is DONE! You can start using it right now!", source=success_source)

        if self.current_state.run_command:
            await self.ui.send_run_command(self.current_state.run_command)

        log.debug("Asking for new feature")
        response = await self.ask_question(
            "Do you have a new feature to add to the project? Just write it here:",
            buttons={"continue": "continue", "end": "No, I'm done"},
            allow_empty=False,
            extra_info="restart_app",
        )

        if response.button == "end" or response.cancelled or not response.text:
            await self.ui.send_message("Thank you for using Pythagora!", source=pythagora_source)
            return AgentResponse.exit(self)

        feature_description = response.text
        self.next_state.epics = self.current_state.epics + [
            {
                "id": uuid4().hex,
                "name": f"Feature #{len(self.current_state.epics)}",
                "test_instructions": None,
                "source": "feature",
                "description": feature_description,
                "summary": None,
                "completed": False,
                "complexity": None,  # Determined and defined in SpecWriter
                "sub_epics": [],
            }
        ]
        # Orchestrator will rerun us to break down the new feature epic
        self.next_state.action = f"Start of feature #{len(self.current_state.epics)}"
        return AgentResponse.update_specification(self, feature_description)

    async def plan_epic(self, epic) -> AgentResponse:
        log.debug(f"Planning tasks for the epic: {epic['name']}")
        await self.send_message("Creating the development plan ...")

        if epic.get("source") == "feature":
            await self.get_relevant_files(user_feedback=epic.get("description"))

        llm = self.get_llm(TECH_LEAD_PLANNING)
        convo = (
            AgentConvo(self)
            .template(
                "plan",
                epic=epic,
                task_type=self.current_state.current_epic.get("source", "app"),
                # FIXME: we're injecting summaries to initial description
                existing_summary=None,
                get_only_api_files=True,
            )
            .require_schema(DevelopmentPlan)
        )

        response: DevelopmentPlan = await llm(convo, parser=JSONParser(DevelopmentPlan))

        convo.remove_last_x_messages(1)
        formatted_epics = [f"Epic #{index}: {epic.description}" for index, epic in enumerate(response.plan, start=1)]
        epics_string = "\n\n".join(formatted_epics)
        convo = convo.assistant(epics_string)
        llm = self.get_llm(TECH_LEAD_EPIC_BREAKDOWN)

        if epic.get("source") == "feature" or epic.get("complexity") == Complexity.SIMPLE:
            await self.send_message(f"Epic 1: {epic['name']}")
            self.next_state.current_epic["sub_epics"] = [
                {
                    "id": 1,
                    "description": epic["name"],
                }
            ]
            await self.send_message("Creating tasks for this epic ...")
            self.next_state.tasks = self.next_state.tasks + [
                {
                    "id": uuid4().hex,
                    "description": task.description,
                    "instructions": None,
                    "pre_breakdown_testing_instructions": None,
                    "status": TaskStatus.TODO,
                    "sub_epic_id": 1,
                }
                for task in response.plan
            ]
        else:
            self.next_state.current_epic["sub_epics"] = [
                {
                    "id": sub_epic_number,
                    "description": sub_epic.description,
                }
                for sub_epic_number, sub_epic in enumerate(response.plan, start=1)
            ]
            for sub_epic_number, sub_epic in enumerate(response.plan, start=1):
                await self.send_message(f"Epic {sub_epic_number}: {sub_epic.description}")
                convo = convo.template(
                    "epic_breakdown",
                    epic_number=sub_epic_number,
                    epic_description=sub_epic.description,
                    get_only_api_files=True,
                ).require_schema(EpicPlan)
                await self.send_message("Creating tasks for this epic ...")
                epic_plan: EpicPlan = await llm(convo, parser=JSONParser(EpicPlan))
                self.next_state.tasks = self.next_state.tasks + [
                    {
                        "id": uuid4().hex,
                        "description": task.description,
                        "instructions": None,
                        "pre_breakdown_testing_instructions": task.testing_instructions,
                        "status": TaskStatus.TODO,
                        "sub_epic_id": sub_epic_number,
                        "related_api_endpoints": [rae.model_dump() for rae in (task.related_api_endpoints or [])],
                    }
                    for task in epic_plan.plan
                ]
                convo.remove_last_x_messages(2)

        await self.ui.send_epics_and_tasks(
            self.next_state.current_epic["sub_epics"],
            self.next_state.tasks,
        )

        await self.ui.send_project_stage({"stage": ProjectStage.OPEN_PLAN})
        response = await self.ask_question(
            "Open and edit your development plan in the Progress tab",
            buttons={"done_editing": "I'm done editing, the plan looks good"},
            default="done_editing",
            buttons_only=True,
            extra_info="edit_plan",
        )

        self.update_epics_and_tasks(response.text)

        await self.ui.send_epics_and_tasks(
            self.next_state.current_epic["sub_epics"],
            self.next_state.tasks,
        )

        await telemetry.trace_code_event(
            "development-plan",
            {
                "num_tasks": len(self.current_state.tasks),
                "num_epics": len(self.current_state.epics),
            },
        )

        return AgentResponse.done(self)

    # TODO - Move to a separate agent for removing mocked data
    async def remove_mocked_data(self):
        files = self.current_state.files
        for file in files:
            file_content = file.content.content
            if "pythagora_mocked_data" in file_content:
                for line in file_content.split("\n"):
                    if "pythagora_mocked_data" in line:
                        file_content = file_content.replace(line + "\n", "")
                file.content.content = file_content
                await self.state_manager.save_file(file.path, file_content)

    def update_epics_and_tasks(self, edited_plan_string):
        edited_plan = json.loads(edited_plan_string)
        updated_tasks = []

        existing_tasks_map = {task["description"]: task for task in self.next_state.tasks}

        self.next_state.current_epic["sub_epics"] = []
        for sub_epic_number, sub_epic in enumerate(edited_plan, start=1):
            self.next_state.current_epic["sub_epics"].append(
                {
                    "id": sub_epic_number,
                    "description": sub_epic["description"],
                }
            )

            for task in sub_epic["tasks"]:
                original_task = existing_tasks_map.get(task["description"])
                if original_task and task == original_task:
                    updated_task = original_task.copy()
                    updated_task["sub_epic_id"] = sub_epic_number
                    updated_tasks.append(updated_task)
                else:
                    updated_tasks.append(
                        {
                            "id": uuid4().hex,
                            "description": task["description"],
                            "instructions": None,
                            "pre_breakdown_testing_instructions": None,
                            "status": TaskStatus.TODO,
                            "sub_epic_id": sub_epic_number,
                        }
                    )

        self.next_state.tasks = updated_tasks
