from __future__ import annotations

import json
import platform
import re
from logging import Logger

import distro
from pydantic import Field

from forge.config.ai_directives import AIDirectives
from forge.config.ai_profile import AIProfile
from forge.json.parsing import extract_dict_from_json
from forge.llm.prompting import ChatPrompt, LanguageModelClassification, PromptStrategy
from forge.llm.prompting.utils import format_numbered_list
from forge.llm.providers.schema import (
    AssistantChatMessage,
    ChatMessage,
    CompletionModelFunction,
)
from forge.models.action import ActionProposal
from forge.models.config import SystemConfiguration, UserConfigurable
from forge.models.json_schema import JSONSchema
from forge.models.utils import ModelWithSummary
from forge.utils.exceptions import InvalidAgentResponseError

_RESPONSE_INTERFACE_NAME = "AssistantResponse"


class AssistantThoughts(ModelWithSummary):
    observations: str = Field(
        description="Relevant observations from your last action (if any)"
    )
    reasoning: str = Field(description="Reasoning behind choosing this action")
    self_criticism: str = Field(description="Constructive self-criticism")
    plan: list[str] = Field(description="Short list that conveys the long-term plan")

    def summary(self) -> str:
        return self.reasoning


class OneShotAgentActionProposal(ActionProposal):
    thoughts: AssistantThoughts  # type: ignore


class OneShotAgentPromptConfiguration(SystemConfiguration):
    DEFAULT_BODY_TEMPLATE: str = (
        "## Constraints\n"
        "You operate within the following constraints:\n"
        "{constraints}\n"
        "\n"
        "## Resources\n"
        "You can leverage access to the following resources:\n"
        "{resources}\n"
        "\n"
        "## Commands\n"
        "These are the ONLY commands you can use."
        " Any action you perform must be possible through one of these commands:\n"
        "{commands}\n"
        "\n"
        "## Best practices\n"
        "{best_practices}\n"
        "\n"
        "## Efficiency Guidelines\n"
        "You have LIMITED steps. Be efficient:\n\n"
        "1. UNDERSTAND BEFORE ACTING: Read ALL relevant files before making changes. "
        "Understand requirements, interfaces, and existing code patterns first.\n\n"
        "2. PARALLEL EXECUTION: When multiple operations don't depend on each other, "
        "execute them simultaneously (e.g., read multiple files at once).\n\n"
        "3. WRITE COMPLETE CODE: Write complete, working implementations. "
        "No stubs, TODOs, or placeholders.\n\n"
        "4. VERIFY AFTER CHANGES: After modifying code, verify it works. "
        "Run available linters/formatters/tests if available.\n\n"
        "5. FIX ROOT CAUSE: When debugging, fix the underlying issue, not symptoms. "
        "If a test fails, the bug is in your code, NOT in the test.\n\n"
        "6. CODE STYLE: Mimic existing code conventions. "
        "Don't add comments unless the logic is genuinely complex.\n\n"
        "7. SECURITY: Never expose, log, or commit secrets, API keys, or credentials."
    )

    DEFAULT_CHOOSE_ACTION_INSTRUCTION: str = (
        "Determine exactly one command to use next based on the given goals "
        "and the progress you have made so far, "
        "and respond using the JSON schema specified previously.\n\n"
        "PARALLEL EXECUTION: When multiple operations don't depend on each other, "
        "you may call multiple independent commands simultaneously. For example:\n"
        "- Read multiple files at once before making changes\n"
        "- Execute multiple independent writes\n"
        "- Run multiple search queries in parallel"
    )

    body_template: str = UserConfigurable(default=DEFAULT_BODY_TEMPLATE)
    choose_action_instruction: str = UserConfigurable(
        default=DEFAULT_CHOOSE_ACTION_INSTRUCTION
    )
    use_prefill: bool = True

    #########
    # State #
    #########
    # progress_summaries: dict[tuple[int, int], str] = Field(
    #     default_factory=lambda: {(0, 0): ""}
    # )


