from __future__ import annotations

import json
import re
import time
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any, AsyncIterator, Literal
from uuid import uuid4

from ai_orchestrator.config.settings import Settings
from ai_orchestrator.schemas.openai_compat import (
    ChatCompletionChunkChoice,
    ChatCompletionChunkDelta,
    ChatCompletionChunkDeltaToolCall,
    ChatCompletionChunkDeltaToolCallFunction,
    ChatCompletionChunkResponse,
    ChatCompletionChoice,
    ChatCompletionRequest,
    ChatCompletionResponse,
    ChatCompletionResponseMessage,
    ChatCompletionResponseToolCall,
    ChatCompletionResponseToolCallFunction,
    ChatCompletionUsage,
    ModelCard,
    ModelListResponse,
    OpenAIErrorDetail,
    OpenAIErrorResponse,
)
from ai_orchestrator.schemas.task_request import TaskConstraints, TaskRequest
from ai_orchestrator.services.model_router import ModelRouter, ModelProfile

if TYPE_CHECKING:
    from ai_orchestrator.schemas.task_state import TaskState
    from ai_orchestrator.services.task_service import TaskService


class OpenAICompatHTTPError(Exception):
    def __init__(
        self,
        message: str,
        *,
        status_code: int,
        error_type: str = "invalid_request_error",
        code: str | None = None,
        param: str | None = None,
    ) -> None:
        super().__init__(message)
        self.status_code = status_code
        self.error_type = error_type
        self.code = code
        self.param = param

    def to_payload(self) -> OpenAIErrorResponse:
        return OpenAIErrorResponse(
            error=OpenAIErrorDetail(
                message=str(self),
                type=self.error_type,
                code=self.code,
                param=self.param,
            )
        )


@dataclass(frozen=True)
class OpenAICompatTarget:
    kind: Literal["chat", "agent"]
    response_model: str
    profile: ModelProfile | None = None


_AUTO_AGENT_FILE_PATTERN = re.compile(
    r"(?i)(?:[a-z]:[\\/]|\.{1,2}[\\/]|/)?[\w .\-\\/]+\.(?:"
    r"py|pyi|js|jsx|ts|tsx|java|cs|go|rs|rb|php|cpp|c|h|hpp|json|yaml|yml|toml|ini|cfg|md|txt|sql|sh|ps1"
    r")\b"
)
_AUTO_AGENT_PATH_PATTERN = re.compile(r"(?i)(?:[a-z]:[\\/]|\.{1,2}[\\/]|/)[^\s]+")
_AUTO_AGENT_ACTION_PATTERN = re.compile(
    r"(?i)\b("
    r"fix|implement|edit|modify|update|refactor|debug|troubleshoot|repair|patch|create|write|add|remove|change|"
    r"run|execute|test|lint|build|compile|apply|open|read|review|inspect|analy[sz]e|investigate|check"
    r")\b"
)
_AUTO_AGENT_RESOURCE_PATTERN = re.compile(
    r"(?i)\b("
    r"repo|repository|codebase|workspace|project|folder|directory|file|files|diff|patch|branch|commit|test suite|"
    r"pull request|pr|artifact|artifacts|bug|bugs|validation|lint|ruff|pytest"
    r")\b"
)
_AUTO_CHAT_EXPLANATION_PATTERN = re.compile(
    r"(?i)\b("
    r"what|why|how|when|which|who|explain|compare|brainstorm|discuss|describe|clarify|summari[sz]e|"
    r"teach|understand|conceptual|conceptually|architecture|tradeoff|tradeoffs"
    r")\b"
)
_AUTO_FORCE_CHAT_PATTERN = re.compile(
    r"(?i)\b("
    r"chat only|just chat|just answer|answer only|no tools|don't use tools|do not use tools|without using tools"
    r")\b"
)
_TASK_BLOCK_PATTERN = re.compile(r"(?is)<task>\s*(.*?)\s*</task>")
_ENVIRONMENT_DETAILS_PATTERN = re.compile(r"(?is)<environment_details>.*$")
_ENVIRONMENT_BLOCK_PATTERN = re.compile(r"(?is)<environment_details>\s*(.*?)\s*</environment_details>")
_CWD_PATTERN = re.compile(r"(?im)^# Current Working Directory \(([^)]+)\) Files\s*$")
_CLINE_PROTOCOL_PATTERN = re.compile(
    r"(?is)(you are cline|<task>|<environment_details>|attempt_completion|ask_followup_question|xml-style tags)"
)
_INTERNAL_REQUEST_KEYS = {
    "metadata",
    "client_context",
    "aiiq_mode",
    "mode",
    "aiiq_repo_path",
    "repo_path",
    "workspace_path",
    "aiiq_max_attempts",
    "max_attempts",
    "aiiq_engine",
    "engine",
}


