#!/usr/bin/env python3
"""
coder - A CLI tool for interacting with coderai API
Connects to OpenAI-compatible API and executes tools automatically.
"""

import os
import sys
import json
import argparse
import subprocess
from pathlib import Path
from typing import Optional, Dict, Any, List, Callable
from dataclasses import dataclass, field

import requests


# ANSI color codes
class Colors:
    """ANSI color codes for terminal output."""
    RESET = "\033[0m"
    BOLD = "\033[1m"
    DIM = "\033[2m"
    RED = "\033[91m"
    GREEN = "\033[92m"
    YELLOW = "\033[93m"
    BLUE = "\033[94m"
    MAGENTA = "\033[95m"
    CYAN = "\033[96m"
    WHITE = "\033[97m"


# Default system prompt for normal models
DEFAULT_SYSTEM_PROMPT = """You are Coder, an AI coding assistant. You help users write, read, and modify code files. You have access to tools for file operations.

## CRITICAL: Response Format

1. ALWAYS maintain proper spacing between words and after punctuation.
2. Use complete sentences with normal spacing.
3. When showing code, use proper code blocks with language identifiers.

## Available Tools

You can invoke tools by outputting JSON inside <tool> tags:

<tool>{"name": "TOOL_NAME", "arguments": {PARAMETERS}}</tool>

### read_file - Read file contents
Purpose: Read one or more files to understand the codebase
Parameters: {"path": "relative/path/to/file"}
Example: <tool>{"name": "read_file", "arguments": {"path": "main.py"}}</tool>

### write_file - Create or overwrite files
Purpose: Write new files or completely replace existing ones
Parameters: {"path": "relative/path", "content": "full file content"}
Example: <tool>{"name": "write_file", "arguments": {"path": "hello.py", "content": "print('Hello World')"}}</tool>

### apply_diff - Modify existing files
Purpose: Make targeted changes to specific sections of files
Parameters: {"path": "relative/path", "diff": "SEARCH/REPLACE block"}
Example: <tool>{"name": "apply_diff", "arguments": {"path": "main.py", "diff": "<<<<<<< SEARCH\ndef old_func():\n    pass\n=======\ndef new_func():\n    return 42\n>>>>>>> REPLACE"}}</tool>

### execute_command - Run shell commands
Purpose: Execute commands like git, npm, python, ls, etc.
Parameters: {"command": "shell command string"}
Example: <tool>{"name": "execute_command", "arguments": {"command": "ls -la"}}</tool>

## Tool Usage Rules

1. READ FIRST: Always read files before modifying them
2. COMPLETE REPLACEMENTS: When using write_file, include the ENTIRE file content
3. TARGETED EDITS: Use apply_diff for small changes to preserve the rest of the file
4. ONE TOOL AT A TIME: Make one tool call, wait for results, then proceed
5. VERIFY CHANGES: After writing files, read them back to confirm

## Workflow Example

User: "Add a function to main.py"
You: <tool>{"name": "read_file", "arguments": {"path": "main.py"}}</tool>
[Tool result shown]
You: [Explain what you'll add, then call write_file or apply_diff]

## Output Style

- Use markdown for formatting
- Show file paths as [`filename`](path/to/file)
- Include code blocks with language tags
- Maintain normal spacing in all responses"""

# Simplified system prompt for small models (under 7B parameters)
SMALL_MODEL_SYSTEM_PROMPT = """You are Coder, an AI assistant. Help with coding tasks.

IMPORTANT RULES:
1. Put ONE space between EVERY word.
2. Put ONE space after periods and commas.
3. Use code blocks with triple backticks.
4. Be concise.

TOOLS:
Use <tool>{"name": "TOOL", "arguments": {}}</tool> format.

Available tools:
- read_file: {"path": "file.py"} - Read a file
- write_file: {"path": "file.py", "content": "code"} - Write a file
- apply_diff: {"path": "file.py", "diff": "SEARCH...REPLACE"} - Edit file
- execute_command: {"command": "ls"} - Run command

ALWAYS add spaces between words."""

# Minimal system prompt for tiny models (under 3B parameters)
TINY_MODEL_SYSTEM_PROMPT = """You are Coder. Help with code.

Rules:
- Space between words
- Space after punctuation
- Use ``` for code
- Be brief

Tools: <tool>{"name":"TOOL","arguments":{}}</tool>
Tools: read_file, write_file, apply_diff, execute_command"""


