"""Convert OpenAPI ``paths`` to the LAPIS [ops] section."""

from __future__ import annotations

import re
from typing import Any

from lapis_spec.models.lapis import LapisField, LapisOperation
from lapis_spec.models.openapi import OpenAPIDict, OperationDict, SchemaDict
from lapis_spec.type_mapper import format_default_value, map_type
from lapis_spec.utils import generate_operation_id, pick_shorter, truncate_description

HTTP_METHODS = {"get", "post", "put", "patch", "delete"}


def convert_ops(
    doc: OpenAPIDict, schema_name_map: dict[str, str]
) -> list[LapisOperation]:
    """Build a list of :class:`LapisOperation` from OpenAPI ``paths``."""
    paths = doc.get("paths", {})
    operations: list[LapisOperation] = []

    for path, path_item in paths.items():
        if not isinstance(path_item, dict):
            continue
        for method in HTTP_METHODS:
            op_dict = path_item.get(method)
            if not isinstance(op_dict, dict):
                continue
            op = _convert_operation(method, path, op_dict, schema_name_map)
            operations.append(op)

    return operations


def _convert_operation(
    method: str,
    path: str,
    op: OperationDict,
    schema_name_map: dict[str, str],
) -> LapisOperation:
    name = op.get("operationId") or generate_operation_id(method, path)
    desc = truncate_description(pick_shorter(op.get("summary"), op.get("description")))
    modifiers = _extract_modifiers(op)
    since = _extract_since(op)

    path_params = set(re.findall(r"\{(\w+)\}", path))
    inputs = _extract_inputs(op, method, path_params, schema_name_map)
    outputs, output_ref = _extract_outputs(op, schema_name_map)

    return LapisOperation(
        name=name,
        method=method.upper(),
        path=path,
        description=desc,
        inputs=inputs,
        outputs=outputs,
        output_ref=output_ref,
        modifiers=modifiers,
        since=since,
    )


def _extract_modifiers(op: OperationDict) -> list[str]:
    mods: list[str] = []
    if op.get("deprecated"):
        note = op.get("x-deprecated-message", "")
        if note:
            mods.append(f'+deprecated "{note}"')
        else:
            mods.append("+deprecated")
    if op.get("x-paginated"):
        mods.append("+paginated")
    if op.get("x-idempotent"):
        mods.append("+idempotent")
    if op.get("x-stream"):
        mods.append("+stream")
    return mods


def _extract_since(op: OperationDict) -> str | None:
    since = op.get("x-since")
    return str(since) if since is not None else None


def _extract_inputs(
    op: OperationDict,
    method: str,
    path_params: set[str],
    schema_name_map: dict[str, str],
) -> list[LapisField]:
    fields: list[LapisField] = []

    for param in op.get("parameters", []):
        if not isinstance(param, dict):
            continue
        f = _convert_parameter(param, method, path_params, schema_name_map)
        if f is not None:
            fields.append(f)

    body = op.get("requestBody", {})
    if isinstance(body, dict):
        body_fields = _extract_request_body(body, schema_name_map)
        fields.extend(body_fields)

    return fields


def _convert_parameter(
    param: dict[str, Any],
    method: str,
    path_params: set[str],
    schema_name_map: dict[str, str],
) -> LapisField | None:
    name = param.get("name")
    if not name:
        return None

    location_in = param.get("in", "query")

    # Path params are implicit from the route {param} and not listed
    if name in path_params or location_in == "path":
        return None

    schema = param.get("schema", {})
    if not isinstance(schema, dict):
        schema = {}

    type_expr = _resolve_type(schema, schema_name_map)
    required = param.get("required", False)
    default = format_default_value(schema.get("default"))

    location: str | None = None
    if location_in == "header":
        location = "header"
    elif location_in == "query" and method.lower() in {"post", "put", "patch"}:
        location = "query"

    return LapisField(
        name=name,
        type_expr=type_expr,
        optional=not required,
        default=default,
        location=location,
    )


def _extract_request_body(
    body: dict[str, Any], schema_name_map: dict[str, str]
) -> list[LapisField]:
    content = body.get("content", {})
    json_content = content.get("application/json", {})
    schema = json_content.get("schema", {})
    if not isinstance(schema, dict):
        return []

    type_name = _get_schema_name(schema, schema_name_map)
    if type_name:
        return [LapisField(name="", type_expr=type_name, optional=False)]

    props = schema.get("properties", {})
    required_set = set(schema.get("required", []))
    fields: list[LapisField] = []

    for name, prop_schema in props.items():
        if not isinstance(prop_schema, dict):
            continue
        type_expr = _resolve_type(prop_schema, schema_name_map)
        default = format_default_value(prop_schema.get("default"))
        fields.append(
            LapisField(
                name=name,
                type_expr=type_expr,
                optional=name not in required_set,
                default=default,
            )
        )

    return fields


def _extract_outputs(
    op: OperationDict, schema_name_map: dict[str, str]
) -> tuple[list[LapisField], str | None]:
    """Extract output fields from the 2xx response.

    Returns ``(fields, output_ref)`` where *output_ref* is set when the
    entire response maps directly to a named type.
    """
    responses = op.get("responses", {})
    for code in ("200", "201", "202", "203", "204"):
        resp = responses.get(code)
        if not isinstance(resp, dict):
            continue
        content = resp.get("content", {})
        json_content = content.get("application/json", {})
        schema = json_content.get("schema", {})
        if not isinstance(schema, dict):
            continue

        type_name = _get_schema_name(schema, schema_name_map)
        if type_name:
            return [], type_name

        if schema.get("type") == "array":
            items = schema.get("items", {})
            if isinstance(items, dict):
                item_name = _get_schema_name(items, schema_name_map)
                if item_name:
                    return [], f"[{item_name}]"

        props = schema.get("properties", {})
        if not props:
            t = _resolve_type(schema, schema_name_map)
            if t and t != "any":
                return [], t
            continue

        fields: list[LapisField] = []
        for name, prop_schema in props.items():
            if not isinstance(prop_schema, dict):
                continue
            type_expr = _resolve_type(prop_schema, schema_name_map)
            fields.append(LapisField(name=name, type_expr=type_expr))
        return fields, None

    return [], None


def _resolve_type(schema: SchemaDict, schema_name_map: dict[str, str]) -> str:
    """Resolve a schema to a LAPIS type, preferring named types."""
    name = _get_schema_name(schema, schema_name_map)
    if name:
        return name

    if schema.get("type") == "array":
        items = schema.get("items", {})
        if isinstance(items, dict):
            inner = _get_schema_name(items, schema_name_map)
            if inner:
                return f"[{inner}]"

    return map_type(schema)


def _get_schema_name(schema: SchemaDict, schema_name_map: dict[str, str]) -> str | None:
    """Try to match a schema to a named LAPIS type."""
    stored = schema.get("x-schema-name")
    if stored and isinstance(stored, str) and stored in schema_name_map:
        return schema_name_map[stored]
    return None
