"""
HTTP API endpoints for AI Runner: /llm, /art, /stt, /tts
- /llm/generate: POST, accepts JSON with prompt + llm_request params, streams NDJSON responses
- /llm/models: GET, lists available models
- /health: GET, returns server health status
- /art: POST, accepts ImageRequest dict, returns ImageResponse dict
- /stt, /tts: POST, stubbed
"""

import os
import json
import threading
import uuid
from http.server import BaseHTTPRequestHandler
from typing import Dict, Optional
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
from airunner.components.llm.managers.llm_request import LLMRequest
from airunner.components.art.managers.stablediffusion.image_response import (
    ImageResponse,
)
from airunner.enums import LLMActionType
from airunner.settings import AIRUNNER_LOG_LEVEL
from airunner.utils.application.get_logger import get_logger
from airunner.components.application.api.api import API
from airunner.components.calendar.data.event import Event
from airunner.components.data.session_manager import session_scope
from airunner.components.llm.data.conversation import Conversation
from airunner.utils.application.get_logger import get_logger

# Lazy import to avoid circular dependency
_api = None
logger = get_logger(__name__)


def get_api():
    """Get or create the API singleton instance."""
    global _api
    logger.info(
        f"DEBUG get_api: _api is {'None' if _api is None else type(_api).__name__}"
    )
    if _api is None:
        _api = API()
    return _api


def set_api(api_instance):
    """Set the global API instance.

    Use this when creating an API instance manually (e.g., in headless mode)
    to ensure tools can access it via get_api().

    Args:
        api_instance: The API/App instance to register globally
    """
    global _api
    logger.info(
        f"DEBUG set_api: Setting global API to {type(api_instance).__name__}"
    )
    _api = api_instance
    logger.info(
        f"DEBUG set_api: Global API is now {'None' if _api is None else type(_api).__name__}"
    )