@dataclass
class Config:
    """Configuration for the coder CLI."""
    api_url: str = "http://localhost:6744/v1"
    token: Optional[str] = None
    system_prompt: str = DEFAULT_SYSTEM_PROMPT
    model: str = "default"
    small: bool = False  # Use small model optimizations
    tiny: bool = False   # Use tiny model optimizations (minimal)
    timeout: int = 600   # Request timeout in seconds
    confirm_all: bool = True  # Confirm before executing tools by default
    confirm_commands: Dict[str, bool] = None  # Per-command confirmation settings
    
    def __post_init__(self):
        if self.confirm_commands is None:
            self.confirm_commands = {}

    @classmethod
    def load(cls, config_path: Optional[str] = None) -> "Config":
        """Load configuration from file or create default."""
        if config_path is None:
            config_path = os.path.expanduser("~/.config/coderai/cli.json")
        
        config = cls()
        
        if os.path.exists(config_path):
            try:
                with open(config_path, 'r') as f:
                    data = json.load(f)
                config.api_url = data.get('api_url', config.api_url)
                config.token = data.get('token')
                config.system_prompt = data.get('system_prompt', config.system_prompt)
                config.model = data.get('model', config.model)
                config.small = data.get('small', config.small)
                config.tiny = data.get('tiny', config.tiny)
                config.timeout = data.get('timeout', config.timeout)
            except (json.JSONDecodeError, IOError) as e:
                print(f"Warning: Could not load config from {config_path}: {e}", file=sys.stderr)
        
        return config
    
    def save(self, config_path: Optional[str] = None) -> None:
        """Save configuration to file."""
        if config_path is None:
            config_path = os.path.expanduser("~/.config/coderai/cli.json")
        
        # Ensure directory exists
        os.makedirs(os.path.dirname(config_path), exist_ok=True)
        
        data = {
            'api_url': self.api_url,
            'token': self.token,
            'system_prompt': self.system_prompt,
            'model': self.model,
            'small': self.small,
            'tiny': self.tiny,
            'timeout': self.timeout
        }
        
        with open(config_path, 'w') as f:
            json.dump(data, f, indent=2)


