"""Interactive session runner for Oclawma.

This module provides the REPL-style interactive session that:
- Maintains conversation history
- Calls tools and feeds results back to LLM
- Handles graceful exit and session summaries
- Handles Ctrl+C interrupts
"""

import asyncio
import re
import signal
from datetime import datetime
from typing import Any, Optional

from oclawma.context.budget import BudgetExceededError, ContextBudget
from oclawma.providers.base import (
    BaseProvider,
    CompletionRequest,
    Message,
)
from oclawma.providers.ollama import OllamaProvider
from oclawma.session import ConversationHistory, SessionStats
from oclawma.session.history_summarizer import (
    RollingHistorySummarizer,
    SummaryConfig,
    SummaryResult,
)
from oclawma.tools import ToolRegistry, ToolResult, create_default_tools


class SessionExitError(Exception):
    """Exception to signal session exit."""

    def __init__(self, message: str = "Session ended"):
        self.message = message
        super().__init__(message)


class InteractiveSession:
    """Interactive REPL session for Oclawma.

    Manages the conversation loop, tool calling, and context budget.
    """

    # Special commands
    EXIT_COMMANDS = {"exit", "quit", "/exit", "/quit", "/bye"}
    CLEAR_COMMAND = "/clear"
    COMPACT_COMMAND = "/compact"
    STATUS_COMMAND = "/status"
    HELP_COMMAND = "/help"

    def __init__(
        self,
        provider: Optional[BaseProvider] = None,
        model: str = "qwen2.5:3b",
        budget: Optional[ContextBudget] = None,
        tool_registry: Optional[ToolRegistry] = None,
        summary_config: Optional[SummaryConfig] = None,
        auto_summarize: bool = True,
    ):
        """Initialize the interactive session.

        Args:
            provider: LLM provider (defaults to Ollama)
            model: Model name to use
            budget: Context budget tracker
            tool_registry: Tool registry (defaults to built-in tools)
            summary_config: Configuration for history summarization
            auto_summarize: Whether to auto-summarize when budget is high
        """
        self.provider = provider or OllamaProvider()
        self.model = model
        self.budget = budget or ContextBudget()
        self.tool_registry = tool_registry or create_default_tools()
        self.history = ConversationHistory()
        self.stats = SessionStats(start_time=datetime.now())
        self._running = False
        self._interrupted = False
        self._auto_summarize = auto_summarize
        self._last_estimated_tokens = 0

        # Initialize rolling history summarizer
        self.summarizer = RollingHistorySummarizer(
            provider=self.provider,
            model=self.model,
            config=summary_config or SummaryConfig(),
            on_summarize=self._on_summarize,
        )

        # Set up system prompt
        self._setup_system_prompt()

    def _setup_system_prompt(self) -> None:
        """Set up the system prompt with tool descriptions."""
        tool_schemas = self.tool_registry.get_all_schemas()

        tool_descriptions = []
        for schema in tool_schemas:
            ", ".join(f"{p['name']}=..." for p in schema.get("parameters", []))
            examples = schema.get("examples", [])
            example_str = f"\n  Example: {examples[0]}" if examples else ""

            tool_descriptions.append(f"- {schema['name']}: {schema['description']}{example_str}")

        tools_text = "\n".join(tool_descriptions) if tool_descriptions else "No tools available."

        system_prompt = f"""You are OCLAWMA, an OpenClaw Workflow Management Agent.

Your task is to help users by executing tools and providing helpful responses.

Available tools:
{tools_text}

When you need to use a tool, respond with:
TOOL_CALL: tool_name(param1=value1, param2=value2)

You can make multiple tool calls in sequence. After each tool result, you'll receive the output.

Important guidelines:
1. Always think step by step
2. Use tools when needed to complete tasks
3. Be concise but helpful
4. If a tool fails, explain why and suggest alternatives
5. Always wait for tool results before proceeding"""

        self.history.set_system_message(system_prompt)

    async def run(self) -> SessionStats:
        """Run the interactive session.

        Returns:
            Session statistics
        """
        self._running = True
        self._setup_signal_handlers()

        # Print welcome message
        self._print_welcome()

        try:
            while self._running:
                try:
                    # Get user input
                    user_input = await self._get_input()

                    if not user_input.strip():
                        continue

                    # Handle special commands
                    if await self._handle_command(user_input):
                        continue

                    # Process user message
                    await self._process_user_message(user_input)

                except SessionExitError:
                    break
                except KeyboardInterrupt:
                    self._handle_interrupt()
                except BudgetExceededError as e:
                    print(f"\n{e.message}")
                    print("Use /compact to free up space or type 'exit' to quit.")
                except Exception as e:
                    print(f"\n❌ Error: {e}")
                    import traceback

                    traceback.print_exc()

        finally:
            self._running = False
            self.stats.end_time = datetime.now()
            await self._cleanup()

        return self.stats

    def _print_welcome(self) -> None:
        """Print welcome message."""
        print("╔══════════════════════════════════════════════════════════╗")
        print("║           🦀 Welcome to OCLAWMA v0.1.0                   ║")
        print("║       OpenClaw Workflow Management Agent                 ║")
        print("╚══════════════════════════════════════════════════════════╝")
        print()
        print(f"Model: {self.model}")
        print(f"Tools: {', '.join(self.tool_registry.list_tools())}")
        print()
        print("Type /help for available commands, or 'exit' to quit.")
        print("Press Ctrl+C to interrupt the current operation.")
        print("─" * 60)

    async def _get_input(self) -> str:
        """Get input from the user.

        Returns:
            User input string
        """
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, lambda: input("\nYou: ").strip())

    async def _handle_command(self, input_text: str) -> bool:
        """Handle special commands.

        Args:
            input_text: User input

        Returns:
            True if command was handled, False otherwise
        """
        cmd = input_text.lower().strip()

        # Exit commands
        if cmd in self.EXIT_COMMANDS:
            raise SessionExitError("User requested exit")

        # Clear command
        if cmd == self.CLEAR_COMMAND:
            self.history.clear()
            self._setup_system_prompt()  # Re-add system prompt
            self.budget.reset()
            print("✓ Conversation history and budget cleared.")
            return True

        # Status command
        if cmd == self.STATUS_COMMAND:
            self._print_status()
            return True

        # Compact command
        if cmd == self.COMPACT_COMMAND:
            await self._compact_history()
            return True

        # Auto-summarize command
        if cmd == "/autosummary":
            self._auto_summarize = not self._auto_summarize
            status = "enabled" if self._auto_summarize else "disabled"
            print(f"✓ Auto-summarization {status}.")
            return True

        # Help command
        if cmd == self.HELP_COMMAND:
            self._print_help()
            return True

        return False

    def _print_status(self) -> None:
        """Print current session status."""
        print()
        print(self.budget.visualize())
        print()
        print(f"Messages: {len(self.history)}")
        print(f"Tools available: {len(self.tool_registry)}")
        print(f"Session started: {self.stats.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
        print(f"Auto-summarize: {'on' if self._auto_summarize else 'off'}")

        # Show summarization stats
        summary_stats = self.summarizer.get_summary_stats()
        if summary_stats["summarization_count"] > 0:
            print(f"Summarizations: {summary_stats['summarization_count']}")
            print(f"Facts preserved: {summary_stats['facts_stored']}")

        status = self.budget.get_status()
        if status.warning_message:
            print()
            print(status.warning_message)

    def _print_help(self) -> None:
        """Print help message."""
        print()
        print("Available commands:")
        print("  exit, quit, /exit, /quit  - End the session")
        print("  /clear                    - Clear conversation history")
        print("  /compact                  - Summarize conversation to save tokens")
        print("  /autosummary              - Toggle automatic summarization")
        print("  /status                   - Show session status and budget")
        print("  /help                     - Show this help message")
        print()
        print("Available tools:")
        for name in sorted(self.tool_registry.list_tools()):
            tool = self.tool_registry.get(name)
            print(f"  {name} - {tool.description}")

    async def _compact_history(self) -> None:
        """Compact conversation history using rolling summarization."""
        if len(self.history) <= 3:
            print("✓ History is already compact.")
            return

        if not self.summarizer.should_summarize(self.history):
            print("✓ History doesn't need summarization yet.")
            print(f"  Current: {self.history.estimate_tokens()} tokens")
            print(f"  Trigger: {self.summarizer.config.trigger_threshold} tokens")
            return

        print("📝 Summarizing conversation history...")

        try:
            result = await self.summarizer.summarize(self.history)

            # Reset budget based on new history
            self.budget.reset()
            estimated_tokens = self.history.estimate_tokens()
            self.budget.allocate(estimated_tokens, strict=False)

            # Show results
            print("✓ History compacted!")
            if result.summary:
                print(f"  Summary: {result.summary[:100]}...")
            print(f"  Messages summarized: {result.messages_summarized}")
            print(f"  Tokens saved: ~{result.tokens_saved}")
            print(f"  Compression ratio: {result.compression_ratio:.1%}")

            if result.facts:
                print(f"  Key facts extracted: {len(result.facts)}")

            print(f"  Token usage: ~{estimated_tokens} tokens")

        except Exception as e:
            print(f"❌ Failed to compact history: {e}")

    def _on_summarize(self, result: SummaryResult) -> None:
        """Callback when summarization occurs.

        Args:
            result: The summarization result
        """
        # Update session stats
        self.stats.tokens_used += result.new_tokens

    async def _process_user_message(self, user_input: str) -> None:
        """Process a user message and get LLM response.

        Args:
            user_input: User's message
        """
        # Add user message to history
        self.history.add_message(role="user", content=user_input)
        self.stats.user_messages += 1
        self.stats.message_count += 1

        # Update budget with incremental token usage
        estimated_tokens = self.history.estimate_tokens()
        delta = estimated_tokens - self._last_estimated_tokens
        if delta > 0:
            self.budget.allocate(delta, strict=False)
        self._last_estimated_tokens = estimated_tokens

        # Check if we should auto-summarize
        if (
            self._auto_summarize
            and self.summarizer.should_summarize(self.history)
            and self.budget.usage_percent >= 75
        ):
            print("\n⚠️  Context budget high. Auto-summarizing...")
            await self._compact_history()

        # Get LLM response with potential tool calls
        await self._get_llm_response()

    async def _get_llm_response(self, max_iterations: int = 5) -> None:
        """Get response from LLM, handling tool calls.

        Args:
            max_iterations: Maximum number of tool call iterations
        """
        iteration = 0

        while iteration < max_iterations:
            iteration += 1

            try:
                # Check budget before making request
                if not self.budget.can_allocate(1000):  # Reserve tokens for response
                    print("\n⚠️  Approaching token limit. Use /compact to free space.")
                    break

                # Convert history to provider format
                messages = [
                    Message(role=m.role, content=m.content) for m in self.history.get_messages()
                ]

                request = CompletionRequest(
                    messages=messages,
                    model=self.model,
                    temperature=0.7,
                    max_tokens=2000,
                )

                print("\nAssistant: ", end="", flush=True)

                # Get response
                response = await self.provider.complete(request)
                content = response.content

                # Update budget with actual usage
                if response.usage:
                    self.budget.allocate(response.usage.total_tokens, strict=False)
                    self.stats.tokens_used += response.usage.total_tokens

                print(content)

                # Add assistant message to history
                self.history.add_message(role="assistant", content=content)
                self.stats.assistant_messages += 1
                self.stats.message_count += 1

                # Check for tool calls
                tool_calls = self._extract_tool_calls(content)

                if not tool_calls:
                    # No tool calls, we're done
                    break

                # Execute tool calls
                for tool_call in tool_calls:
                    if self._interrupted:
                        break

                    result = await self._execute_tool_call(tool_call)

                    # Add tool result to history
                    self.history.add_message(
                        role="user",  # Tool results go as user messages
                        content=result.to_llm_context(),
                        metadata={"type": "tool_result", "tool": tool_call["name"]},
                    )

                    self.stats.tool_calls += 1
                    self.stats.tool_results.append(result.output[:100])

                if self._interrupted:
                    self._interrupted = False
                    break

                # Continue loop to get assistant's response to tool results
                continue

            except Exception as e:
                print(f"\n❌ Error getting LLM response: {e}")
                break

    def _extract_tool_calls(self, content: str) -> list[dict[str, Any]]:
        """Extract tool calls from LLM response.

        Args:
            content: LLM response content

        Returns:
            List of tool call dictionaries
        """
        tool_calls = []

        # Pattern: TOOL_CALL: tool_name(param=value, param2=value2)
        # Only match explicit TOOL_CALL prefix to prevent unintended tool execution
        patterns = [
            r"TOOL_CALL:\s*(\w+)\s*\(([^)]*)\)",
        ]

        for pattern in patterns:
            matches = re.finditer(pattern, content)
            for match in matches:
                tool_name = match.group(1)
                params_str = match.group(2)

                # Skip if not a valid tool
                if not self.tool_registry.has(tool_name):
                    continue

                # Parse parameters
                params = self._parse_tool_params(params_str)

                tool_calls.append(
                    {
                        "name": tool_name,
                        "params": params,
                        "full_match": match.group(0),
                    }
                )

        return tool_calls

    def _parse_tool_params(self, params_str: str) -> dict[str, Any]:
        """Parse tool parameters from string.

        Args:
            params_str: Parameter string like "key=value, key2='value 2'"

        Returns:
            Dictionary of parameters
        """
        params = {}

        if not params_str.strip():
            return params

        # Simple parsing: key=value pairs separated by commas
        # Handle quoted values
        current_key = ""
        current_value = ""
        in_value = False
        in_quotes = False
        quote_char = None

        i = 0
        while i < len(params_str):
            char = params_str[i]

            if not in_value:
                if char == "=":
                    in_value = True
                elif not char.isspace():
                    current_key += char
            else:
                if not in_quotes and char in ('"', "'"):
                    in_quotes = True
                    quote_char = char
                elif in_quotes and char == quote_char:
                    in_quotes = False
                    quote_char = None
                elif not in_quotes and char == ",":
                    params[current_key.strip()] = self._convert_value(current_value.strip())
                    current_key = ""
                    current_value = ""
                    in_value = False
                else:
                    current_value += char

            i += 1

        # Add last parameter
        if current_key.strip():
            params[current_key.strip()] = self._convert_value(current_value.strip())

        return params

    def _convert_value(self, value: str) -> Any:
        """Convert a string value to appropriate type.

        Args:
            value: String value

        Returns:
            Converted value
        """
        value = value.strip()

        # Try integer
        try:
            return int(value)
        except ValueError:
            pass

        # Try float
        try:
            return float(value)
        except ValueError:
            pass

        # Try boolean
        if value.lower() in ("true", "yes", "on"):
            return True
        if value.lower() in ("false", "no", "off"):
            return False

        # Remove surrounding quotes
        if (value.startswith('"') and value.endswith('"')) or (
            value.startswith("'") and value.endswith("'")
        ):
            value = value[1:-1]

        return value

    async def _execute_tool_call(self, tool_call: dict[str, Any]) -> ToolResult:
        """Execute a tool call.

        Args:
            tool_call: Tool call dictionary

        Returns:
            Tool execution result
        """
        tool_name = tool_call["name"]
        params = tool_call["params"]

        print(f"\n[Executing: {tool_name}({', '.join(f'{k}={v!r}' for k, v in params.items())})]")

        try:
            result = await self.tool_registry.execute(tool_name, **params)

            if result.success:
                print("  ✓ Success")
            else:
                print(f"  ✗ Failed: {result.error_message}")

            return result

        except Exception as e:
            print(f"  ✗ Error: {e}")
            return ToolResult(
                tool_name=tool_name,
                success=False,
                output="",
                error_message=str(e),
            )

    def _setup_signal_handlers(self) -> None:
        """Set up signal handlers for graceful shutdown."""
        try:
            # Only works on Unix
            loop = asyncio.get_event_loop()
            for sig in (signal.SIGINT, signal.SIGTERM):
                loop.add_signal_handler(sig, self._signal_handler)
        except (NotImplementedError, ValueError):
            # Windows or already handled
            pass

    def _signal_handler(self) -> None:
        """Handle interrupt signals."""
        if self._running:
            self._interrupted = True
            print("\n\n⚠️  Interrupted. Stopping current operation...")

    def _handle_interrupt(self) -> None:
        """Handle keyboard interrupt during input."""
        print("\n\nPress Ctrl+C again to exit, or type 'exit' to quit gracefully.")
        try:
            # Give user a chance to confirm
            import time

            time.sleep(0.5)
        except KeyboardInterrupt:
            print("\n\nExiting...")
            raise SessionExitError("Interrupted by user") from None

    async def _cleanup(self) -> None:
        """Clean up resources."""
        try:
            if isinstance(self.provider, OllamaProvider):
                await self.provider.close()
        except Exception:
            pass

    def print_summary(self) -> None:
        """Print session summary."""
        print()
        print("═" * 60)
        print("                    SESSION SUMMARY")
        print("═" * 60)

        duration = (self.stats.end_time or datetime.now()) - self.stats.start_time

        print(f"Duration:        {duration}")
        print(f"Messages:        {self.stats.message_count}")
        print(f"  - User:        {self.stats.user_messages}")
        print(f"  - Assistant:   {self.stats.assistant_messages}")
        print(f"Tool calls:      {self.stats.tool_calls}")
        print(f"Tokens used:     {self.stats.tokens_used:,}")

        # Show summarization stats
        summary_stats = self.summarizer.get_summary_stats()
        if summary_stats["summarization_count"] > 0:
            print(f"Summarizations:  {summary_stats['summarization_count']}")
            print(f"Facts preserved: {summary_stats['facts_stored']}")

        print()
        print(self.budget.visualize())
        print("═" * 60)
