"""Parse OpenAPI/Swagger specs to auto-detect body_template and response_path."""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import httpx
import yaml

PAYLOAD_FIELD_NAMES = (
    "prompt", "message", "query", "input", "text",
    "question", "content", "user_message", "request",
)

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


class OpenAPIParseError(Exception):
    """Raised when an OpenAPI spec cannot be parsed."""


@dataclass
class EndpointInfo:
    path: str
    summary: str
    body_template: str
    response_path: str
    raw_request_schema: dict[str, Any] = field(default_factory=dict)
    raw_response_schema: dict[str, Any] = field(default_factory=dict)


# ---------------------------------------------------------------------------
# Spec loading
# ---------------------------------------------------------------------------


def load_spec(source: str) -> dict[str, Any]:
    """Load an OpenAPI spec from a URL or local file path."""
    source = source.strip()
    if source.startswith(("http://", "https://")):
        try:
            resp = httpx.get(source, timeout=15, follow_redirects=True)
            resp.raise_for_status()
            text = resp.text
        except httpx.HTTPError as e:
            raise OpenAPIParseError(f"Failed to fetch spec: {e}") from e
    else:
        path = Path(source)
        if not path.exists():
            raise OpenAPIParseError(f"File not found: {source}")
        text = path.read_text()

    try:
        data = yaml.safe_load(text)
    except yaml.YAMLError as e:
        raise OpenAPIParseError(f"Invalid YAML/JSON: {e}") from e

    if not isinstance(data, dict):
        raise OpenAPIParseError("Spec must be a YAML/JSON object")
    return data


# ---------------------------------------------------------------------------
# Ref resolution
# ---------------------------------------------------------------------------


def _resolve_ref(spec: dict[str, Any], ref: str, _depth: int = 0) -> dict[str, Any]:
    """Resolve a $ref pointer like #/components/schemas/Foo."""
    if _depth > 10:
        raise OpenAPIParseError("Circular $ref detected")
    parts = ref.lstrip("#/").split("/")
    node: Any = spec
    for part in parts:
        if isinstance(node, dict):
            node = node.get(part, {})
        else:
            return {}
    if isinstance(node, dict) and "$ref" in node:
        return _resolve_ref(spec, node["$ref"], _depth + 1)
    return node if isinstance(node, dict) else {}


def _resolve_schema(spec: dict[str, Any], schema: dict[str, Any]) -> dict[str, Any]:
    """Resolve a schema, following $ref and merging allOf."""
    if "$ref" in schema:
        return _resolve_ref(spec, schema["$ref"])
    if "allOf" in schema:
        merged: dict[str, Any] = {}
        merged_props: dict[str, Any] = {}
        for sub in schema["allOf"]:
            resolved = _resolve_schema(spec, sub)
            merged_props.update(resolved.get("properties", {}))
            merged.update(resolved)
        merged["properties"] = merged_props
        return merged
    if "oneOf" in schema and schema["oneOf"]:
        return _resolve_schema(spec, schema["oneOf"][0])
    if "anyOf" in schema and schema["anyOf"]:
        return _resolve_schema(spec, schema["anyOf"][0])
    return schema


# ---------------------------------------------------------------------------
# Version detection
# ---------------------------------------------------------------------------


def _detect_spec_version(spec: dict[str, Any]) -> str:
    if "openapi" in spec and str(spec["openapi"]).startswith("3."):
        return "3.x"
    if "swagger" in spec and str(spec["swagger"]).startswith("2."):
        return "2.0"
    raise OpenAPIParseError(
        "Could not detect OpenAPI version. "
        "Expected 'openapi: 3.x.x' or 'swagger: 2.0'."
    )


# ---------------------------------------------------------------------------
# Schema extraction
# ---------------------------------------------------------------------------


def _extract_request_schema_v3(
    spec: dict[str, Any], path_key: str,
) -> dict[str, Any] | None:
    post = spec.get("paths", {}).get(path_key, {}).get("post", {})
    content = post.get("requestBody", {}).get("content", {})
    schema = content.get("application/json", {}).get("schema")
    if schema:
        return _resolve_schema(spec, schema)
    return None


def _extract_response_schema_v3(
    spec: dict[str, Any], path_key: str,
) -> dict[str, Any] | None:
    post = spec.get("paths", {}).get(path_key, {}).get("post", {})
    responses = post.get("responses", {})
    for status in ("200", "201", "default"):
        resp = responses.get(status, {})
        schema = resp.get("content", {}).get("application/json", {}).get("schema")
        if schema:
            return _resolve_schema(spec, schema)
    return None


def _extract_request_schema_v2(
    spec: dict[str, Any], path_key: str,
) -> dict[str, Any] | None:
    post = spec.get("paths", {}).get(path_key, {}).get("post", {})
    for param in post.get("parameters", []):
        if param.get("in") == "body":
            schema = param.get("schema")
            if schema:
                return _resolve_schema(spec, schema)
    return None


