"""
AUTARCH Agent System
Autonomous agent that uses LLM to accomplish tasks with tools
"""

import re
import json
from typing import Optional, List, Dict, Any, Callable
from dataclasses import dataclass, field
from enum import Enum

from .llm import get_llm, LLM, LLMError
from .tools import get_tool_registry, ToolRegistry
from .banner import Colors


class AgentState(Enum):
    """Agent execution states."""
    IDLE = "idle"
    THINKING = "thinking"
    EXECUTING = "executing"
    WAITING_USER = "waiting_user"
    COMPLETE = "complete"
    ERROR = "error"


@dataclass
class AgentStep:
    """Record of a single agent step."""
    thought: str
    tool_name: Optional[str] = None
    tool_args: Optional[Dict[str, Any]] = None
    tool_result: Optional[str] = None
    error: Optional[str] = None


@dataclass
class AgentResult:
    """Result of an agent task execution."""
    success: bool
    summary: str
    steps: List[AgentStep] = field(default_factory=list)
    error: Optional[str] = None


class Agent:
    """Autonomous agent that uses LLM and tools to accomplish tasks."""

    SYSTEM_PROMPT = """You are Hal, the autonomous agent for AUTARCH (darkHal Security Group).

RULES:
- No markdown. No ASCII art. Plain text only.
- Detect the OS before running commands. Run "uname -s" first if unsure.
- Only run commands for the detected OS. Never list commands for multiple platforms.
- On Linux: detect the distro first (cat /etc/os-release). Use apt for Debian/Ubuntu, dnf for Fedora, etc.
- Never prefix commands with sudo. The system handles root access automatically via the daemon.
- Run ONE command at a time. Check the result before continuing.
- Keep responses short. No filler.

FORMAT — you MUST use this exact format for every response:

THOUGHT: your reasoning
ACTION: tool_name
PARAMS: {{"param": "value"}}

When done:
THOUGHT: summary
ACTION: task_complete
PARAMS: {{"summary": "what was done"}}

When you need input:
THOUGHT: why you need to ask
ACTION: ask_user
PARAMS: {{"question": "your question"}}

{tools_description}
"""

    def __init__(
        self,
        llm: LLM = None,
        tools: ToolRegistry = None,
        max_steps: int = 20,
        verbose: bool = True
    ):
        """Initialize the agent.

        Args:
            llm: LLM instance to use. Uses global if not provided.
            tools: Tool registry to use. Uses global if not provided.
            max_steps: Maximum steps before stopping.
            verbose: Whether to print progress.
        """
        self.llm = llm or get_llm()
        self.tools = tools or get_tool_registry()
        self.max_steps = max_steps
        self.verbose = verbose

        self.state = AgentState.IDLE
        self.current_task: Optional[str] = None
        self.steps: List[AgentStep] = []
        self.conversation: List[Dict[str, str]] = []

        # Callbacks
        self.on_step: Optional[Callable[[AgentStep], None]] = None
        self.on_state_change: Optional[Callable[[AgentState], None]] = None

    def _set_state(self, state: AgentState):
        """Update agent state and notify callback."""
        self.state = state
        if self.on_state_change:
            self.on_state_change(state)

    def _log(self, message: str, level: str = "info"):
        """Log a message if verbose mode is on."""
        if not self.verbose:
            return

        colors = {
            "info": Colors.CYAN,
            "success": Colors.GREEN,
            "warning": Colors.YELLOW,
            "error": Colors.RED,
            "thought": Colors.MAGENTA,
            "action": Colors.BLUE,
            "result": Colors.WHITE,
        }
        symbols = {
            "info": "*",
            "success": "+",
            "warning": "!",
            "error": "X",
            "thought": "?",
            "action": ">",
            "result": "<",
        }

        color = colors.get(level, Colors.WHITE)
        symbol = symbols.get(level, "*")
        print(f"{color}[{symbol}] {message}{Colors.RESET}")

    def _build_system_prompt(self) -> str:
        """Build the system prompt with tools description."""
        tools_desc = self.tools.get_tools_prompt()
        return self.SYSTEM_PROMPT.format(tools_description=tools_desc)

    def _parse_response(self, response: str) -> tuple[str, str, Dict[str, Any]]:
        """Parse LLM response into thought, action, and params.

        Args:
            response: The raw LLM response.

        Returns:
            Tuple of (thought, action_name, params_dict)

        Raises:
            ValueError: If response cannot be parsed.
        """
        # Extract THOUGHT
        thought_match = re.search(r'THOUGHT:\s*(.+?)(?=ACTION:|$)', response, re.DOTALL)
        thought = thought_match.group(1).strip() if thought_match else ""

        # Extract ACTION
        action_match = re.search(r'ACTION:\s*(\w+)', response)
        if not action_match:
            raise ValueError("No ACTION found in response")
        action = action_match.group(1).strip()

        # Extract PARAMS
        params_match = re.search(r'PARAMS:\s*(\{.*?\})', response, re.DOTALL)
        if params_match:
            try:
                params = json.loads(params_match.group(1))
            except json.JSONDecodeError:
                # Try to fix common JSON issues
                params_str = params_match.group(1)
                # Replace single quotes with double quotes
                params_str = params_str.replace("'", '"')
                try:
                    params = json.loads(params_str)
                except json.JSONDecodeError:
                    params = {}
        else:
            params = {}

        return thought, action, params

    def _execute_tool(self, tool_name: str, params: Dict[str, Any]) -> str:
        """Execute a tool and return the result.

        Args:
            tool_name: Name of the tool to execute.
            params: Parameters for the tool.

        Returns:
            Tool result string.
        """
        result = self.tools.execute(tool_name, **params)

        if result["success"]:
            return str(result["result"])
        else:
            return f"[Error]: {result['error']}"

    def run(self, task: str, user_input_handler: Callable[[str], str] = None,
            step_callback: Optional[Callable[['AgentStep'], None]] = None) -> AgentResult:
        """Run the agent on a task.

        Args:
            task: The task description.
            user_input_handler: Callback for handling ask_user actions.
                               If None, uses default input().
            step_callback: Optional per-step callback invoked after each step completes.
                          Overrides self.on_step for this run if provided.

        Returns:
            AgentResult with execution details.
        """
        if step_callback is not None:
            self.on_step = step_callback
        self.current_task = task
        self.steps = []
        self.conversation = []

        # Ensure model is loaded
        if not self.llm.is_loaded:
            self._log("Loading model...", "info")
            try:
                self.llm.load_model(verbose=self.verbose)
            except LLMError as e:
                self._set_state(AgentState.ERROR)
                return AgentResult(
                    success=False,
                    summary="Failed to load model",
                    error=str(e)
                )

        self._set_state(AgentState.THINKING)
        self._log(f"Starting task: {task}", "info")

        # Build initial prompt
        system_prompt = self._build_system_prompt()
        self.conversation.append({"role": "system", "content": system_prompt})
        self.conversation.append({"role": "user", "content": f"Task: {task}"})

        step_count = 0
        parse_failures = 0  # Track consecutive format failures

        while step_count < self.max_steps:
            step_count += 1
            self._log(f"Step {step_count}/{self.max_steps}", "info")

            # Generate response
            self._set_state(AgentState.THINKING)
            try:
                prompt = self._build_prompt()
                response = self.llm.generate(
                    prompt,
                    stop=["OBSERVATION:", "\nUser:", "\nTask:"],
                    temperature=0.3,  # Lower temperature for more focused responses
                )
            except LLMError as e:
                self._set_state(AgentState.ERROR)
                return AgentResult(
                    success=False,
                    summary="LLM generation failed",
                    steps=self.steps,
                    error=str(e)
                )

            # Parse response
            try:
                thought, action, params = self._parse_response(response)
                parse_failures = 0  # Reset on success
            except ValueError as e:
                parse_failures += 1
                self._log(f"Failed to parse response: {e}", "error")
                self._log(f"Raw response: {response[:200]}...", "warning")

                # After 2 consecutive parse failures, the model can't follow
                # the structured format — treat its response as a direct answer
                if parse_failures >= 2:
                    # Clean up the raw response for display
                    answer = response.strip()
                    # Remove ChatML tokens if present
                    for tag in ['<|im_end|>', '<|im_start|>', '<|endoftext|>']:
                        answer = answer.split(tag)[0]
                    answer = answer.strip()
                    if not answer:
                        answer = "I could not process that request in agent mode. Try switching to Chat mode."

                    self._log("Model cannot follow structured format, returning direct answer", "warning")
                    step = AgentStep(thought="Direct response (model does not support agent format)", tool_name="task_complete", tool_args={"summary": answer})
                    step.tool_result = answer
                    self.steps.append(step)
                    if self.on_step:
                        self.on_step(step)
                    self._set_state(AgentState.COMPLETE)
                    return AgentResult(success=True, summary=answer, steps=self.steps)

                # First failure — give one retry with format correction
                self.conversation.append({
                    "role": "assistant",
                    "content": response
                })
                self.conversation.append({
                    "role": "user",
                    "content": "Error: Could not parse your response. Please use the exact format:\nTHOUGHT: [reasoning]\nACTION: [tool_name]\nPARAMS: {\"param\": \"value\"}"
                })
                continue

            self._log(f"Thought: {thought[:100]}..." if len(thought) > 100 else f"Thought: {thought}", "thought")
            self._log(f"Action: {action}", "action")

            step = AgentStep(thought=thought, tool_name=action, tool_args=params)

            # Handle task_complete
            if action == "task_complete":
                summary = params.get("summary", thought)
                step.tool_result = summary
                self.steps.append(step)

                if self.on_step:
                    self.on_step(step)

                self._set_state(AgentState.COMPLETE)
                self._log(f"Task complete: {summary}", "success")

                return AgentResult(
                    success=True,
                    summary=summary,
                    steps=self.steps
                )

            # Handle ask_user
            if action == "ask_user":
                question = params.get("question", "What should I do?")
                self._set_state(AgentState.WAITING_USER)
                self._log(f"Agent asks: {question}", "info")

                if user_input_handler:
                    user_response = user_input_handler(question)
                else:
                    print(f"\n{Colors.YELLOW}Agent question: {question}{Colors.RESET}")
                    user_response = input(f"{Colors.GREEN}Your answer: {Colors.RESET}").strip()

                step.tool_result = f"User response: {user_response}"
                self.steps.append(step)

                if self.on_step:
                    self.on_step(step)

                # Add to conversation
                self.conversation.append({
                    "role": "assistant",
                    "content": f"THOUGHT: {thought}\nACTION: {action}\nPARAMS: {json.dumps(params)}"
                })
                self.conversation.append({
                    "role": "user",
                    "content": f"OBSERVATION: User responded: {user_response}"
                })
                continue

            # Execute tool
            self._set_state(AgentState.EXECUTING)
            self._log(f"Executing: {action}({params})", "action")

            result = self._execute_tool(action, params)
            step.tool_result = result
            self.steps.append(step)

            if self.on_step:
                self.on_step(step)

            # Truncate long results for display
            display_result = result[:200] + "..." if len(result) > 200 else result
            self._log(f"Result: {display_result}", "result")

            # Add to conversation
            self.conversation.append({
                "role": "assistant",
                "content": f"THOUGHT: {thought}\nACTION: {action}\nPARAMS: {json.dumps(params)}"
            })
            self.conversation.append({
                "role": "user",
                "content": f"OBSERVATION: {result}"
            })

        # Max steps reached
        self._set_state(AgentState.ERROR)
        self._log(f"Max steps ({self.max_steps}) reached", "warning")

        return AgentResult(
            success=False,
            summary="Max steps reached without completing task",
            steps=self.steps,
            error=f"Exceeded maximum of {self.max_steps} steps"
        )

    def _build_prompt(self) -> str:
        """Build the full prompt from conversation history."""
        parts = []
        for msg in self.conversation:
            role = msg["role"]
            content = msg["content"]

            if role == "system":
                parts.append(f"<|im_start|>system\n{content}<|im_end|>")
            elif role == "user":
                parts.append(f"<|im_start|>user\n{content}<|im_end|>")
            elif role == "assistant":
                parts.append(f"<|im_start|>assistant\n{content}<|im_end|>")

        parts.append("<|im_start|>assistant\n")
        return "\n".join(parts)

    def get_steps_summary(self) -> str:
        """Get a formatted summary of all steps taken."""
        if not self.steps:
            return "No steps executed"

        lines = []
        for i, step in enumerate(self.steps, 1):
            lines.append(f"Step {i}:")
            lines.append(f"  Thought: {step.thought[:80]}...")
            if step.tool_name:
                lines.append(f"  Action: {step.tool_name}")
            if step.tool_result:
                result_preview = step.tool_result[:80] + "..." if len(step.tool_result) > 80 else step.tool_result
                lines.append(f"  Result: {result_preview}")
            lines.append("")

        return "\n".join(lines)