class OneShotAgentPromptStrategy(PromptStrategy):
    default_configuration: OneShotAgentPromptConfiguration = (
        OneShotAgentPromptConfiguration()
    )

    def __init__(
        self,
        configuration: OneShotAgentPromptConfiguration,
        logger: Logger,
    ):
        self.config = configuration
        self.response_schema = JSONSchema.from_dict(
            OneShotAgentActionProposal.model_json_schema()
        )
        self.logger = logger

    @property
    def llm_classification(self) -> LanguageModelClassification:
        return LanguageModelClassification.FAST_MODEL  # FIXME: dynamic switching

    def build_prompt(
        self,
        *,
        messages: list[ChatMessage],
        task: str,
        ai_profile: AIProfile,
        ai_directives: AIDirectives,
        commands: list[CompletionModelFunction],
        include_os_info: bool,
        **extras,
    ) -> ChatPrompt:
        """Constructs and returns a prompt with the following structure:
        1. System prompt
        3. `cycle_instruction`
        """
        system_prompt, response_prefill = self.build_system_prompt(
            ai_profile=ai_profile,
            ai_directives=ai_directives,
            commands=commands,
            include_os_info=include_os_info,
        )

        final_instruction_msg = ChatMessage.user(self.config.choose_action_instruction)

        return ChatPrompt(
            messages=[
                ChatMessage.system(system_prompt),
                ChatMessage.user(f'"""{task}"""'),
                *messages,
                final_instruction_msg,
            ],
            prefill_response=response_prefill if self.config.use_prefill else "",
            functions=commands,
        )

    def build_system_prompt(
        self,
        ai_profile: AIProfile,
        ai_directives: AIDirectives,
        commands: list[CompletionModelFunction],
        include_os_info: bool,
    ) -> tuple[str, str]:
        """
        Builds the system prompt.

        Returns:
            str: The system prompt body
            str: The desired start for the LLM's response; used to steer the output
        """
        response_fmt_instruction, response_prefill = self.response_format_instruction()
        system_prompt_parts = (
            self._generate_intro_prompt(ai_profile)
            + (self._generate_os_info() if include_os_info else [])
            + [
                self.config.body_template.format(
                    constraints=format_numbered_list(ai_directives.constraints),
                    resources=format_numbered_list(ai_directives.resources),
                    commands=self._generate_commands_list(commands),
                    best_practices=format_numbered_list(ai_directives.best_practices),
                )
            ]
            + [
                "## Your Task\n"
                "The user will specify a task for you to execute, in triple quotes,"
                " in the next message. Your job is to complete the task while following"
                " your directives as given above, and terminate when done.\n\n"
                "For coding tasks: Read ALL files first. Tests define correct "
                "behavior - follow them exactly, even if it differs from intuition."
            ]
            + ["## RESPONSE FORMAT\n" + response_fmt_instruction]
        )

        # Join non-empty parts together into paragraph format
        return (
            "\n\n".join(filter(None, system_prompt_parts)).strip("\n"),
            response_prefill,
        )

    def response_format_instruction(self) -> tuple[str, str]:
        response_schema = self.response_schema.model_copy(deep=True)
        assert response_schema.properties
        # use_tool comes from tool_calls, so remove from JSON schema
        if "use_tool" in response_schema.properties:
            del response_schema.properties["use_tool"]

        # Unindent for performance
        response_format = re.sub(
            r"\n\s+",
            "\n",
            response_schema.to_typescript_object_interface(_RESPONSE_INTERFACE_NAME),
        )
        response_prefill = f'{{\n    "{list(response_schema.properties.keys())[0]}":'

        return (
            (
                f"YOU MUST ALWAYS RESPOND WITH A JSON OBJECT OF THE FOLLOWING TYPE:\n"
                f"{response_format}"
                "\n\nYOU MUST ALSO INVOKE A TOOL!"
            ),
            response_prefill,
        )

    def _generate_intro_prompt(self, ai_profile: AIProfile) -> list[str]:
        """Generates the introduction part of the prompt.

        Returns:
            list[str]: A list of strings forming the introduction part of the prompt.
        """
        return [
            f"You are {ai_profile.ai_name}, {ai_profile.ai_role.rstrip('.')}.",
            "Make decisions independently. Only use ask_user when you truly need "
            "clarification that cannot be inferred from the task or context. "
            "Play to your strengths as an LLM and pursue simple strategies.",
        ]

    def _generate_os_info(self) -> list[str]:
        """Generates the OS information part of the prompt.

        Params:
            config (Config): The configuration object.

        Returns:
            str: The OS information part of the prompt.
        """
        os_name = platform.system()
        os_info = (
            platform.platform(terse=True)
            if os_name != "Linux"
            else distro.name(pretty=True)
        )
        return [f"The OS you are running on is: {os_info}"]

    def _generate_commands_list(self, commands: list[CompletionModelFunction]) -> str:
        """Lists the commands available to the agent.

        Params:
            agent: The agent for which the commands are being listed.

        Returns:
            str: A string containing a numbered list of commands.
        """
        try:
            return format_numbered_list([cmd.fmt_line() for cmd in commands])
        except AttributeError:
            self.logger.warning(f"Formatting commands failed. {commands}")
            raise

    def parse_response_content(
        self,
        response: AssistantChatMessage,
    ) -> OneShotAgentActionProposal:
        if not response.content:
            # Some models (e.g. GPT-5) return tool_calls without text content.
            # Use a minimal thoughts dict so we can still proceed.
            if response.tool_calls:
                self.logger.info(
                    "Assistant response has no text content but has tool_calls; "
                    "using default thoughts"
                )
                assistant_reply_dict: dict = {
                    "thoughts": {
                        "observations": "",
                        "text": "",
                        "reasoning": "",
                        "self_criticism": "",
                        "plan": [],
                        "speak": "",
                    }
                }
            else:
                raise InvalidAgentResponseError(
                    "Assistant response has no text content"
                )
        else:
            self.logger.debug(
                "LLM response content:"
                + (
                    f"\n{response.content}"
                    if "\n" in response.content
                    else f" '{response.content}'"
                )
            )
            assistant_reply_dict = extract_dict_from_json(response.content)
            self.logger.debug(
                "Parsing object extracted from LLM response:\n"
                f"{json.dumps(assistant_reply_dict, indent=4)}"
            )

        # Always expect tool calls - native tool calling is always enabled
        if not response.tool_calls:
            raise InvalidAgentResponseError("Assistant did not use a tool")
        assistant_reply_dict["use_tool"] = response.tool_calls[0].function
        # Capture all tool calls for parallel execution
        if len(response.tool_calls) > 1:
            assistant_reply_dict["use_tools"] = [
                tc.function for tc in response.tool_calls
            ]

        parsed_response = OneShotAgentActionProposal.model_validate(
            assistant_reply_dict
        )
        parsed_response.raw_message = response.model_copy()
        return parsed_response