class ToolExecutor:
    """Executes tool calls from the LLM."""
    
    def __init__(self, working_dir: str = "."):
        self.working_dir = working_dir
        self.tools = self._define_tools()
    
    def _define_tools(self) -> List[Dict[str, Any]]:
        """Define available tools in OpenAI format."""
        return [
            {
                "type": "function",
                "function": {
                    "name": "read_file",
                    "description": "Read the contents of a file",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "path": {
                                "type": "string",
                                "description": "Path to the file to read (relative to working directory)"
                            }
                        },
                        "required": ["path"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "write_file",
                    "description": "Write content to a file (creates or overwrites)",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "path": {
                                "type": "string",
                                "description": "Path to the file to write (relative to working directory)"
                            },
                            "content": {
                                "type": "string",
                                "description": "Content to write to the file"
                            }
                        },
                        "required": ["path", "content"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "apply_diff",
                    "description": "Apply a diff/patch to a file. Use SEARCH/REPLACE blocks format.",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "path": {
                                "type": "string",
                                "description": "Path to the file to modify"
                            },
                            "diff": {
                                "type": "string",
                                "description": "Diff content in SEARCH/REPLACE format: <<<<<<< SEARCH\\n[old content]\\n=======\\n[new content]\\n>>>>>>> REPLACE"
                            }
                        },
                        "required": ["path", "diff"]
                    }
                }
            },
            {
                "type": "function",
                "function": {
                    "name": "execute_command",
                    "description": "Execute a shell command",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "command": {
                                "type": "string",
                                "description": "The shell command to execute"
                            },
                            "cwd": {
                                "type": "string",
                                "description": "Working directory for the command (optional, defaults to current)"
                            }
                        },
                        "required": ["command"]
                    }
                }
            }
        ]
    
    def execute(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
        """Execute a tool call and return the result."""
        try:
            if tool_name == "read_file":
                return self._read_file(arguments["path"])
            elif tool_name == "write_file":
                return self._write_file(arguments["path"], arguments["content"])
            elif tool_name == "apply_diff":
                return self._apply_diff(arguments["path"], arguments["diff"])
            elif tool_name == "execute_command":
                cwd = arguments.get("cwd", self.working_dir)
                return self._execute_command(arguments["command"], cwd)
            else:
                return {"error": f"Unknown tool: {tool_name}"}
        except Exception as e:
            return {"error": str(e)}
    
    def _read_file(self, path: str) -> Dict[str, Any]:
        """Read a file and return its contents."""
        full_path = os.path.join(self.working_dir, path)
        full_path = os.path.abspath(full_path)
        
        if not os.path.exists(full_path):
            return {"error": f"File not found: {path}"}
        
        if not os.path.isfile(full_path):
            return {"error": f"Path is not a file: {path}"}
        
        try:
            with open(full_path, 'r', encoding='utf-8', errors='replace') as f:
                content = f.read()
            return {"content": content, "path": path}
        except Exception as e:
            return {"error": f"Failed to read file: {e}"}
    
    def _write_file(self, path: str, content: str) -> Dict[str, Any]:
        """Write content to a file."""
        full_path = os.path.join(self.working_dir, path)
        full_path = os.path.abspath(full_path)
        
        # Ensure directory exists
        os.makedirs(os.path.dirname(full_path), exist_ok=True)
        
        try:
            with open(full_path, 'w', encoding='utf-8') as f:
                f.write(content)
            return {"success": True, "path": path, "bytes_written": len(content)}
        except Exception as e:
            return {"error": f"Failed to write file: {e}"}
    
    def _apply_diff(self, path: str, diff: str) -> Dict[str, Any]:
        """Apply a SEARCH/REPLACE diff to a file."""
        full_path = os.path.join(self.working_dir, path)
        full_path = os.path.abspath(full_path)
        
        if not os.path.exists(full_path):
            return {"error": f"File not found: {path}"}
        
        try:
            with open(full_path, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Parse and apply SEARCH/REPLACE blocks
            import re
            pattern = r'<<<<<<< SEARCH\n(.*?)\n=======\n(.*?)\n>>>>>>> REPLACE'
            matches = list(re.finditer(pattern, diff, re.DOTALL))
            
            if not matches:
                return {"error": "No valid SEARCH/REPLACE blocks found in diff"}
            
            new_content = content
            replacements = 0
            
            for match in matches:
                search_text = match.group(1)
                replace_text = match.group(2)
                
                if search_text in new_content:
                    new_content = new_content.replace(search_text, replace_text, 1)
                    replacements += 1
                else:
                    return {"error": f"Search text not found in file: {search_text[:50]}..."}
            
            with open(full_path, 'w', encoding='utf-8') as f:
                f.write(new_content)
            
            return {
                "success": True, 
                "path": path, 
                "replacements": replacements
            }
        except Exception as e:
            return {"error": f"Failed to apply diff: {e}"}
    
    def _execute_command(self, command: str, cwd: str) -> Dict[str, Any]:
        """Execute a shell command."""
        try:
            result = subprocess.run(
                command,
                shell=True,
                cwd=cwd,
                capture_output=True,
                text=True,
                timeout=300  # 5 minute timeout
            )
            
            return {
                "success": result.returncode == 0,
                "returncode": result.returncode,
                "stdout": result.stdout,
                "stderr": result.stderr,
                "command": command
            }
        except subprocess.TimeoutExpired:
            return {"error": "Command timed out after 5 minutes"}
        except Exception as e:
            return {"error": f"Failed to execute command: {e}"}


class CoderClient:
    """Client for interacting with the coderai API."""
    
    def __init__(self, config: Config):
        self.config = config
        self.tool_executor = ToolExecutor()
        self.conversation_history: List[Dict[str, Any]] = []
    
    def chat(self, message: str, stream: bool = True) -> str:
        """Send a message to the API and get response."""
        # Add user message to history
        self.conversation_history.append({
            "role": "user",
            "content": message
        })
        
        # Prepare messages with system prompt
        messages = [{"role": "system", "content": self.config.system_prompt}]
        messages.extend(self.conversation_history)
        
        headers = {"Content-Type": "application/json"}
        if self.config.token:
            headers["Authorization"] = f"Bearer {self.config.token}"
        
        payload = {
            "model": self.config.model,
            "messages": messages,
            "tools": self.tool_executor.tools,
            "tool_choice": "auto",
            "stream": stream
        }
        
        try:
            response = requests.post(
                f"{self.config.api_url}/chat/completions",
                headers=headers,
                json=payload,
                stream=stream,
                timeout=self.config.timeout
            )
            response.raise_for_status()
            
            if stream:
                return self._handle_streaming_response(response)
            else:
                return self._handle_non_streaming_response(response)
                
        except requests.exceptions.ConnectionError:
            return "Error: Could not connect to API. Is the server running?"
        except requests.exceptions.Timeout:
            return "Error: Request timed out."
        except requests.exceptions.RequestException as e:
            return f"Error: API request failed: {e}"
    
    def _handle_streaming_response(self, response: requests.Response) -> str:
        """Handle streaming response from API."""
        import time
        import re
        import threading
        
        full_content = ""
        tool_calls = []
        in_thinking = False
        thinking_content = ""
        thinking_start_time = 0
        last_elapsed = -1
        thinking_line_printed = False
        timer_thread = None
        stop_timer = threading.Event()
        
        def update_timer():
            """Background thread to update timer every second."""
            while not stop_timer.is_set():
                if in_thinking:
                    current_time = time.time()
                    elapsed = int(current_time - thinking_start_time)
                    print_thinking_line(elapsed, thinking_content, timer_update=True)
                time.sleep(0.5)
        
        def print_thinking_line(elapsed, content, final=False, timer_update=False):
            """Print or update the thinking line."""
            nonlocal thinking_line_printed, last_elapsed
            
            # Only update if elapsed changed or it's a forced update
            if timer_update and elapsed == last_elapsed:
                return
            last_elapsed = elapsed
            
            # Filter out <tool>, <tool_call>, and <tools> tags and newlines for display
            display_content = re.sub(r'<tool>.*?</tool>', '', content, flags=re.DOTALL)
            display_content = re.sub(r'<tool_call>.*?</tool_call>', '', display_content, flags=re.DOTALL)
            display_content = re.sub(r'<tools>.*?</tools>', '', display_content, flags=re.DOTALL)
            display_content = display_content.replace('\n', ' ').replace('\r', '')
            display_content = display_content.strip()
            
            # Get last 60 chars to fit on screen
            if len(display_content) > 60:
                display_content = "..." + display_content[-60:]
            
            line = f"[{elapsed}s] Thinking: [{display_content}]"
            
            # Clear line and print
            print(f"\r\033[K{Colors.DIM}{line}{Colors.RESET}", end='', flush=True)
            thinking_line_printed = True
            
            if final:
                print()  # Move to new line
        
        def parse_tool_calls_from_content(text):
            """Parse tool calls from content in various formats."""
            parsed = []
            
            # Format 1: <tool_call>{"name": "...", "arguments": {...}}</tool_call>
            pattern1 = r'<tool_call>\s*(\{[^}]+\})\s*</tool_call>'
            matches1 = re.findall(pattern1, text, re.DOTALL)
            for match in matches1:
                try:
                    tool_data = json.loads(match)
                    parsed.append({
                        'id': f'call_{len(parsed)}',
                        'type': 'function',
                        'function': {
                            'name': tool_data.get('name', ''),
                            'arguments': json.dumps(tool_data.get('arguments', {}))
                        }
                    })
                except json.JSONDecodeError:
                    continue
            
            # Format 2: XML format with <tools><tool><name>...</name><arguments>...</arguments></tool></tools>
            # Parse write_file format
            write_file_pattern = r'<tool>\s*<name>write_file</name>\s*<arguments>\s*<file\s+path="([^"]+)">\s*<content>(.*?)</content>\s*</file>\s*</arguments>\s*</tool>'
            for match in re.finditer(write_file_pattern, text, re.DOTALL | re.IGNORECASE):
                path = match.group(1)
                content = match.group(2)
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': 'write_file',
                        'arguments': json.dumps({'path': path, 'content': content.strip()})
                    }
                })
            
            # Parse read_file format
            read_file_pattern = r'<tool>\s*<name>read_file</name>\s*<arguments>\s*<path>([^<]+)</path>\s*</arguments>\s*</tool>'
            for match in re.finditer(read_file_pattern, text, re.DOTALL | re.IGNORECASE):
                path = match.group(1).strip()
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': 'read_file',
                        'arguments': json.dumps({'path': path})
                    }
                })
            
            # Parse execute_command format
            exec_pattern = r'<tool>\s*<name>execute_command</name>\s*<arguments>\s*(?:<command>)?([^<]+)(?:</command>)?\s*</arguments>\s*</tool>'
            for match in re.finditer(exec_pattern, text, re.DOTALL | re.IGNORECASE):
                command = match.group(1).strip()
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': 'execute_command',
                        'arguments': json.dumps({'command': command})
                    }
                })
            
            # Parse apply_diff format
            diff_pattern = r'<tool>\s*<name>apply_diff</name>\s*<arguments>\s*<path>([^<]+)</path>\s*<diff>(.*?)</diff>\s*</arguments>\s*</tool>'
            for match in re.finditer(diff_pattern, text, re.DOTALL | re.IGNORECASE):
                path = match.group(1).strip()
                diff = match.group(2).strip()
                parsed.append({
                    'id': f'call_{len(parsed)}',
                    'type': 'function',
                    'function': {
                        'name': 'apply_diff',
                        'arguments': json.dumps({'path': path, 'diff': diff})
                    }
                })
            
            return parsed
        
        # Start timer thread
        timer_thread = threading.Thread(target=update_timer, daemon=True)
        timer_thread.start()
        
        # Use iter_content with smaller chunk size for better real-time handling
        buffer = ""
        
        try:
            for chunk in response.iter_content(chunk_size=256, decode_unicode=True):
                if chunk:
                    buffer += chunk
                    
                    # Process complete lines from buffer
                    while '\n' in buffer:
                        line, buffer = buffer.split('\n', 1)
                        
                        # Handle SSE format
                        if line.startswith('data: '):
                            line = line[6:]
                        
                        if line == '[DONE]':
                            break
                        
                        if not line:
                            continue
                        
                        try:
                            data = json.loads(line)
                            delta = data.get('choices', [{}])[0].get('delta', {})
                            
                            # Handle content
                            content = delta.get('content')
                            if content:
                                # Check for thinking tags
                                if '<think>' in content:
                                    in_thinking = True
                                    thinking_start_time = time.time()
                                    last_elapsed = 0
                                    thinking_content = ""
                                    thinking_line_printed = False
                                    # Print initial thinking line immediately
                                    print_thinking_line(0, "")
                                    continue
                                
                                if in_thinking:
                                    if '</think>' in content:
                                        # End of thinking
                                        in_thinking = False
                                        parts = content.split('</think>', 1)
                                        think_part = parts[0]
                                        if think_part:
                                            thinking_content += think_part
                                            # Update display one last time
                                            elapsed = int(time.time() - thinking_start_time)
                                            print_thinking_line(elapsed, thinking_content)
                                        
                                        # Show final thinking line with newline
                                        elapsed = int(time.time() - thinking_start_time)
                                        print_thinking_line(elapsed, thinking_content, final=True)
                                        
                                        # Get content after </think>
                                        actual_content = parts[1] if len(parts) > 1 else ""
                                        if actual_content:
                                            # Check for tool calls in the content
                                            parsed_tools = parse_tool_calls_from_content(actual_content)
                                            if parsed_tools:
                                                tool_calls.extend(parsed_tools)
                                            else:
                                                print(actual_content, end='', flush=True)
                                                full_content += actual_content
                                    else:
                                        # Still thinking - accumulate and update display immediately
                                        thinking_content += content
                                        elapsed = int(time.time() - thinking_start_time)
                                        print_thinking_line(elapsed, thinking_content)
                                else:
                                    # Check for tool calls in normal content too
                                    parsed_tools = parse_tool_calls_from_content(content)
                                    if parsed_tools:
                                        tool_calls.extend(parsed_tools)
                                    else:
                                        print(content, end='', flush=True)
                                        full_content += content
                            
                            # Handle tool calls (OpenAI format)
                            delta_tool_calls = delta.get('tool_calls')
                            if delta_tool_calls:
                                for tc in delta_tool_calls:
                                    index = tc.get('index', 0)
                                    
                                    # Extend tool_calls list if needed
                                    while len(tool_calls) <= index:
                                        tool_calls.append({
                                            'id': '',
                                            'type': 'function',
                                            'function': {'name': '', 'arguments': ''}
                                        })
                                    
                                    # Update tool call
                                    if 'id' in tc:
                                        tool_calls[index]['id'] = tc['id']
                                    if 'function' in tc:
                                        if 'name' in tc['function']:
                                            tool_calls[index]['function']['name'] = tc['function']['name']
                                        if 'arguments' in tc['function']:
                                            tool_calls[index]['function']['arguments'] += tc['function']['arguments']
                            
                        except json.JSONDecodeError:
                            continue
        finally:
            stop_timer.set()
            if timer_thread:
                timer_thread.join(timeout=1)
        
        if in_thinking:
            print()  # End thinking line if still in thinking
        print()  # Newline after streaming
        
        # Execute tool calls if any
        if tool_calls:
            tool_results = []
            
            for tc in tool_calls:
                tool_name = tc['function']['name']
                try:
                    arguments = json.loads(tc['function']['arguments'])
                except json.JSONDecodeError:
                    arguments = {}
                
                # Format arguments for display
                if tool_name == 'execute_command':
                    args_str = arguments.get('command', '')
                elif tool_name == 'read_file':
                    args_str = arguments.get('path', '')
                elif tool_name == 'write_file':
                    args_str = f"{arguments.get('path', '')} ({len(arguments.get('content', ''))} bytes)"
                elif tool_name == 'apply_diff':
                    args_str = arguments.get('path', '')
                else:
                    args_str = str(arguments)
                
                # Show tool call with colors: yellow "Calling tool:", red tool name, white args
                print(f"\n{Colors.YELLOW}Calling tool:{Colors.RESET} {Colors.RED}{tool_name}{Colors.RESET} -> {args_str}")
                
                # Check if confirmation is needed
                needs_confirm = self.config.confirm_all
                if tool_name in self.config.confirm_commands:
                    needs_confirm = self.config.confirm_commands[tool_name]
                
                if needs_confirm:
                    confirm = input(f"{Colors.YELLOW}Execute? (y/N): {Colors.RESET}").strip().lower()
                    if confirm not in ('y', 'yes'):
                        result = {"error": "User declined execution", "declined": True}
                        print(f"{Colors.YELLOW}Skipped{Colors.RESET}")
                    else:
                        result = self.tool_executor.execute(tool_name, arguments)
                else:
                    result = self.tool_executor.execute(tool_name, arguments)
                
                # Show result summary
                if "error" in result:
                    print(f"{Colors.RED}Error: {result['error']}{Colors.RESET}")
                elif result.get('declined'):
                    pass  # Already printed "Skipped"
                else:
                    print(f"{Colors.GREEN}Success{Colors.RESET}")
                    # Show command output for execute_command
                    if tool_name == 'execute_command' and 'stdout' in result:
                        stdout = result['stdout'].strip()
                        if stdout:
                            # Show first few lines of output
                            lines = stdout.split('\n')[:20]
                            print(f"{Colors.CYAN}Output:{Colors.RESET}")
                            for line in lines:
                                print(f"  {line}")
                            if len(stdout.split('\n')) > 20:
                                print(f"  {Colors.DIM}... ({len(stdout.split(chr(10))) - 20} more lines){Colors.RESET}")
                
                tool_results.append({
                    "tool_call_id": tc['id'],
                    "role": "tool",
                    "content": json.dumps(result)
                })
            
            # Add assistant message with tool calls to history
            self.conversation_history.append({
                "role": "assistant",
                "content": full_content or None,
                "tool_calls": [
                    {
                        "id": tc['id'],
                        "type": "function",
                        "function": tc['function']
                    } for tc in tool_calls
                ]
            })
            
            # Add tool results to history
            self.conversation_history.extend(tool_results)
            
            # Get follow-up response with tool results
            print(f"\n{Colors.DIM}[Getting follow-up response...]{Colors.RESET}")
            return self._get_follow_up_response()
        
        # Add assistant response to history
        if full_content:
            self.conversation_history.append({
                "role": "assistant",
                "content": full_content
            })
        
        return full_content
    
    def _handle_non_streaming_response(self, response: requests.Response) -> str:
        """Handle non-streaming response from API."""
        data = response.json()
        message = data.get('choices', [{}])[0].get('message', {})
        content = message.get('content', '')
        tool_calls = message.get('tool_calls', [])
        
        if content:
            print(content)
        
        # Execute tool calls if any
        if tool_calls:
            print("\n[Executing tools...]")
            tool_results = []
            
            for tc in tool_calls:
                tool_name = tc['function']['name']
                try:
                    arguments = json.loads(tc['function']['arguments'])
                except json.JSONDecodeError:
                    arguments = {}
                
                print(f"  → {tool_name}({arguments})")
                result = self.tool_executor.execute(tool_name, arguments)
                tool_results.append({
                    "tool_call_id": tc['id'],
                    "role": "tool",
                    "content": json.dumps(result)
                })
                
                if "error" in result:
                    print(f"    Error: {result['error']}")
                else:
                    print(f"    Success")
            
            # Add to history
            self.conversation_history.append({
                "role": "assistant",
                "content": content or None,
                "tool_calls": tool_calls
            })
            self.conversation_history.extend(tool_results)
            
            # Get follow-up response
            print("\n[Getting follow-up response...]")
            return self._get_follow_up_response()
        
        # Add to history
        self.conversation_history.append({
            "role": "assistant",
            "content": content
        })
        
        return content
    
    def _get_follow_up_response(self) -> str:
        """Get follow-up response after tool execution."""
        messages = [{"role": "system", "content": self.config.system_prompt}]
        messages.extend(self.conversation_history)
        
        headers = {"Content-Type": "application/json"}
        if self.config.token:
            headers["Authorization"] = f"Bearer {self.config.token}"
        
        payload = {
            "model": self.config.model,
            "messages": messages,
            "tools": self.tool_executor.tools,
            "tool_choice": "auto",
            "stream": True
        }
        
        response = requests.post(
            f"{self.config.api_url}/chat/completions",
            headers=headers,
            json=payload,
            stream=True,
            timeout=self.config.timeout
        )
        response.raise_for_status()
        
        return self._handle_streaming_response(response)
    
    def clear_history(self):
        """Clear conversation history."""
        self.conversation_history = []


