from __future__ import annotations

import json
import time
from typing import Any

import httpx

from leshy.core.models import TargetConfig, TargetResponse
from leshy.core.registry import target_registry
from leshy.targets.base import Target

# Preset body templates
_PRESETS: dict[str, dict[str, Any]] = {
    "openai_chat": {
        "body_template": json.dumps(
            {
                "model": "{{model}}",
                "messages": [{"role": "user", "content": "{{payload}}"}],
            }
        ),
        "response_path": "$.choices[0].message.content",
        "defaults": {"model": "gpt-4o-mini"},
    },
}


class JsonPathError(Exception):
    """Raised when JSONPath extraction fails."""


_COMMON_TEXT_FIELDS = (
    "text", "message", "content", "response", "answer",
    "output", "result", "reply", "data",
)


def _find_text_field(data: dict[str, Any]) -> str | None:
    """Try common field names to find a text response in JSON data."""
    if not isinstance(data, dict):
        return None
    for field in _COMMON_TEXT_FIELDS:
        val = data.get(field)
        if isinstance(val, str) and val:
            return val
    return None


def _extract_json_path(data: Any, path: str) -> str:
    """Extract a value from nested data using JSONPath-like syntax.

    Supports: $.key, $.key[0], $.key[-1], $.key.nested, $["dotted.key"]
    """
    if path.startswith("$."):
        raw = path[2:]
    elif path.startswith("$["):
        raw = path[1:]
    else:
        return str(data)
    parts: list[str] = []
    i = 0
    while i < len(raw):
        if raw[i] == ".":
            i += 1
        elif raw[i] == "[":
            end = raw.index("]", i)
            parts.append(raw[i : end + 1])
            i = end + 1
        else:
            # Read until next . or [
            end = i
            while end < len(raw) and raw[end] not in (".", "["):
                end += 1
            parts.append(raw[i:end])
            i = end

    current = data
    for part in parts:
        try:
            if part.startswith("[") and part.endswith("]"):
                inner = part[1:-1]
                # Quoted key: $["key.with.dots"]
                if (inner.startswith('"') and inner.endswith('"')) or (
                    inner.startswith("'") and inner.endswith("'")
                ):
                    current = current[inner[1:-1]]
                else:
                    current = current[int(inner)]
            else:
                current = current[part]
        except (KeyError, IndexError, TypeError) as e:
            raise JsonPathError(
                f"Failed to extract '{part}' from path '{path}': {e}"
            ) from e
    return str(current)


def _render_template(template: str, variables: dict[str, str]) -> str:
    """Replace {{var}} placeholders in a template string."""
    result = template
    for key, value in variables.items():
        result = result.replace("{{" + key + "}}", value)
    return result


@target_registry.register("http")
class HttpTarget(Target):
    """Generic HTTP target with body template rendering and response extraction."""

    name = "http"

    def __init__(self, config: TargetConfig, **kwargs: Any) -> None:
        super().__init__(config, **kwargs)
        self._client: httpx.AsyncClient | None = None
        self._preset_defaults: dict[str, str] = {}

        if config.preset and config.preset in _PRESETS:
            preset = _PRESETS[config.preset]
            if config.body_template is None:
                config.body_template = preset["body_template"]
            if config.response_path is None:
                config.response_path = preset.get("response_path")
            self._preset_defaults = preset.get("defaults", {})

    async def setup(self) -> None:
        self._client = httpx.AsyncClient(
            timeout=httpx.Timeout(self.config.timeout),
            headers=self.config.headers,
        )

    async def send(self, payload: str, *, thread_id: str | None = None) -> TargetResponse:
        if self._client is None:
            await self.setup()
        assert self._client is not None

        variables: dict[str, str] = {
            "payload": payload,
            **self._preset_defaults,
        }
        if thread_id:
            variables["thread_id"] = thread_id
            variables["session_id"] = thread_id

        body_str: str | None = None
        if self.config.body_template:
            body_str = _render_template(self.config.body_template, variables)

        start = time.monotonic()
        try:
            if body_str:
                resp = await self._client.request(
                    self.config.method,
                    self.config.url,
                    content=body_str,
                    headers={"Content-Type": "application/json"},
                )
            else:
                resp = await self._client.request(
                    self.config.method,
                    self.config.url,
                )
        except httpx.HTTPError as e:
            latency = (time.monotonic() - start) * 1000
            return TargetResponse(
                content=f"HTTP Error: {e}",
                raw={"error": str(e)},
                status_code=0,
                latency_ms=latency,
                thread_id=thread_id,
            )

        latency = (time.monotonic() - start) * 1000

        raw_data: dict[str, Any] = {}
        metadata: dict[str, Any] = {}
        content = resp.text
        try:
            raw_data = resp.json()
            if self.config.response_path:
                content = _extract_json_path(raw_data, self.config.response_path)
        except json.JSONDecodeError:
            if self.config.response_path:
                metadata["parse_warning"] = (
                    "Response is not valid JSON; evaluating raw text"
                )
        except JsonPathError:
            # JSONPath failed — try common field names as fallback
            fallback = _find_text_field(raw_data)
            if fallback is not None:
                content = fallback
                metadata["parse_warning"] = (
                    f"JSONPath '{self.config.response_path}' not found; "
                    f"auto-detected text field used instead"
                )
            else:
                metadata["parse_warning"] = (
                    f"JSONPath '{self.config.response_path}' not found; "
                    f"evaluating full response body"
                )

        return TargetResponse(
            content=content,
            raw=raw_data,
            status_code=resp.status_code,
            latency_ms=latency,
            thread_id=thread_id,
            metadata=metadata,
        )

    async def teardown(self) -> None:
        if self._client:
            await self._client.aclose()
            self._client = None