def _extract_response_schema_v2(
    spec: dict[str, Any], path_key: str,
) -> dict[str, Any] | None:
    post = spec.get("paths", {}).get(path_key, {}).get("post", {})
    responses = post.get("responses", {})
    for status in ("200", "201", "default"):
        schema = responses.get(status, {}).get("schema")
        if schema:
            return _resolve_schema(spec, schema)
    return None


# ---------------------------------------------------------------------------
# Body template generation
# ---------------------------------------------------------------------------


def _default_value(schema: dict[str, Any]) -> Any:
    """Return a sensible placeholder value for a schema type."""
    t = schema.get("type", "string")
    if t == "string":
        return ""
    if t in ("integer", "number"):
        return 0
    if t == "boolean":
        return False
    if t == "array":
        return []
    if t == "object":
        return {}
    return ""


def _find_payload_field(
    properties: dict[str, Any], spec: dict[str, Any],
) -> str:
    """Find the most likely payload field in request properties."""
    for candidate in PAYLOAD_FIELD_NAMES:
        if candidate in properties:
            return candidate
    # Fallback: first string-typed property
    for key, prop in properties.items():
        resolved = _resolve_schema(spec, prop)
        if resolved.get("type") == "string":
            return key
    return next(iter(properties))


def _build_body_template(
    spec: dict[str, Any], schema: dict[str, Any],
) -> str:
    """Build a body_template string from a request schema."""
    schema = _resolve_schema(spec, schema)
    properties = schema.get("properties", {})
    if not properties:
        return '{"prompt": "{{payload}}"}'

    # Detect OpenAI-style messages array
    if "messages" in properties:
        msg_schema = _resolve_schema(spec, properties["messages"])
        if msg_schema.get("type") == "array":
            obj: dict[str, Any] = {}
            for key, prop_schema in properties.items():
                prop_schema = _resolve_schema(spec, prop_schema)
                if key == "messages":
                    obj[key] = [{"role": "user", "content": "{{payload}}"}]
                else:
                    obj[key] = _default_value(prop_schema)
            return json.dumps(obj)

    payload_field = _find_payload_field(properties, spec)
    obj = {}
    for key, prop_schema in properties.items():
        prop_schema = _resolve_schema(spec, prop_schema)
        if key == payload_field:
            obj[key] = "{{payload}}"
        else:
            obj[key] = _default_value(prop_schema)
    return json.dumps(obj)


# ---------------------------------------------------------------------------
# Response path generation
# ---------------------------------------------------------------------------


def _build_response_path(
    spec: dict[str, Any], schema: dict[str, Any],
) -> str | None:
    """Build a JSONPath string from a response schema."""
    schema = _resolve_schema(spec, schema)
    properties = schema.get("properties", {})

    # Direct match at top level
    for candidate in RESPONSE_FIELD_NAMES:
        if candidate in properties:
            return f"$.{candidate}"

    # One level deep: nested objects and arrays
    for key, prop_schema in properties.items():
        resolved = _resolve_schema(spec, prop_schema)
        if resolved.get("type") == "object":
            nested = resolved.get("properties", {})
            for candidate in RESPONSE_FIELD_NAMES:
                if candidate in nested:
                    return f"$.{key}.{candidate}"
        elif resolved.get("type") == "array":
            items = resolved.get("items", {})
            items = _resolve_schema(spec, items)
            if items.get("type") == "object":
                nested = items.get("properties", {})
                for candidate in RESPONSE_FIELD_NAMES:
                    if candidate in nested:
                        return f"$.{key}[0].{candidate}"

    # Fallback: first string property
    for key, prop_schema in properties.items():
        resolved = _resolve_schema(spec, prop_schema)
        if resolved.get("type") == "string":
            return f"$.{key}"

    return None


# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------


def parse_openapi_spec(source: str) -> list[EndpointInfo]:
    """Parse an OpenAPI/Swagger spec and return info for POST endpoints."""
    spec = load_spec(source)
    version = _detect_spec_version(spec)

    endpoints: list[EndpointInfo] = []
    paths = spec.get("paths", {})

    for path_key, path_item in paths.items():
        if not isinstance(path_item, dict) or "post" not in path_item:
            continue

        post = path_item["post"]
        summary = (
            post.get("summary", "")
            or post.get("description", "")
            or path_key
        )

        if version == "3.x":
            req_schema = _extract_request_schema_v3(spec, path_key)
            resp_schema = _extract_response_schema_v3(spec, path_key)
        else:
            req_schema = _extract_request_schema_v2(spec, path_key)
            resp_schema = _extract_response_schema_v2(spec, path_key)

        body_template = (
            _build_body_template(spec, req_schema) if req_schema
            else '{"prompt": "{{payload}}"}'
        )
        response_path = (
            _build_response_path(spec, resp_schema) if resp_schema
            else None
        )

        endpoints.append(EndpointInfo(
            path=path_key,
            summary=summary[:80],
            body_template=body_template,
            response_path=response_path or "$.response",
            raw_request_schema=req_schema or {},
            raw_response_schema=resp_schema or {},
        ))

    return endpoints