def run_interactive_shell(client: CoderClient) -> None:
    """Run interactive REPL shell."""
    print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
    print(f"{Colors.BOLD}{Colors.GREEN}  coder - Interactive Coding Assistant{Colors.RESET}")
    print(f"{Colors.CYAN}{'=' * 60}{Colors.RESET}")
    print(f"{Colors.DIM}Type /quit, /exit or press Ctrl+C to exit.{Colors.RESET}")
    print(f"{Colors.DIM}Type /clear to clear conversation history.{Colors.RESET}")
    print(f"{Colors.DIM}Type /help for more commands.{Colors.RESET}")
    print(f"{Colors.DIM}End line with \\ to continue on next line (multiline){Colors.RESET}")
    print(f"{Colors.CYAN}{'-' * 60}{Colors.RESET}")
    
    while True:
        try:
            # Colorful prompt
            prompt = f"{Colors.BOLD}{Colors.BLUE}CoderCLI>{Colors.RESET} "
            lines = []
            
            while True:
                line = input(prompt)
                # Check if line ends with backslash for continuation
                if line.rstrip().endswith('\\'):
                    lines.append(line.rstrip()[:-1])  # Remove backslash
                    prompt = f"{Colors.BLUE}  ...>{Colors.RESET} "  # Continuation prompt
                else:
                    lines.append(line)
                    break
            
            user_input = '\n'.join(lines).strip()
            
            if not user_input:
                continue
            
            # Print separator after user input
            print(f"{Colors.CYAN}{'─' * 40}{Colors.RESET}")
            
            # Handle commands with / prefix
            cmd = user_input.lower()
            
            if cmd in ('/quit', '/exit', '/q'):
                print(f"{Colors.GREEN}Goodbye!{Colors.RESET}")
                break
            
            if cmd == '/clear' or cmd == '/c':
                client.clear_history()
                print(f"{Colors.YELLOW}Conversation history cleared.{Colors.RESET}")
                continue
            
            if cmd == '/help' or cmd == '/h':
                print_help()
                continue
            
            if cmd.startswith('/read '):
                path = user_input[6:].strip()
                result = client.tool_executor._read_file(path)
                if 'content' in result:
                    print(f"\n{Colors.CYAN}--- Content of {path} ---{Colors.RESET}")
                    print(result['content'])
                    print(f"{Colors.CYAN}--- End ---{Colors.RESET}")
                else:
                    print(f"{Colors.RED}Error: {result.get('error', 'Unknown error')}{Colors.RESET}")
                continue
            
            if cmd.startswith('/exec '):
                command = user_input[6:].strip()
                result = client.tool_executor._execute_command(command, ".")
                print(f"\n{Colors.GREEN}$ {command}{Colors.RESET}")
                if result.get('stdout'):
                    print(result['stdout'])
                if result.get('stderr'):
                    print(f"{Colors.RED}stderr: {result['stderr']}{Colors.RESET}", file=sys.stderr)
                if result.get('returncode', 0) != 0:
                    print(f"{Colors.RED}Exit code: {result['returncode']}{Colors.RESET}")
                continue
            
            if cmd.startswith('/confirm '):
                parts = user_input[9:].strip().split()
                if len(parts) >= 2:
                    tool_name = parts[0]
                    setting = parts[1].lower()
                    if setting in ('yes', 'y', 'true', '1'):
                        client.config.confirm_commands[tool_name] = True
                        print(f"{Colors.GREEN}Confirmation enabled for {tool_name}{Colors.RESET}")
                    elif setting in ('no', 'n', 'false', '0'):
                        client.config.confirm_commands[tool_name] = False
                        print(f"{Colors.YELLOW}Confirmation disabled for {tool_name}{Colors.RESET}")
                    else:
                        print(f"{Colors.RED}Invalid setting. Use 'yes' or 'no'{Colors.RESET}")
                else:
                    print(f"{Colors.RED}Usage: /confirm <tool_name> <yes/no>{Colors.RESET}")
                    print(f"  Example: /confirm execute_command no")
                    print(f"  Example: /confirm write_file yes")
                continue
            
            # Send message to LLM
            client.chat(user_input)
            
        except KeyboardInterrupt:
            print(f"\n{Colors.GREEN}Goodbye!{Colors.RESET}")
            break
        except EOFError:
            print(f"\n{Colors.GREEN}Goodbye!{Colors.RESET}")
            break