class AIRunnerAPIRequestHandler(BaseHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        self.logger = get_logger(__name__, AIRUNNER_LOG_LEVEL)
        self._timeout: int = (
            60  # 60 second timeout for generation requests with RAG
        )
        super().__init__(*args, **kwargs)

    def _set_headers(self, status=200, content_type="application/json"):
        self.send_response(status)
        self.send_header("Content-type", content_type)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()

    def do_OPTIONS(self):
        self.send_response(204)
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Content-Type")
        self.end_headers()

    def _read_request_body(self) -> bytes:
        """Read raw request body bytes from the HTTP request.

        Returns:
            Raw bytes from request body
        """
        content_length = int(self.headers.get("Content-Length", 0))
        transfer_encoding = (
            self.headers.get("Transfer-Encoding", "") or ""
        ).lower()

        if content_length > 0:
            return self.rfile.read(content_length)
        elif "chunked" in transfer_encoding:
            return self._read_chunked()
        else:
            return b""

    def _try_parse_json(self, post_data: bytes) -> Optional[Dict]:
        """Try to parse request body as JSON.

        Args:
            post_data: Raw request body bytes

        Returns:
            Parsed dict if successful, None otherwise
        """
        try:
            return json.loads(post_data.decode("utf-8")) if post_data else {}
        except Exception:
            return None

    def _parse_llm_request_json(self, data: Dict):
        """Parse llm_request field from string to dict if needed.

        Args:
            data: Dictionary to update in-place
        """
        if "llm_request" in data and isinstance(data["llm_request"], str):
            try:
                data["llm_request"] = json.loads(data["llm_request"])
            except Exception:
                pass

    def _try_parse_form_data(self, post_data: bytes) -> Optional[Dict]:
        """Try to parse request body as form-encoded data.

        Args:
            post_data: Raw request body bytes

        Returns:
            Parsed dict if successful, None otherwise
        """
        from urllib.parse import parse_qs

        try:
            form = parse_qs(post_data.decode("utf-8"))
            data = {
                k: (v[0] if isinstance(v, list) and len(v) > 0 else v)
                for k, v in form.items()
            }
            self._parse_llm_request_json(data)
            return data
        except Exception:
            return None

    def _send_parse_error(self, error_msg: str):
        """Send a 400 error response for parse failures.

        Args:
            error_msg: Error message to send
        """
        self._set_headers(400)
        self.wfile.write(json.dumps({"error": error_msg}).encode("utf-8"))

    def _parse_request_body(self) -> Optional[Dict]:
        """Parse request body from JSON or form-encoded data.

        Returns:
            Dict containing parsed request data, or None if parsing failed
        """
        post_data = self._read_request_body()
        content_type = (self.headers.get("Content-Type", "") or "").lower()

        self.logger.debug(f"Content-Type: {content_type}")
        self.logger.debug(
            f"Raw post_data (first 200 chars): {post_data[:200]}"
        )

        result = self._try_parse_json(post_data)
        if result is not None:
            self.logger.debug("Successfully parsed as JSON")
            return result

        if "application/x-www-form-urlencoded" in content_type:
            self.logger.debug("Attempting to parse as form-encoded data")
            result = self._try_parse_form_data(post_data)
            if result is not None:
                self.logger.debug("Successfully parsed as form-encoded")
                return result
            self._send_parse_error("Invalid form data")
        else:
            self._send_parse_error("Invalid JSON")
        return None

    def do_POST(self):
        """Handle POST requests by parsing body and routing to appropriate handler."""
        path = self.path.rstrip("/")
        data = self._parse_request_body()
        if data is None:
            return  # Error response already sent by _parse_request_body

        # Route to appropriate handler
        route_map = {
            "/llm": self._handle_llm,
            "/llm/generate": self._handle_llm,
            "/llm/generate_batch": self._handle_llm_batch,
            "/admin/reset_memory": lambda d: self._handle_reset_memory(),
            "/admin/reset_database": lambda d: self._handle_reset_database(),
            "/admin/shutdown": lambda d: self._handle_shutdown(),
            "/art": lambda d: self._handle_stub("ART not implemented"),
            "/stt": lambda d: self._handle_stub("STT not implemented"),
            "/tts": lambda d: self._handle_stub("TTS not implemented"),
        }

        handler = route_map.get(path)
        if handler:
            handler(data)
        else:
            self._set_headers(404)
            self.wfile.write(
                json.dumps({"error": "Not found"}).encode("utf-8")
            )

    def do_GET(self):
        """Handle GET requests for /health and other endpoints."""
        path = self.path.rstrip("/")

        if path == "/health":
            self._handle_health()
        elif path == "/llm/models":
            self._handle_llm_models()
        else:
            self._set_headers(404)
            self.wfile.write(
                json.dumps({"error": "Not found"}).encode("utf-8")
            )

    def _handle_health(self):
        """Health check endpoint."""
        self._set_headers(200)
        health_data = {
            "status": "ready",
            "services": {
                "llm": True,
                "art": True,
                "tts": False,  # Stub for now
                "stt": False,  # Stub for now
            },
            "version": "2.0.0",
        }
        self.wfile.write(json.dumps(health_data).encode("utf-8"))

    def _handle_llm_models(self):
        """List available LLM models."""
        self._set_headers(200)
        # TODO: Get actual model list from API
        models_data = {
            "models": [{"name": "default", "type": "local", "loaded": False}]
        }
        self.wfile.write(json.dumps(models_data).encode("utf-8"))

    def _extract_llm_request_data(self, data: Dict) -> Dict:
        """Extract and normalize llm_request data from request.

        Args:
            data: Request data dictionary

        Returns:
            Normalized llm_request dictionary
        """
        llm_request_raw = data.get("llm_request", {})
        return llm_request_raw if isinstance(llm_request_raw, dict) else {}

    def _map_top_level_params(self, data: Dict, llm_request_data: Dict):
        """Map top-level parameters to llm_request fields."""
        param_mapping = {
            "temperature": "temperature",
            "max_tokens": "max_new_tokens",
            "top_p": "top_p",
            "top_k": "top_k",
            "repetition_penalty": "repetition_penalty",
            "use_memory": "use_memory",
            "tool_categories": "tool_categories",
            "model": "model",
            "rag_files": "rag_files",
            # Mode-based routing / override options
            "use_mode_routing": "use_mode_routing",
            "mode_override": "mode_override",
        }
        excluded = {
            "prompt",
            "system_prompt",
            "action",
            "stream",
            "llm_request",
        }
        for client_param, llm_param in param_mapping.items():
            if client_param in data and client_param not in excluded:
                llm_request_data[llm_param] = data[client_param]

    def _parse_action_type(self, action_str: str) -> LLMActionType:
        """Parse action string to LLMActionType enum.

        Args:
            action_str: Action string from request

        Returns:
            LLMActionType enum value
        """
        try:
            return (
                LLMActionType[action_str]
                if isinstance(action_str, str)
                else action_str
            )
        except KeyError:
            return LLMActionType.CHAT

    def _handle_llm(self, data):
        """Handle LLM generation request with streaming support."""
        print("HANDLE LLM CALLED")
        print("data", data)
        prompt = data.get("prompt")
        if not prompt:
            self._set_headers(400)
            self.wfile.write(
                json.dumps({"error": "Missing 'prompt' field"}).encode("utf-8")
            )
            return

        system_prompt = data.get("system_prompt")
        action = self._parse_action_type(data.get("action", "CHAT"))
        stream = data.get("stream", True)
        llm_request_data = self._extract_llm_request_data(data)
        self._map_top_level_params(data, llm_request_data)
        llm_request = self._create_llm_request(llm_request_data)
        if system_prompt:
            llm_request.system_prompt = system_prompt
        request_id = str(uuid.uuid4())
        if stream:
            self._handle_llm_stream(prompt, action, llm_request, request_id)
        else:
            self._handle_llm_non_stream(
                prompt, action, llm_request, request_id
            )

    def _create_llm_request(self, params: Dict) -> LLMRequest:
        """Create LLMRequest from dictionary parameters.

        Args:
            params: Dictionary of LLM parameters

        Returns:
            LLMRequest object with specified or default parameters
        """
        llm_request = LLMRequest()
        self.logger.debug(f"Creating LLMRequest from params: {params}")
        for key, value in params.items():
            if hasattr(llm_request, key):
                setattr(llm_request, key, value)
                self.logger.debug(f"Set LLMRequest.{key} = {value}")
            else:
                self.logger.warning(
                    f"Ignoring unknown LLMRequest parameter: {key}={value}"
                )
        return llm_request

    def _parse_chunk_size(self, line: bytes) -> Optional[int]:
        """Parse chunk size from HTTP chunked encoding line.

        Args:
            line: Raw line containing chunk size

        Returns:
            Chunk size as integer, or None if invalid
        """
        try:
            return int(line.split(b";", 1)[0].strip(), 16)
        except Exception:
            return None

    def _consume_trailers(self):
        """Consume HTTP trailer headers until CRLF."""
        while True:
            trailer = self.rfile.readline()
            if not trailer or trailer.strip() == b"":
                break

    def _read_chunked(self) -> bytes:
        """Read a chunked HTTP request body from self.rfile.

        Returns the full concatenated body bytes.
        """
        data = b""
        try:
            while True:
                line = self.rfile.readline()
                if not line or not line.strip():
                    if not line:
                        break
                    continue
                size = self._parse_chunk_size(line.strip())
                if size is None:
                    break
                if size == 0:
                    self._consume_trailers()
                    break
                chunk = self.rfile.read(size)
                data += chunk
                self.rfile.read(2)  # Read trailing CRLF
        except Exception:
            pass
        return data

    def _handle_llm_stream(
        self,
        prompt: str,
        action: LLMActionType,
        llm_request: LLMRequest,
        request_id: str,
    ):
        """Handle streaming LLM response as NDJSON.

        Each line is a JSON object with:
        - message: text chunk
        - is_first_message: bool
        - is_end_of_message: bool
        - sequence_number: int
        """
        self._set_headers(200, content_type="application/x-ndjson")

        # Register callback to collect streaming responses
        complete_event = threading.Event()

        def stream_callback(data: dict):
            """Callback for streaming responses."""
            response = data.get("response")
            if response:
                # Convert action enum to string for JSON serialization
                action_str = getattr(response, "action", None)
                if action_str is not None:
                    action_str = (
                        str(action_str.value)
                        if hasattr(action_str, "value")
                        else str(action_str)
                    )
                else:
                    action_str = (
                        str(action.value)
                        if hasattr(action, "value")
                        else str(action)
                    )

                response_data = {
                    "message": response.message,
                    "is_first_message": response.is_first_message,
                    "is_end_of_message": response.is_end_of_message,
                    "sequence_number": getattr(response, "sequence_number", 0),
                    "action": action_str,
                    "tools": getattr(response, "tools", None),
                }
                try:
                    self.logger.debug(
                        f"HTTP Server: stream_callback invoked: msg_len={len(response.message) if response and response.message else 0}, is_end={response.is_end_of_message}, seq={getattr(response, 'sequence_number', None)}"
                    )
                except Exception:
                    pass
                self.wfile.write(
                    json.dumps(response_data).encode("utf-8") + b"\n"
                )
                self.wfile.flush()

                if response.is_end_of_message:
                    complete_event.set()

        # Send LLM request with request_id and callback
        api = get_api()
        self.logger.debug(
            f"Sending to API with llm_request.max_new_tokens={llm_request.max_new_tokens}"
        )
        import inspect

        self.logger.info(
            f"HTTP Server: send_request file: {inspect.getfile(api.llm.send_request)}"
        )
        self.logger.info(
            f"HTTP Server: About to call api.llm.send_request, api type={type(api)}, api.llm type={type(api.llm)}"
        )
        try:
            api.llm.send_request(
                prompt=prompt,
                action=action,
                llm_request=llm_request,
                request_id=request_id,
                callback=stream_callback,
            )
            self.logger.debug(
                "HTTP Server: send_request returned (non-blocking)"
            )
            self.logger.info(
                "HTTP Server: api.llm.send_request completed successfully"
            )
        except Exception as e:
            self.logger.error(
                f"HTTP Server: Exception calling send_request: {e}",
                exc_info=True,
            )
            # Immediately return error NDJSON to HTTP client - avoids long timeout
            try:
                error_response = {
                    "message": f"Error invoking LLM: {str(e)}",
                    "is_first_message": True,
                    "is_end_of_message": True,
                    "sequence_number": 0,
                    "error": True,
                }
                self.wfile.write(
                    json.dumps(error_response).encode("utf-8") + b"\n"
                )
                self.wfile.flush()
            except Exception:
                pass
            return

        # Wait for completion (with timeout)
        self.logger.debug(
            f"HTTP Server: Waiting for completion event with timeout {self._timeout}s (request_id={request_id})"
        )
        if not complete_event.wait(
            timeout=self._timeout
        ):  # 5 minute timeout for longer generations
            # Timeout - send error response
            error_response = {
                "message": "Request timeout",
                "is_first_message": True,
                "is_end_of_message": True,
                "sequence_number": 0,
                "error": True,
            }
            self.wfile.write(
                json.dumps(error_response).encode("utf-8") + b"\n"
            )
            self.wfile.flush()
        else:
            self.logger.debug(
                f"HTTP Server: complete_event set for request_id={request_id}"
            )

    def _handle_llm_non_stream(
        self,
        prompt: str,
        action: LLMActionType,
        llm_request: LLMRequest,
        request_id: str,
    ):
        """Handle non-streaming LLM response as single JSON object."""
        self.logger.debug(
            f"_handle_llm_non_stream ENTERED with request_id={request_id}"
        )
        self._set_headers(200)
        self.logger.debug("_handle_llm_non_stream Headers set")

        # Collect all response chunks
        complete_message = []
        executed_tools = []  # Track tools executed
        complete_event = threading.Event()

        def collect_callback(data: dict):
            """Callback to collect response chunks."""
            self.logger.debug(
                f"HTTP Callback {id(collect_callback)} CALLED with data keys: {list(data.keys())}"
            )
            response = data.get("response")
            self.logger.debug(
                f"HTTP Callback Response type: {type(response)}, is_end: {response.is_end_of_message if response else None}"
            )
            self.logger.info(
                f"HTTP Callback Received response: message_len={len(response.message) if response else 0}, is_end={response.is_end_of_message if response else None}"
            )
            if response:
                complete_message.append(response.message)
                # Extract tools from response object if this is the final message
                if (
                    response.is_end_of_message
                    and hasattr(response, "tools")
                    and response.tools
                ):
                    self.logger.info(
                        f"HTTP Callback Tools found in response: {response.tools}"
                    )
                    executed_tools.extend(response.tools)
                elif response.is_end_of_message:
                    self.logger.warning(
                        f"HTTP Callback End of message but no tools. has_tools_attr={hasattr(response, 'tools')}, tools_value={getattr(response, 'tools', None)}"
                    )
                self.logger.debug(
                    f"HTTP Callback Complete message so far: {len(complete_message)} chunks"
                )
                if response.is_end_of_message:
                    self.logger.debug(
                        f"HTTP Callback END OF MESSAGE - setting event {id(complete_event)}"
                    )
                    self.logger.info(
                        "HTTP Callback End of message detected, setting event"
                    )
                    complete_event.set()
                    self.logger.debug(
                        f"HTTP Callback Event set: {complete_event.is_set()}"
                    )
                else:
                    self.logger.debug(
                        "HTTP Callback Not end yet, waiting for more..."
                    )

        self.logger.debug(
            f"HTTP Server Registering callback {id(collect_callback)} for request {request_id}"
        )
        self.logger.debug(f"HTTP Server Event object: {id(complete_event)}")

        # Send LLM request with request_id and callback
        self.logger.debug("HTTP Server About to call api.llm.send_request...")
        api = get_api()
        api.llm.send_request(
            prompt=prompt,
            action=action,
            llm_request=llm_request,
            request_id=request_id,
            callback=collect_callback,
        )
        self.logger.debug("HTTP Server api.llm.send_request completed")

        self.logger.debug(
            f"HTTP Server Waiting for event {id(complete_event)} with {self._timeout}s timeout..."
        )

        # Wait for completion (with timeout)
        if complete_event.wait(
            timeout=self._timeout
        ):  # 5 minute timeout for longer generations
            # Success - return complete message
            response_data = {
                "message": "".join(complete_message),
                "is_first_message": True,
                "is_end_of_message": True,
                "sequence_number": 0,
                "action": (
                    action.value if hasattr(action, "value") else str(action)
                ),
                "tools": executed_tools,  # Include list of executed tools
            }
        else:
            # Timeout
            response_data = {
                "message": "Request timeout",
                "is_first_message": True,
                "is_end_of_message": True,
                "sequence_number": 0,
                "error": True,
                "tools": [],
            }

        self.wfile.write(json.dumps(response_data).encode("utf-8"))

    def _handle_llm_batch(self, data):
        """Handle batch LLM generation request.

        Expected JSON format:
        {
            "prompts": ["prompt1", "prompt2", ...],
            "system_prompt": "...",  // optional, applied to all
            "action": "CHAT",  // optional
            "stream": false,  // batch doesn't support streaming
            "async": false,  // if true, returns batch_id immediately
            "llm_request": {...}  // optional params for all requests
        }
        """
        prompts = data.get("prompts")
        if not prompts or not isinstance(prompts, list):
            self._set_headers(400)
            self.wfile.write(
                json.dumps(
                    {"error": "Missing or invalid 'prompts' field"}
                ).encode("utf-8")
            )
            return

        system_prompt = data.get("system_prompt")
        action_str = data.get("action", "CHAT")
        is_async = data.get("async", False)

        # Ensure llm_request_data is always a dict, not a string
        llm_request_raw = data.get("llm_request", {})
        llm_request_data = (
            llm_request_raw if isinstance(llm_request_raw, dict) else {}
        )

        # Handle top-level LLM parameters
        param_mapping = {
            "temperature": "temperature",
            "max_tokens": "max_new_tokens",
            "top_p": "top_p",
            "top_k": "top_k",
            "repetition_penalty": "repetition_penalty",
            "use_memory": "use_memory",
            "tool_categories": "tool_categories",
        }

        for client_param, llm_param in param_mapping.items():
            if client_param in data and client_param not in [
                "prompts",
                "system_prompt",
                "action",
                "stream",
                "async",
                "llm_request",
            ]:
                llm_request_data[llm_param] = data[client_param]

        # Parse action type
        try:
            action = (
                LLMActionType[action_str]
                if isinstance(action_str, str)
                else action_str
            )
        except KeyError:
            action = LLMActionType.CHAT

        # Create LLMRequest
        llm_request = self._create_llm_request(llm_request_data)

        if is_async:
            # Async mode: return batch_id immediately
            batch_id = str(uuid.uuid4())
            # TODO: Store batch state for polling
            self._set_headers(202)  # Accepted
            response_data = {
                "batch_id": batch_id,
                "status": "processing",
                "total": len(prompts),
            }
            self.wfile.write(json.dumps(response_data).encode("utf-8"))
        else:
            # Sync mode: process all and return responses
            self._handle_llm_batch_sync(
                prompts, system_prompt, action, llm_request
            )

    def _handle_llm_batch_sync(
        self,
        prompts: list,
        system_prompt: Optional[str],
        action: LLMActionType,
        llm_request: LLMRequest,
    ):
        """Handle synchronous batch LLM generation."""
        self._set_headers(200)

        responses = []
        total = len(prompts)

        def process_single_prompt(index, prompt):
            """Process a single prompt and return (index, result)."""
            start_time = time.time()
            request_id = str(uuid.uuid4())
            complete_message = []
            complete_event = threading.Event()

            def collect_callback(data: dict):
                response = data.get("response")
                if response:
                    complete_message.append(response.message)
                    if response.is_end_of_message:
                        complete_event.set()

            api = get_api()
            api.llm.send_request(
                prompt=prompt,
                action=action,
                llm_request=llm_request,
                request_id=request_id,
                callback=collect_callback,
            )

            # Wait for completion
            if complete_event.wait(timeout=self._timeout):
                text = "".join(complete_message)
                success = True
                error = None
            else:
                text = ""
                success = False
                error = "Request timeout"

            return {
                "index": index,
                "prompt": prompt,
                "text": text,
                "success": success,
                "error": error,
                "duration": time.time() - start_time,
            }

        # Process prompts in parallel
        with ThreadPoolExecutor(max_workers=4) as executor:
            futures = {
                executor.submit(process_single_prompt, i, prompt): i
                for i, prompt in enumerate(prompts)
            }

            for future in as_completed(futures):
                try:
                    result = future.result()
                    responses.append(result)
                except Exception as e:
                    index = futures[future]
                    responses.append(
                        {
                            "index": index,
                            "prompt": prompts[index],
                            "text": "",
                            "success": False,
                            "error": str(e),
                            "duration": 0.0,
                        }
                    )

        # Sort by original order
        responses.sort(key=lambda x: x["index"])

        response_data = {
            "responses": responses,
            "total": total,
            "successful": sum(1 for r in responses if r["success"]),
            "failed": sum(1 for r in responses if not r["success"]),
        }

        self.wfile.write(json.dumps(response_data).encode("utf-8"))

    def _handle_art(self, data):
        self._set_headers(200)
        # For now, just return a stub ImageResponse
        response = ImageResponse(
            images=None,
            data=None,
            active_rect=None,
            is_outpaint=False,
        )
        self.wfile.write(json.dumps(response.to_dict()).encode("utf-8"))

    def _handle_stub(self, msg):
        self._set_headers(200)
        self.wfile.write(json.dumps({"result": msg}).encode("utf-8"))

    def _handle_reset_memory(self):
        """Reset LLM conversation memory.

        This endpoint clears the LLM's conversation history, useful for
        tests to prevent contamination between test runs.

        This clears:
        1. In-memory workflow state and checkpoints
        2. Persisted conversation in database
        3. Creates a FRESH conversation with new ID to prevent checkpoint restoration
        """
        self.logger.info("Endpoint called!")
        sys.stdout.flush()
        try:
            self.logger.info("Getting API...")
            sys.stdout.flush()
            api = get_api()
            self.logger.info(f"API: {api}")
            sys.stdout.flush()

            try:
                # Mark all conversations as non-current
                Conversation.objects.update_by(
                    {"current": True}, current=False
                )

                # Create a completely fresh conversation
                new_convo = Conversation.create()
                new_conv_id = new_convo.id

                self.logger.info(
                    f"Created new conversation ID {new_conv_id} for fresh start"
                )

                # Update workflow manager to use new conversation
                if hasattr(api.llm, "model_manager") and api.llm.model_manager:
                    workflow_manager = getattr(
                        api.llm.model_manager, "_workflow_manager", None
                    )
                    self.logger.info(
                        f"workflow_manager exists: {workflow_manager is not None}"
                    )
                    if workflow_manager:
                        # CRITICAL: Clear memory FIRST to wipe checkpoint cache
                        # BEFORE setting new conversation ID (which rebuilds workflow)
                        self.logger.info("About to call clear_memory()")
                        if hasattr(workflow_manager, "clear_memory"):
                            workflow_manager.clear_memory()
                            self.logger.info("LLM conversation memory cleared")
                            self.logger.info("clear_memory() called")

                        # NOW set new conversation ID - rebuilds workflow with clean state
                        self.logger.info("About to call set_conversation_id()")
                        if hasattr(workflow_manager, "set_conversation_id"):
                            workflow_manager.set_conversation_id(new_conv_id)
                            self.logger.info(
                                f"Workflow manager using new conversation {new_conv_id}"
                            )
                            self.logger.info(
                                f"set_conversation_id({new_conv_id}) called"
                            )

            except Exception as db_err:
                self.logger.error(
                    f"Error creating new conversation: {db_err}", exc_info=True
                )

            self._set_headers(200)
            self.wfile.write(
                json.dumps({"status": "memory_cleared"}).encode("utf-8")
            )
        except Exception as e:
            self.logger.error(f"Error resetting memory: {e}", exc_info=True)
            self._set_headers(500)
            self.wfile.write(json.dumps({"error": str(e)}).encode("utf-8"))

    def _handle_reset_database(self):
        """Reset test database by clearing all test-related tables.

        This endpoint is specifically for test isolation - it clears
        tables that accumulate test data (like Events) to prevent
        contamination between test runs.

        ONLY clears tables when AIRUNNER_ENVIRONMENT=test to prevent
        accidental data loss in production.
        """
        try:
            # SAFETY: Only allow in test environment
            if os.environ.get("AIRUNNER_ENVIRONMENT") != "test":
                self._set_headers(403)
                self.wfile.write(
                    json.dumps(
                        {
                            "error": "reset_database only allowed in test environment"
                        }
                    ).encode("utf-8")
                )
                return
            deleted_counts = {}
            with session_scope() as session:
                # Clear calendar events
                event_count = session.query(Event).delete()
                deleted_counts["events"] = event_count
                session.commit()

            self.logger.info(f"Test database cleared: {deleted_counts}")

            self._set_headers(200)
            self.wfile.write(
                json.dumps(
                    {"status": "database_cleared", "deleted": deleted_counts}
                ).encode("utf-8")
            )
        except Exception as e:
            self.logger.error(f"Error resetting database: {e}", exc_info=True)
            self._set_headers(500)
            self.wfile.write(json.dumps({"error": str(e)}).encode("utf-8"))

    def _handle_shutdown(self):
        """Gracefully shutdown the headless server.

        This endpoint allows clients to remotely shutdown the server.
        The shutdown happens after sending the response to avoid
        connection errors.
        """
        try:
            self._set_headers(200)
            self.wfile.write(
                json.dumps({"status": "shutting_down"}).encode("utf-8")
            )

            # Schedule shutdown in a background thread to allow response to complete
            def delayed_shutdown():
                time.sleep(0.5)  # Let response finish sending
                self.logger.info(
                    "Shutdown requested via /admin/shutdown endpoint"
                )
                api = get_api()
                if api:
                    api.cleanup()
                sys.exit(0)

            shutdown_thread = threading.Thread(
                target=delayed_shutdown, daemon=True
            )
            shutdown_thread.start()

        except Exception as e:
            self.logger.error(f"Error during shutdown: {e}", exc_info=True)
            self._set_headers(500)
            self.wfile.write(json.dumps({"error": str(e)}).encode("utf-8"))


# Usage: pass AIRunnerAPIRequestHandler to your HTTP server for /llm, /art, /stt, /tts endpoints.
