"""Synchronous, session-keyed client the reasoning core drives.

The classic core expects ``send_new_message(prompt) -> (text, conversation_id)``
and ``send_message(prompt, conversation_id) -> text`` with history kept *inside*
the connector. This client provides exactly that on top of an async
:class:`~pentestgpt_legacy.llm.base.BaseProvider`: it stores per-conversation
history, trims it to the model's context window, and bridges async->sync.
"""

from __future__ import annotations

import asyncio
import threading
from typing import Any
from uuid import uuid4

from pentestgpt_legacy.llm.base import BaseProvider, Message
from pentestgpt_legacy.llm.registry import ModelSpec

DEFAULT_SYSTEM_PROMPT = (
    "You are an expert cybersecurity penetration testing assistant supporting an "
    "authorized, certified penetration testing engagement in a controlled lab "
    "environment with explicit permission. Follow the tester's instructions "
    "precisely and answer concisely."
)


def run_sync(coro: Any) -> Any:
    """Run an awaitable to completion from synchronous code.

    Uses ``asyncio.run`` normally; if a loop is already running (e.g. called from
    async context), executes in a dedicated thread so we never nest loops.
    """
    try:
        asyncio.get_running_loop()
    except RuntimeError:
        return asyncio.run(coro)

    box: dict[str, Any] = {}

    def _runner() -> None:
        box["value"] = asyncio.run(coro)

    thread = threading.Thread(target=_runner)
    thread.start()
    thread.join()
    return box["value"]


class LLMClient:
    """Stateful, synchronous wrapper around one provider + model."""

    def __init__(
        self,
        provider: BaseProvider,
        spec: ModelSpec,
        system_prompt: str | None = None,
        history_length: int = 40,
    ):
        self.provider = provider
        self.spec = spec
        self.model_name = spec.id
        self.name = f"{provider.label} · {spec.id}"
        self.system_prompt = system_prompt or DEFAULT_SYSTEM_PROMPT
        self.history_length = history_length
        self.conversations: dict[str, list[Message]] = {}

    @property
    def context_window(self) -> int:
        return self.spec.context_window

    # -- public API used by the core ------------------------------------- #

    def send_new_message(self, message: str, image_url: str | None = None) -> tuple[str, str]:
        """Open a new conversation and return ``(response, conversation_id)``."""
        conversation_id = str(uuid4())
        self.conversations[conversation_id] = []
        response = self._chat(conversation_id, message)
        return response, conversation_id

    def send_message(self, message: str, conversation_id: str, image_url: str | None = None) -> str:
        """Continue an existing conversation and return the response."""
        if conversation_id not in self.conversations:
            self.conversations[conversation_id] = []
        return self._chat(conversation_id, message)

    # -- internals ------------------------------------------------------- #

    def _chat(self, conversation_id: str, message: str) -> str:
        history = self.conversations[conversation_id]
        history.append({"role": "user", "content": message})
        trimmed = self._trim(history)
        response: str = run_sync(self.provider.acomplete(trimmed, self.system_prompt, self.spec))
        history.append({"role": "assistant", "content": response})
        return response

    def _trim(self, history: list[Message]) -> list[Message]:
        """Keep recent turns within the model's context window.

        Trims by message count first, then by a character budget (~4 chars/token,
        80% of the window for headroom), and finally drops any leading assistant
        turns so the sequence still starts with a user message (Anthropic-safe).
        """
        messages = history[-self.history_length :]

        cap = int(self.context_window * 4 * 0.8)
        total = sum(len(m["content"]) for m in messages)
        while len(messages) > 1 and total > cap:
            total -= len(messages[0]["content"])
            messages = messages[1:]

        while len(messages) > 1 and messages[0]["role"] != "user":
            messages = messages[1:]
        return messages