def print_help():
    """Print help information."""
    print(f"""
{Colors.BOLD}{Colors.CYAN}Commands:{Colors.RESET}
  {Colors.GREEN}/quit, /exit, /q{Colors.RESET}    Exit the shell
  {Colors.GREEN}/clear, /c{Colors.RESET}          Clear conversation history
  {Colors.GREEN}/help, /h{Colors.RESET}           Show this help message
  
{Colors.BOLD}{Colors.CYAN}Shortcuts:{Colors.RESET}
  {Colors.YELLOW}/read <path>{Colors.RESET}      Read a file directly
  {Colors.YELLOW}/exec <command>{Colors.RESET}   Execute a shell command directly
  {Colors.YELLOW}/confirm <tool> <y/n>{Colors.RESET}  Enable/disable tool confirmation

{Colors.BOLD}{Colors.CYAN}Multiline Input:{Colors.RESET}
  End a line with {Colors.YELLOW}\\{Colors.RESET} to continue on next line
  Example:
    CoderCLI> This is a \\
      ...> multiline message

{Colors.BOLD}{Colors.CYAN}The assistant can use tools to:{Colors.RESET}
  - {Colors.BLUE}read_file{Colors.RESET}: Read file contents
  - {Colors.BLUE}write_file{Colors.RESET}: Write/create files
  - {Colors.BLUE}apply_diff{Colors.RESET}: Apply patches to files
  - {Colors.BLUE}execute_command{Colors.RESET}: Run shell commands
  
{Colors.DIM}Tool execution requires confirmation by default.{Colors.RESET}
{Colors.DIM}Disable confirmation with: /confirm <tool_name> no{Colors.RESET}
""")