class OpenAICompatService:
    def __init__(
        self,
        *,
        settings: Settings,
        model_router: ModelRouter | None = None,
        task_service: TaskService | None = None,
    ) -> None:
        self.settings = settings
        self.model_router = model_router or ModelRouter(settings)
        self.task_service = task_service

    def require_api_key(self, authorization_header: str | None) -> None:
        expected = (self.settings.openai_compat_api_key or "").strip()
        if not expected:
            return
        scheme, _, token = (authorization_header or "").partition(" ")
        if scheme.lower() != "bearer" or token.strip() != expected:
            raise OpenAICompatHTTPError(
                "Invalid API key provided.",
                status_code=401,
                error_type="invalid_request_error",
                code="invalid_api_key",
            )

    def list_models(self) -> ModelListResponse:
        profile = self._default_profile()
        created = int(time.time())
        model_cards: list[ModelCard] = []
        seen: set[str] = set()

        def add_model(model_id: str | None, owned_by: str) -> None:
            normalized = (model_id or "").strip()
            if not normalized or normalized in seen:
                return
            seen.add(normalized)
            model_cards.append(ModelCard(id=normalized, created=created, owned_by=owned_by))

        add_model(self.settings.openai_compat_auto_model_name, "aiiq")
        add_model(self.settings.openai_compat_model_name, profile.provider or "aiiq")
        add_model(self.settings.openai_compat_agent_model_name, "aiiq")
        add_model(profile.model, profile.provider or "aiiq")
        return ModelListResponse(
            data=model_cards
        )

    def get_model(self, model_id: str) -> ModelCard:
        for model in self.list_models().data:
            if model.id == model_id:
                return model
        raise OpenAICompatHTTPError(
            f"The model `{model_id}` does not exist.",
            status_code=404,
            code="model_not_found",
            param="model",
        )

    async def create_chat_completion(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None = None,
    ) -> ChatCompletionResponse:
        target = self._resolve_target(request, headers=headers)
        client_context = self._extract_client_context(request, headers=headers)
        cline_protocol = self._is_cline_protocol_request(request, client_context=client_context)
        if target.kind == "agent":
            return await self._create_agent_chat_completion(
                request,
                target.response_model,
                headers=headers,
                cline_protocol=cline_protocol,
                client_context=client_context,
            )

        assert target.profile is not None
        raw_response = await self.model_router.chat_completion(
            messages=self._serialize_messages(request),
            profile=target.profile,
            overrides=self._completion_overrides(request, stream=False),
        )
        normalized = self._normalize_chat_completion_response(raw_response, target.response_model)
        if cline_protocol:
            self._wrap_completion_for_cline(normalized)
        return normalized

    async def stream_chat_completion(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None = None,
    ) -> AsyncIterator[str]:
        target = self._resolve_target(request, headers=headers)
        client_context = self._extract_client_context(request, headers=headers)
        cline_protocol = self._is_cline_protocol_request(request, client_context=client_context)
        if target.kind == "agent":
            async for chunk in self._stream_agent_chat_completion(
                request,
                target.response_model,
                headers=headers,
                cline_protocol=cline_protocol,
                client_context=client_context,
            ):
                yield chunk
            return

        assert target.profile is not None
        stream = await self.model_router.chat_completion(
            messages=self._serialize_messages(request),
            profile=target.profile,
            overrides=self._completion_overrides(request, stream=True),
        )
        if cline_protocol:
            buffered_chunks: list[Any] = []
            async for chunk in stream:
                buffered_chunks.append(chunk)
            final_content = self._collapse_stream_content(buffered_chunks)
            wrapped_content = self._cline_attempt_completion_payload(final_content)
            yield self._single_content_stream_response(target.response_model, wrapped_content)
            yield "data: [DONE]\n\n"
            return
        async for chunk in stream:
            payload = self._normalize_chat_completion_chunk(chunk, target.response_model)
            yield f"data: {payload.model_dump_json(exclude_none=True)}\n\n"
        yield "data: [DONE]\n\n"

    def _default_profile(self) -> ModelProfile:
        base_profile = self.model_router.profile_for(self.settings.openai_compat_model_step)
        upstream_model = (self.settings.openai_compat_upstream_model or "").strip()
        if not upstream_model:
            return base_profile
        upstream_provider = (self.settings.openai_compat_upstream_provider or "").strip() or base_profile.provider
        return ModelProfile(
            model=upstream_model,
            provider=upstream_provider,
            temperature=base_profile.temperature,
            max_tokens=base_profile.max_tokens,
            dimensions=base_profile.dimensions,
            litellm_params=dict(base_profile.litellm_params),
        )

    def _resolve_profile(self, requested_model: str | None) -> ModelProfile:
        profile = self._default_profile()
        normalized = (requested_model or "").strip()
        allowed_models = {self.settings.openai_compat_model_name, profile.model}
        if not normalized or normalized in allowed_models:
            return profile
        raise OpenAICompatHTTPError(
            f"Unknown model `{normalized}`. Available models: {', '.join(sorted(allowed_models))}.",
            status_code=404,
            code="model_not_found",
            param="model",
        )

    def _resolve_target(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None = None,
    ) -> OpenAICompatTarget:
        profile = self._default_profile()
        normalized = (request.model or "").strip()
        auto_model = (self.settings.openai_compat_auto_model_name or "").strip()
        agent_model = (self.settings.openai_compat_agent_model_name or "").strip()
        chat_alias = self.settings.openai_compat_model_name
        if not normalized and auto_model:
            normalized = auto_model
        forced_mode = self._resolve_forced_mode(request, headers=headers)
        if agent_model and normalized == agent_model:
            return OpenAICompatTarget(kind="agent", response_model=agent_model)
        if auto_model and normalized == auto_model:
            target_kind = forced_mode or self._infer_auto_mode(request)
            if target_kind == "agent":
                return OpenAICompatTarget(kind="agent", response_model=auto_model)
            return OpenAICompatTarget(
                kind="chat",
                response_model=auto_model,
                profile=profile,
            )
        if not normalized or normalized in {chat_alias, profile.model}:
            return OpenAICompatTarget(
                kind="chat",
                response_model=normalized or chat_alias,
                profile=profile,
            )
        available_models = [model.id for model in self.list_models().data]
        raise OpenAICompatHTTPError(
            f"Unknown model `{normalized}`. Available models: {', '.join(sorted(available_models))}.",
            status_code=404,
            code="model_not_found",
            param="model",
        )

    def _resolve_forced_mode(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None,
    ) -> Literal["chat", "agent"] | None:
        raw_value = self._request_override(
            request,
            headers=headers,
            header_name="X-AIIQ-Mode",
            body_keys=("aiiq_mode", "mode"),
        )
        if raw_value in (None, ""):
            return None
        normalized = str(raw_value).strip().lower()
        if normalized == "auto":
            return None
        if normalized in {"chat", "agent"}:
            return normalized
        raise OpenAICompatHTTPError(
            "Unsupported AIIQ mode. Allowed values: auto, chat, agent.",
            status_code=400,
            code="invalid_mode",
            param="mode",
        )

    def _infer_auto_mode(self, request: ChatCompletionRequest) -> Literal["chat", "agent"]:
        latest_user = ""
        recent_user_messages: list[str] = []
        for message in request.messages:
            content = self._message_content_to_text(message.content)
            if not content:
                continue
            if message.role == "user":
                normalized_user_content = self._extract_primary_user_text(content)
                if not normalized_user_content:
                    continue
                latest_user = normalized_user_content
                recent_user_messages.append(normalized_user_content)

        latest_text = latest_user.strip()
        context_text = "\n".join(recent_user_messages[-3:]).strip()
        lower_latest = latest_text.lower()
        lower_context = context_text.lower()

        if not latest_text:
            return "chat"
        if _AUTO_FORCE_CHAT_PATTERN.search(latest_text):
            return "chat"
        if lower_latest in {"hi", "hello", "hey", "yo", "sup", "hola"}:
            return "chat"
        if len(lower_latest.split()) <= 3 and _AUTO_AGENT_ACTION_PATTERN.search(lower_latest) is None:
            return "chat"

        agent_score = 0
        chat_score = 0

        if _AUTO_AGENT_PATH_PATTERN.search(latest_text) or _AUTO_AGENT_FILE_PATTERN.search(latest_text):
            agent_score += 3
        if _AUTO_AGENT_ACTION_PATTERN.search(lower_latest):
            agent_score += 2
        if _AUTO_AGENT_RESOURCE_PATTERN.search(lower_latest):
            agent_score += 1
        if "```" in latest_text:
            agent_score += 1
        if any(token in lower_latest for token in ("traceback", "stack trace", "exception", "error:", "failing test")):
            agent_score += 2
        if any(
            phrase in lower_latest
            for phrase in (
                "please read",
                "look at",
                "open the file",
                "update the file",
                "run the tests",
                "apply the patch",
                "fix the bug",
                "implement this",
            )
        ):
            agent_score += 2
        if not agent_score and context_text and lower_latest in {"continue", "go ahead", "do it", "fix it"}:
            if _AUTO_AGENT_PATH_PATTERN.search(context_text) or _AUTO_AGENT_FILE_PATTERN.search(context_text):
                agent_score += 2
            if _AUTO_AGENT_ACTION_PATTERN.search(lower_context):
                agent_score += 1

        if _AUTO_CHAT_EXPLANATION_PATTERN.search(lower_latest):
            chat_score += 2
        if latest_text.endswith("?"):
            chat_score += 1
        if any(
            phrase in lower_latest
            for phrase in (
                "help me understand",
                "what do you think",
                "pros and cons",
                "high level",
                "at a high level",
            )
        ):
            chat_score += 2

        if agent_score >= 3:
            return "agent"
        if agent_score > chat_score + 1:
            return "agent"
        return "chat"

    async def _create_agent_chat_completion(
        self,
        request: ChatCompletionRequest,
        response_model: str,
        *,
        headers: Mapping[str, str] | None,
        cline_protocol: bool = False,
        client_context: dict[str, Any] | None = None,
    ) -> ChatCompletionResponse:
        task_service = self._require_task_service()
        submitted = await task_service.submit_task(
            self._build_agent_task_request(request, headers=headers, client_context=client_context)
        )
        try:
            task = await task_service.wait_for_completion(
                submitted.task_id,
                timeout_seconds=float(self.settings.openai_compat_agent_timeout_seconds),
            )
        except TimeoutError as exc:
            raise OpenAICompatHTTPError(
                "Timed out waiting for the AIIQ agent task to finish. Ensure the worker is running and try again.",
                status_code=504,
                error_type="server_error",
                code="agent_timeout",
            ) from exc

        created = int(time.time())
        content = self._format_agent_completion(task)
        return ChatCompletionResponse(
            id=f"chatcmpl-{uuid4().hex}",
            created=created,
            model=response_model,
            choices=[
                ChatCompletionChoice(
                    index=0,
                    message=ChatCompletionResponseMessage(
                        content=self._cline_attempt_completion_payload(content) if cline_protocol else content
                    ),
                    finish_reason="stop",
                )
            ],
        )

    async def _stream_agent_chat_completion(
        self,
        request: ChatCompletionRequest,
        response_model: str,
        *,
        headers: Mapping[str, str] | None,
        cline_protocol: bool = False,
        client_context: dict[str, Any] | None = None,
    ) -> AsyncIterator[str]:
        task_service = self._require_task_service()
        response_id = f"chatcmpl-{uuid4().hex}"
        created = int(time.time())
        if cline_protocol:
            submitted = await task_service.submit_task(
                self._build_agent_task_request(request, headers=headers, client_context=client_context)
            )
            try:
                final_task = await task_service.wait_for_completion(
                    submitted.task_id,
                    timeout_seconds=float(self.settings.openai_compat_agent_timeout_seconds),
                )
            except TimeoutError as exc:
                raise OpenAICompatHTTPError(
                    "Timed out waiting for the AIIQ agent task to finish. Ensure the worker is running and try again.",
                    status_code=504,
                    error_type="server_error",
                    code="agent_timeout",
                ) from exc
            content = self._cline_attempt_completion_payload(self._format_agent_completion(final_task))
            yield self._single_content_stream_response(response_model, content, response_id=response_id, created=created)
            yield "data: [DONE]\n\n"
            return

        yield self._chunk_payload(
            response_id,
            response_model,
            created=created,
            role="assistant",
        )

        submitted = await task_service.submit_task(
            self._build_agent_task_request(request, headers=headers, client_context=client_context)
        )
        yield self._chunk_payload(
            response_id,
            response_model,
            created=created,
            content=f"Submitted task {submitted.task_id}.\n",
        )

        poll_interval = float(self.settings.openai_compat_agent_poll_interval_seconds)
        deadline = time.monotonic() + float(self.settings.openai_compat_agent_timeout_seconds)
        last_status_signature: tuple[str, str | None, int] | None = None
        while True:
            task = await task_service.get_status(submitted.task_id)
            if task is None:
                raise OpenAICompatHTTPError(
                    f"Task `{submitted.task_id}` disappeared before completion.",
                    status_code=500,
                    error_type="server_error",
                    code="agent_task_missing",
                )
            status_value = getattr(task.status, "value", str(task.status))
            current_signature = (status_value, task.current_step, task.attempt)
            if current_signature != last_status_signature:
                yield self._chunk_payload(
                    response_id,
                    response_model,
                    created=created,
                    content=self._format_agent_status(task.task_id, status_value, task.current_step, task.attempt),
                )
                last_status_signature = current_signature
            if status_value in {"completed", "failed", "cancelled"}:
                break
            if time.monotonic() >= deadline:
                raise OpenAICompatHTTPError(
                    "Timed out waiting for the AIIQ agent task to finish. Ensure the worker is running and try again.",
                    status_code=504,
                    error_type="server_error",
                    code="agent_timeout",
                )
            await self._sleep(poll_interval)

        final_task = await task_service.wait_for_completion(submitted.task_id, timeout_seconds=0.0)
        yield self._chunk_payload(
            response_id,
            response_model,
            created=created,
            content=self._format_agent_completion(final_task),
        )
        yield self._chunk_payload(
            response_id,
            response_model,
            created=created,
            finish_reason="stop",
        )
        yield "data: [DONE]\n\n"

    def _build_agent_task_request(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None,
        client_context: dict[str, Any] | None = None,
    ) -> TaskRequest:
        normalized_context = client_context or self._extract_client_context(request, headers=headers)
        repo_path = self._resolve_agent_repo_path(request, headers=headers, client_context=normalized_context)
        max_attempts = self._resolve_agent_max_attempts(request, headers=headers)
        engine = self._resolve_agent_engine(request, headers=headers)
        extra: dict[str, Any] = {}
        if normalized_context:
            extra["client_context"] = normalized_context
        constraints = TaskConstraints(max_attempts=max_attempts, engine=engine, extra=extra)
        return TaskRequest(
            goal=self._build_agent_goal(request, client_context=normalized_context),
            repo_path=repo_path,
            constraints=constraints,
        )

    def _build_agent_goal(self, request: ChatCompletionRequest, *, client_context: dict[str, Any] | None = None) -> str:
        latest_user = ""
        transcript_lines: list[str] = []
        for message in request.messages:
            content = self._message_content_to_text(message.content)
            if not content:
                continue
            if message.role == "user":
                normalized_user_content = self._extract_primary_user_text(content)
                if not normalized_user_content:
                    continue
                latest_user = normalized_user_content
                transcript_lines.append(f"USER: {normalized_user_content}")

        if not transcript_lines:
            raise OpenAICompatHTTPError(
                "The request must include at least one message with content.",
                status_code=400,
                code="invalid_messages",
                param="messages",
            )

        primary_goal = latest_user or self._extract_primary_user_text(
            self._message_content_to_text(request.messages[-1].content)
        )
        if not primary_goal:
            primary_goal = "Execute the requested coding task from the conversation context."
        goal_text = primary_goal
        if len(transcript_lines) > 1:
            goal_text += "\n\nConversation context:\n" + "\n".join(transcript_lines)
        context_summary = self._format_client_context_summary(client_context)
        if context_summary:
            goal_text += "\n\nClient context:\n" + context_summary
        return goal_text

    @staticmethod
    def _extract_primary_user_text(content: str) -> str:
        stripped = content.strip()
        if not stripped:
            return ""
        task_match = _TASK_BLOCK_PATTERN.search(stripped)
        if task_match:
            return task_match.group(1).strip()
        return _ENVIRONMENT_DETAILS_PATTERN.sub("", stripped).strip()

    def _resolve_agent_repo_path(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None,
        client_context: dict[str, Any] | None = None,
    ) -> Path:
        raw_value = self._request_override(
            request,
            headers=headers,
            header_name="X-AIIQ-Repo-Path",
            body_keys=("aiiq_repo_path", "repo_path", "workspace_path"),
        )
        candidates: list[Any] = [raw_value]
        if client_context:
            candidates.extend(
                [
                    client_context.get("workspace_root"),
                    client_context.get("current_working_directory"),
                ]
            )
        candidates.append(self.settings.openai_compat_agent_repo_path)

        fallback_path: Path | None = None
        for candidate in candidates:
            if candidate in (None, ""):
                continue
            path = Path(str(candidate)).expanduser()
            if fallback_path is None:
                fallback_path = path
            if path.exists() and path.is_dir():
                return path

        if fallback_path is None:
            raise OpenAICompatHTTPError(
                "The `aiiq-agent` model requires a repo path. Set AIIQ_OPENAI_COMPAT_AGENT_REPO_PATH in .env or provide X-AIIQ-Repo-Path.",
                status_code=400,
                code="agent_repo_path_required",
                param="model",
            )
        return fallback_path

    def _resolve_agent_max_attempts(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None,
    ) -> int:
        raw_value = self._request_override(
            request,
            headers=headers,
            header_name="X-AIIQ-Max-Attempts",
            body_keys=("aiiq_max_attempts", "max_attempts"),
        )
        if raw_value in (None, ""):
            raw_value = self.settings.openai_compat_agent_max_attempts or self.settings.default_max_attempts
        return self._parse_bounded_int(raw_value, minimum=1, maximum=10, param="max_attempts")

    def _resolve_agent_engine(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None,
    ) -> str | None:
        raw_value = self._request_override(
            request,
            headers=headers,
            header_name="X-AIIQ-Engine",
            body_keys=("aiiq_engine", "engine"),
        )
        if raw_value in (None, ""):
            raw_value = self.settings.openai_compat_agent_engine
        if raw_value in (None, ""):
            return None
        normalized = str(raw_value).strip()
        if normalized not in {"langgraph", "microsoft"}:
            raise OpenAICompatHTTPError(
                "Unsupported AIIQ agent engine. Allowed values: langgraph, microsoft.",
                status_code=400,
                code="invalid_engine",
                param="engine",
            )
        return normalized

    def _request_override(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None,
        header_name: str,
        body_keys: tuple[str, ...],
    ) -> Any:
        if headers is not None:
            header_value = headers.get(header_name)
            if header_value not in (None, ""):
                return header_value
        extras = request.model_extra or {}
        for key in body_keys:
            if extras.get(key) not in (None, ""):
                return extras[key]
        metadata = extras.get("metadata")
        if isinstance(metadata, dict):
            for key in body_keys:
                if metadata.get(key) not in (None, ""):
                    return metadata[key]
        return None

    def _is_cline_protocol_request(
        self,
        request: ChatCompletionRequest,
        *,
        client_context: dict[str, Any] | None = None,
    ) -> bool:
        if client_context:
            if str(client_context.get("response_protocol", "")).lower() == "cline":
                return True
            if str(client_context.get("client", "")).lower() == "cline":
                return True
        for message in request.messages:
            content = self._message_content_to_text(message.content)
            if content and _CLINE_PROTOCOL_PATTERN.search(content):
                return True
        return False

    def _extract_client_context(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None,
    ) -> dict[str, Any]:
        explicit = self._metadata_client_context(request, headers=headers)
        cline_context = self._cline_environment_client_context(request)
        merged: dict[str, Any] = {}
        merged.update(cline_context)
        merged.update(explicit)
        return self._normalize_client_context(merged)

    def _metadata_client_context(
        self,
        request: ChatCompletionRequest,
        *,
        headers: Mapping[str, str] | None,
    ) -> dict[str, Any]:
        extras = request.model_extra or {}
        metadata = extras.get("metadata")
        context: dict[str, Any] = {}
        if isinstance(metadata, dict):
            nested = metadata.get("client_context")
            if isinstance(nested, dict):
                self._merge_context_values(context, nested)
            self._merge_context_values(
                context,
                {
                    "client": metadata.get("client"),
                    "response_protocol": metadata.get("response_protocol"),
                    "workspace_root": metadata.get("workspace_root") or metadata.get("cwd"),
                    "current_working_directory": metadata.get("current_working_directory"),
                    "active_file": metadata.get("active_file"),
                    "selected_files": metadata.get("selected_files"),
                    "open_tabs": metadata.get("open_tabs"),
                    "visible_files": metadata.get("visible_files"),
                    "selected_text": metadata.get("selected_text"),
                    "shell": metadata.get("shell"),
                    "os": metadata.get("os"),
                    "approval_mode": metadata.get("approval_mode"),
                },
            )
        self._merge_context_values(
            context,
            {
                "client": extras.get("client"),
                "response_protocol": extras.get("response_protocol"),
                "workspace_root": extras.get("workspace_root") or extras.get("cwd"),
                "current_working_directory": extras.get("current_working_directory"),
                "active_file": extras.get("active_file"),
                "selected_files": extras.get("selected_files"),
                "open_tabs": extras.get("open_tabs"),
                "visible_files": extras.get("visible_files"),
                "selected_text": extras.get("selected_text"),
                "shell": extras.get("shell"),
                "os": extras.get("os"),
                "approval_mode": extras.get("approval_mode"),
            },
        )
        if headers is not None:
            self._merge_context_values(
                context,
                {
                    "client": headers.get("X-AIIQ-Client"),
                    "response_protocol": headers.get("X-AIIQ-Response-Protocol"),
                    "workspace_root": headers.get("X-AIIQ-Workspace-Root"),
                    "active_file": headers.get("X-AIIQ-Active-File"),
                    "shell": headers.get("X-AIIQ-Shell"),
                    "os": headers.get("X-AIIQ-OS"),
                    "approval_mode": headers.get("X-AIIQ-Approval-Mode"),
                },
            )
        return context

    @staticmethod
    def _merge_context_values(target: dict[str, Any], updates: Mapping[str, Any]) -> None:
        for key, value in updates.items():
            if value in (None, ""):
                continue
            target[key] = value

    def _cline_environment_client_context(self, request: ChatCompletionRequest) -> dict[str, Any]:
        context: dict[str, Any] = {}
        for message in request.messages:
            content = self._message_content_to_text(message.content)
            if not content:
                continue
            if not _CLINE_PROTOCOL_PATTERN.search(content):
                continue
            context.setdefault("client", "cline")
            context.setdefault("response_protocol", "cline")
            match = _ENVIRONMENT_BLOCK_PATTERN.search(content)
            if match is None:
                continue
            block = match.group(1)
            cwd_match = _CWD_PATTERN.search(block)
            if cwd_match:
                context.setdefault("workspace_root", cwd_match.group(1).strip())
                context.setdefault("current_working_directory", cwd_match.group(1).strip())
            visible_files = self._extract_cline_section_lines(block, "Visual Studio Code Visible Files")
            open_tabs = self._extract_cline_section_lines(block, "Visual Studio Code Open Tabs")
            if visible_files:
                context.setdefault("visible_files", visible_files)
            if open_tabs:
                context.setdefault("open_tabs", open_tabs)
                context.setdefault("active_file", open_tabs[0])
            current_mode = self._extract_cline_section_lines(block, "Current Mode")
            if current_mode:
                context.setdefault("approval_mode", current_mode[0])
        return context

    @staticmethod
    def _extract_cline_section_lines(block: str, heading: str) -> list[str]:
        marker = f"# {heading}"
        capture = False
        lines: list[str] = []
        for raw_line in block.splitlines():
            line = raw_line.rstrip()
            if line.strip() == marker:
                capture = True
                lines = []
                continue
            if capture and line.startswith("# "):
                break
            if not capture:
                continue
            stripped = line.strip()
            if not stripped or stripped.startswith("```"):
                continue
            if stripped.startswith("(No ") and stripped.endswith(")"):
                return []
            lines.append(stripped)
        return lines

    def _normalize_client_context(self, context: Mapping[str, Any]) -> dict[str, Any]:
        normalized: dict[str, Any] = {}
        for key in ("client", "response_protocol", "workspace_root", "current_working_directory", "active_file", "selected_text", "shell", "os", "approval_mode"):
            value = context.get(key)
            if isinstance(value, str) and value.strip():
                normalized[key] = value.strip()

        workspace_root = normalized.get("workspace_root") or normalized.get("current_working_directory")
        for key in ("active_file",):
            if key in normalized:
                normalized[key] = self._normalize_client_path(normalized[key], workspace_root=workspace_root)

        for key in ("selected_files", "open_tabs", "visible_files"):
            values = self._normalize_string_list(context.get(key))
            if values:
                normalized[key] = [self._normalize_client_path(value, workspace_root=workspace_root) for value in values]

        if "active_file" not in normalized:
            open_tabs = normalized.get("open_tabs")
            if isinstance(open_tabs, list) and open_tabs:
                normalized["active_file"] = open_tabs[0]
        if normalized.get("client") == "cline" and "response_protocol" not in normalized:
            normalized["response_protocol"] = "cline"
        return normalized

    @staticmethod
    def _normalize_string_list(value: Any) -> list[str]:
        if value is None:
            return []
        if isinstance(value, str):
            candidate = value.strip()
            return [candidate] if candidate else []
        if not isinstance(value, list):
            return []
        result: list[str] = []
        seen: set[str] = set()
        for item in value:
            if not isinstance(item, str):
                continue
            candidate = item.strip()
            if not candidate or candidate in seen:
                continue
            seen.add(candidate)
            result.append(candidate)
            if len(result) >= 20:
                break
        return result

    @staticmethod
    def _normalize_client_path(path: str, *, workspace_root: str | None) -> str:
        normalized = path.strip().replace("\\", "/")
        if workspace_root:
            try:
                workspace = Path(workspace_root)
                candidate = Path(path)
                if candidate.is_absolute():
                    relative = candidate.relative_to(workspace)
                    return relative.as_posix()
            except (ValueError, OSError):
                pass
            workspace_prefix = workspace_root.strip().replace("\\", "/").rstrip("/")
            if normalized.lower().startswith(workspace_prefix.lower() + "/"):
                return normalized[len(workspace_prefix) + 1 :]
        return normalized

    @staticmethod
    def _format_client_context_summary(client_context: Mapping[str, Any] | None) -> str:
        if not client_context:
            return ""
        lines: list[str] = []
        for key in ("client", "response_protocol", "workspace_root", "active_file", "shell", "os", "approval_mode"):
            value = client_context.get(key)
            if isinstance(value, str) and value:
                lines.append(f"- {key}: {value}")
        for key in ("selected_files", "open_tabs"):
            value = client_context.get(key)
            if isinstance(value, list) and value:
                lines.append(f"- {key}: {', '.join(value[:5])}")
        return "\n".join(lines)

    def _wrap_completion_for_cline(self, response: ChatCompletionResponse) -> None:
        if not response.choices:
            return
        message = response.choices[0].message
        content = self._message_content_to_text(message.content)
        message.content = self._cline_attempt_completion_payload(content)

    @staticmethod
    def _cline_attempt_completion_payload(content: str) -> str:
        stripped = content.strip()
        if not stripped:
            stripped = "Completed."
        if "<attempt_completion>" in stripped or "<ask_followup_question>" in stripped:
            return stripped
        return f"<attempt_completion>\n<result>\n{stripped}\n</result>\n</attempt_completion>"

    def _single_content_stream_response(
        self,
        response_model: str,
        content: str,
        *,
        response_id: str | None = None,
        created: int | None = None,
    ) -> str:
        payload = ChatCompletionChunkResponse(
            id=response_id or f"chatcmpl-{uuid4().hex}",
            created=created or int(time.time()),
            model=response_model,
            choices=[
                ChatCompletionChunkChoice(
                    index=0,
                    delta=ChatCompletionChunkDelta(role="assistant", content=content),
                    finish_reason="stop",
                )
            ],
        )
        return f"data: {payload.model_dump_json(exclude_none=True)}\n\n"

    def _collapse_stream_content(self, chunks: list[Any]) -> str:
        parts: list[str] = []
        for chunk in chunks:
            for choice in self._list_value(self._value(chunk, "choices")):
                delta = self._value(choice, "delta")
                content = self._content_value(self._value(delta, "content"))
                if isinstance(content, str) and content:
                    parts.append(content)
        return "".join(parts)

    def _require_task_service(self) -> TaskService:
        if self.task_service is None:
            raise OpenAICompatHTTPError(
                "The AIIQ agent mode is unavailable because no task service is configured.",
                status_code=503,
                error_type="server_error",
                code="agent_unavailable",
            )
        return self.task_service

    def _format_agent_completion(self, task: TaskState) -> str:
        status_value = getattr(task.status, "value", str(task.status))
        lines = [
            f"Task ID: {task.task_id}",
            f"Status: {status_value}",
        ]
        if task.summary:
            lines.append(f"Summary: {task.summary}")
        if task.error_message:
            lines.append(f"Error: {task.error_message}")
        if task.artifacts:
            lines.append("Artifacts:")
            for artifact in task.artifacts[:10]:
                description = f" ({artifact.description})" if artifact.description else ""
                lines.append(f"- {artifact.path}{description}")
            if len(task.artifacts) > 10:
                lines.append(f"- ... {len(task.artifacts) - 10} more")
        return "\n".join(lines)

    @staticmethod
    def _format_agent_status(task_id: str, status: str, current_step: str | None, attempt: int) -> str:
        parts = [f"Task {task_id}", f"status={status}", f"attempt={attempt}"]
        if current_step:
            parts.append(f"step={current_step}")
        return " | ".join(parts) + "\n"

    @staticmethod
    def _parse_bounded_int(value: Any, *, minimum: int, maximum: int, param: str) -> int:
        try:
            normalized = int(value)
        except (TypeError, ValueError) as exc:
            raise OpenAICompatHTTPError(
                f"`{param}` must be an integer.",
                status_code=400,
                code=f"invalid_{param}",
                param=param,
            ) from exc
        if normalized < minimum or normalized > maximum:
            raise OpenAICompatHTTPError(
                f"`{param}` must be between {minimum} and {maximum}.",
                status_code=400,
                code=f"invalid_{param}",
                param=param,
            )
        return normalized

    @staticmethod
    def _message_content_to_text(value: Any) -> str:
        if value is None:
            return ""
        if isinstance(value, str):
            return value.strip()
        if isinstance(value, list):
            parts: list[str] = []
            for item in value:
                if isinstance(item, dict):
                    text = item.get("text")
                    if isinstance(text, str) and text.strip():
                        parts.append(text.strip())
            return "\n".join(parts).strip()
        return str(value).strip()

    @staticmethod
    async def _sleep(seconds: float) -> None:
        import asyncio

        await asyncio.sleep(seconds)

    def _chunk_payload(
        self,
        response_id: str,
        response_model: str,
        *,
        created: int,
        role: str | None = None,
        content: str | None = None,
        finish_reason: str | None = None,
    ) -> str:
        payload = ChatCompletionChunkResponse(
            id=response_id,
            created=created,
            model=response_model,
            choices=[
                ChatCompletionChunkChoice(
                    index=0,
                    delta=ChatCompletionChunkDelta(role=role, content=content),
                    finish_reason=finish_reason,
                )
            ],
        )
        return f"data: {payload.model_dump_json(exclude_none=True)}\n\n"

    @staticmethod
    def _serialize_messages(request: ChatCompletionRequest) -> list[dict[str, Any]]:
        return [message.model_dump(exclude_none=True) for message in request.messages]

    def _completion_overrides(self, request: ChatCompletionRequest, *, stream: bool) -> dict[str, Any]:
        overrides: dict[str, Any] = {}
        if request.temperature is not None:
            overrides["temperature"] = request.temperature
        if request.top_p is not None:
            overrides["top_p"] = request.top_p
        if request.n is not None:
            overrides["n"] = request.n
        if request.stop is not None:
            overrides["stop"] = request.stop
        if request.max_tokens is not None:
            overrides["max_tokens"] = request.max_tokens
        elif request.max_completion_tokens is not None:
            overrides["max_tokens"] = request.max_completion_tokens
        if request.presence_penalty is not None:
            overrides["presence_penalty"] = request.presence_penalty
        if request.frequency_penalty is not None:
            overrides["frequency_penalty"] = request.frequency_penalty
        if request.tools is not None:
            overrides["tools"] = [tool.model_dump(exclude_none=True) for tool in request.tools]
        if request.tool_choice is not None:
            if isinstance(request.tool_choice, str):
                overrides["tool_choice"] = request.tool_choice
            else:
                overrides["tool_choice"] = request.tool_choice.model_dump(exclude_none=True)
        if request.parallel_tool_calls is not None:
            overrides["parallel_tool_calls"] = request.parallel_tool_calls
        if request.response_format is not None:
            overrides["response_format"] = request.response_format
        if request.seed is not None:
            overrides["seed"] = request.seed
        if request.user is not None:
            overrides["user"] = request.user
        if request.stream_options is not None:
            overrides["stream_options"] = request.stream_options
        overrides["stream"] = stream
        for key, value in (request.model_extra or {}).items():
            if key in _INTERNAL_REQUEST_KEYS:
                continue
            overrides[key] = value
        return overrides

    def _normalize_chat_completion_response(self, response: Any, response_model: str) -> ChatCompletionResponse:
        created = self._int_value(self._value(response, "created"), default=int(time.time()))
        choices = [
            ChatCompletionChoice(
                index=self._int_value(self._value(choice, "index"), default=index),
                message=self._normalize_response_message(self._value(choice, "message")),
                finish_reason=self._string_or_none(self._value(choice, "finish_reason")),
                logprobs=self._dict_or_none(self._value(choice, "logprobs")),
            )
            for index, choice in enumerate(self._list_value(self._value(response, "choices")))
        ]
        return ChatCompletionResponse(
            id=self._string_or_none(self._value(response, "id")) or f"chatcmpl-{uuid4().hex}",
            created=created,
            model=response_model or self._string_or_none(self._value(response, "model")) or "",
            choices=choices,
            usage=self._normalize_usage(self._value(response, "usage")),
            system_fingerprint=self._string_or_none(self._value(response, "system_fingerprint")),
        )

    def _normalize_chat_completion_chunk(self, chunk: Any, response_model: str) -> ChatCompletionChunkResponse:
        created = self._int_value(self._value(chunk, "created"), default=int(time.time()))
        choices = [
            ChatCompletionChunkChoice(
                index=self._int_value(self._value(choice, "index"), default=index),
                delta=self._normalize_delta(self._value(choice, "delta")),
                finish_reason=self._string_or_none(self._value(choice, "finish_reason")),
                logprobs=self._dict_or_none(self._value(choice, "logprobs")),
            )
            for index, choice in enumerate(self._list_value(self._value(chunk, "choices")))
        ]
        return ChatCompletionChunkResponse(
            id=self._string_or_none(self._value(chunk, "id")) or f"chatcmpl-{uuid4().hex}",
            created=created,
            model=response_model or self._string_or_none(self._value(chunk, "model")) or "",
            choices=choices,
            usage=self._normalize_usage(self._value(chunk, "usage")),
            system_fingerprint=self._string_or_none(self._value(chunk, "system_fingerprint")),
        )

    def _normalize_response_message(self, message: Any) -> ChatCompletionResponseMessage:
        return ChatCompletionResponseMessage(
            content=self._content_value(self._value(message, "content")),
            tool_calls=[
                ChatCompletionResponseToolCall(
                    id=self._string_or_none(self._value(tool_call, "id")) or f"call_{uuid4().hex[:12]}",
                    function=ChatCompletionResponseToolCallFunction(
                        name=self._string_or_none(self._value(self._value(tool_call, "function"), "name")) or "",
                        arguments=self._string_or_none(
                            self._value(self._value(tool_call, "function"), "arguments")
                        )
                        or "",
                    ),
                )
                for tool_call in self._list_value(self._value(message, "tool_calls"))
            ]
            or None,
            refusal=self._string_or_none(self._value(message, "refusal")),
        )

    def _normalize_delta(self, delta: Any) -> ChatCompletionChunkDelta:
        return ChatCompletionChunkDelta(
            role=self._string_or_none(self._value(delta, "role")),
            content=self._content_value(self._value(delta, "content")),
            refusal=self._string_or_none(self._value(delta, "refusal")),
            tool_calls=[
                ChatCompletionChunkDeltaToolCall(
                    index=self._int_value(self._value(tool_call, "index")),
                    id=self._string_or_none(self._value(tool_call, "id")),
                    function=ChatCompletionChunkDeltaToolCallFunction(
                        name=self._string_or_none(self._value(self._value(tool_call, "function"), "name")),
                        arguments=self._string_or_none(
                            self._value(self._value(tool_call, "function"), "arguments")
                        ),
                    ),
                )
                for tool_call in self._list_value(self._value(delta, "tool_calls"))
            ]
            or None,
        )

    @staticmethod
    def _normalize_usage(usage: Any) -> ChatCompletionUsage | None:
        if usage is None:
            return None
        return ChatCompletionUsage(
            prompt_tokens=OpenAICompatService._int_value(OpenAICompatService._value(usage, "prompt_tokens")),
            completion_tokens=OpenAICompatService._int_value(OpenAICompatService._value(usage, "completion_tokens")),
            total_tokens=OpenAICompatService._int_value(OpenAICompatService._value(usage, "total_tokens")),
        )

    @staticmethod
    def _value(obj: Any, key: str, default: Any = None) -> Any:
        if obj is None:
            return default
        if isinstance(obj, dict):
            return obj.get(key, default)
        return getattr(obj, key, default)

    @staticmethod
    def _list_value(value: Any) -> list[Any]:
        if value is None:
            return []
        if isinstance(value, list):
            return value
        if isinstance(value, tuple):
            return list(value)
        return [value]

    @staticmethod
    def _dict_or_none(value: Any) -> dict[str, Any] | None:
        if value is None:
            return None
        if isinstance(value, dict):
            return value
        model_dump = getattr(value, "model_dump", None)
        if callable(model_dump):
            return model_dump(exclude_none=True)
        return None

    @staticmethod
    def _content_value(value: Any) -> str | list[dict[str, Any]] | None:
        if value is None:
            return None
        if isinstance(value, str):
            return value
        if isinstance(value, list):
            normalized: list[dict[str, Any]] = []
            for item in value:
                if isinstance(item, dict):
                    normalized.append(item)
                    continue
                model_dump = getattr(item, "model_dump", None)
                if callable(model_dump):
                    normalized.append(model_dump(exclude_none=True))
            return normalized
        return str(value)

    @staticmethod
    def _string_or_none(value: Any) -> str | None:
        if value is None:
            return None
        if isinstance(value, str):
            return value
        if isinstance(value, (dict, list)):
            return json.dumps(value)
        return str(value)

    @staticmethod
    def _int_value(value: Any, default: int | None = None) -> int | None:
        if value is None:
            return default
        try:
            return int(value)
        except (TypeError, ValueError):
            return default
