"""Convert OpenAPI ``components.schemas`` to LAPIS [types] section."""

from __future__ import annotations

from typing import Any

from lapis_spec.models.lapis import LapisEnumType, LapisField, LapisObjectType
from lapis_spec.models.openapi import OpenAPIDict, SchemaDict
from lapis_spec.parser.ref_resolver import count_refs, ref_to_schema_name
from lapis_spec.type_mapper import format_default_value, map_type


def convert_types(
    doc: OpenAPIDict,
) -> tuple[list[LapisEnumType | LapisObjectType], dict[str, str]]:
    """Build LAPIS type definitions from ``components.schemas``.

    Returns
    -------
    tuple
        A ``(types_list, schema_name_map)`` pair.  ``schema_name_map`` maps
        OpenAPI schema names to LAPIS type names for schemas that are promoted
        to named types (referenced more than once).  Schemas referenced only
        once should be inlined at the usage site.
    """
    schemas = doc.get("components", {}).get("schemas", {})
    if not schemas:
        return [], {}

    ref_counts = count_refs(doc)
    multi_ref_names = _schemas_referenced_multiple_times(ref_counts)

    all_names = set(schemas.keys())

    types: list[LapisEnumType | LapisObjectType] = []
    schema_name_map: dict[str, str] = {}

    for name in schemas:
        if name not in multi_ref_names and name not in all_names:
            continue

        schema = schemas[name]
        if not isinstance(schema, dict):
            continue

        if _is_enum(schema):
            enum_type = _convert_enum(name, schema)
            types.append(enum_type)
            schema_name_map[name] = name
        elif _is_object(schema):
            obj_type = _convert_object(name, schema, schemas)
            types.append(obj_type)
            schema_name_map[name] = name

    return types, schema_name_map


def _schemas_referenced_multiple_times(ref_counts: dict[str, int]) -> set[str]:
    result: set[str] = set()
    for ref, cnt in ref_counts.items():
        name = ref_to_schema_name(ref)
        if name and cnt > 1:
            result.add(name)
    return result


def _is_enum(schema: SchemaDict) -> bool:
    return "enum" in schema


def _is_object(schema: SchemaDict) -> bool:
    if schema.get("type") == "object" or "properties" in schema:
        return True
    return False


def _convert_enum(name: str, schema: SchemaDict) -> LapisEnumType:
    values = [str(v) for v in schema.get("enum", [])]
    return LapisEnumType(name=name, values=values)


def _convert_object(
    name: str, schema: SchemaDict, all_schemas: dict[str, SchemaDict]
) -> LapisObjectType:
    props = schema.get("properties", {})
    required_set = set(schema.get("required", []))
    fields: list[LapisField] = []

    for field_name, field_schema in props.items():
        if not isinstance(field_schema, dict):
            continue
        f = _convert_field(field_name, field_schema, field_name in required_set, all_schemas)
        fields.append(f)

    return LapisObjectType(name=name, fields=fields)


def _convert_field(
    name: str,
    schema: SchemaDict,
    is_required: bool,
    all_schemas: dict[str, SchemaDict],
) -> LapisField:
    type_expr = _resolve_field_type(schema, all_schemas)
    default = format_default_value(schema.get("default"))

    since = schema.get("x-since")
    if since is not None:
        since = str(since)

    deprecated = schema.get("deprecated", False) or schema.get("x-deprecated", False)
    deprecated_note = None
    x_dep = schema.get("x-deprecated")
    if isinstance(x_dep, str):
        deprecated_note = x_dep
    elif deprecated and schema.get("description"):
        pass

    return LapisField(
        name=name,
        type_expr=type_expr,
        optional=not is_required,
        default=default,
        since=since,
        deprecated=bool(deprecated),
        deprecated_note=deprecated_note,
    )


def _resolve_field_type(schema: SchemaDict, all_schemas: dict[str, SchemaDict]) -> str:
    """Determine the LAPIS type expression for a field's schema.

    If the schema looks like a reference to a known named schema, return that
    name directly.  Otherwise, use the type mapper.
    """
    ref_name = _match_known_schema(schema, all_schemas)
    if ref_name:
        return ref_name

    if schema.get("type") == "array":
        items = schema.get("items", {})
        if isinstance(items, dict):
            inner_name = _match_known_schema(items, all_schemas)
            if inner_name:
                return f"[{inner_name}]"

    return map_type(schema)


def _match_known_schema(schema: SchemaDict, all_schemas: dict[str, SchemaDict]) -> str | None:
    """Check if *schema* is structurally identical to a named schema.

    This is a best-effort heuristic: we check ``x-schema-name`` first
    (set during ref resolution), then fall back to structural equality.
    """
    stored = schema.get("x-schema-name")
    if stored and isinstance(stored, str) and stored in all_schemas:
        return stored

    if schema.get("type") == "object" and "properties" in schema:
        for name, candidate in all_schemas.items():
            if not isinstance(candidate, dict):
                continue
            if _schemas_equal(schema, candidate):
                return name
    return None


def _schemas_equal(a: dict[str, Any], b: dict[str, Any]) -> bool:
    """Shallow structural comparison of two schemas."""
    a_props = set(a.get("properties", {}).keys())
    b_props = set(b.get("properties", {}).keys())
    if not a_props or not b_props:
        return False
    return a_props == b_props