def main():
    parser = argparse.ArgumentParser(
        description="coder - CLI tool for coderai API",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  coder                    Start interactive shell
  coder "Hello!"           Send a single message
  coder -m "Fix bug"       Send message non-interactively
  coder --config /path     Use custom config file
        """
    )
    
    parser.add_argument(
        'message',
        nargs='?',
        help='Message to send (if not provided, starts interactive shell)'
    )
    
    parser.add_argument(
        '-m', '--message',
        dest='msg_flag',
        help='Message to send (alternative to positional argument)'
    )
    
    parser.add_argument(
        '--api-url',
        help='API URL (default: from config or http://localhost:6744/v1)'
    )
    
    parser.add_argument(
        '--token',
        help='API token (default: from config)'
    )
    
    parser.add_argument(
        '--config',
        help='Path to config file (default: ~/.config/coderai/cli.json)'
    )
    
    parser.add_argument(
        '--init-config',
        action='store_true',
        help='Create default config file and exit'
    )
    
    parser.add_argument(
        '--no-stream',
        action='store_true',
        help='Disable streaming responses'
    )
    
    parser.add_argument(
        '--small',
        action='store_true',
        help='Use small model mode (simplified prompt for models under 7B parameters)'
    )
    
    parser.add_argument(
        '--tiny',
        action='store_true',
        help='Use tiny model mode (minimal prompt for models under 3B parameters)'
    )
    
    parser.add_argument(
        '--timeout',
        type=int,
        default=600,
        help='Request timeout in seconds (default: 600)'
    )
    
    args = parser.parse_args()
    
    # Handle init-config
    if args.init_config:
        config = Config()
        config.save()
        config_path = os.path.expanduser("~/.config/coderai/cli.json")
        print(f"Created default config at: {config_path}")
        print(json.dumps({
            'api_url': config.api_url,
            'token': config.token,
            'system_prompt': config.system_prompt,
            'model': config.model,
            'small': config.small,
            'tiny': config.tiny
        }, indent=2))
        return
    
    # Load config
    config = Config.load(args.config)
    
    # Override with command line args
    if args.api_url:
        config.api_url = args.api_url
    if args.token:
        config.token = args.token
    if args.small:
        config.small = True
    if args.tiny:
        config.tiny = True
    if args.timeout:
        config.timeout = args.timeout
    
    # Apply small/tiny model system prompt if enabled
    if config.tiny:
        config.system_prompt = TINY_MODEL_SYSTEM_PROMPT
        print("[Tiny model mode enabled - using minimal system prompt]")
    elif config.small:
        config.system_prompt = SMALL_MODEL_SYSTEM_PROMPT
        print("[Small model mode enabled - using simplified system prompt]")
    
    # Create client
    client = CoderClient(config)
    
    # Get message
    message = args.message or args.msg_flag
    
    if message:
        # Single message mode
        client.chat(message, stream=not args.no_stream)
    else:
        # Interactive shell mode
        run_interactive_shell(client)


if __name__ == "__main__":
    main()
