"""Map OpenAPI type + format combinations to LAPIS scalar type strings."""

from __future__ import annotations

from typing import Any

from lapis_spec.models.openapi import SchemaDict

_FORMAT_MAP: dict[tuple[str, str | None], str] = {
    ("string", None): "str",
    ("string", "date"): "date",
    ("string", "date-time"): "datetime",
    ("string", "binary"): "file",
    ("string", "byte"): "str",
    ("string", "password"): "str",
    ("string", "uri"): "str",
    ("string", "url"): "str",
    ("string", "email"): "str",
    ("string", "uuid"): "str",
    ("integer", None): "int",
    ("integer", "int32"): "int",
    ("integer", "int64"): "int",
    ("number", None): "float",
    ("number", "float"): "float",
    ("number", "double"): "float",
    ("boolean", None): "bool",
}


def map_type(schema: SchemaDict, *, schema_names: dict[str, str] | None = None) -> str:
    """Convert an OpenAPI schema dict to a LAPIS type expression string.

    Parameters
    ----------
    schema:
        An OpenAPI schema object (already resolved / flattened).
    schema_names:
        Optional mapping of OpenAPI schema name to LAPIS type name for
        replacing inline references with named type references.

    Returns
    -------
    str
        A LAPIS type expression (e.g. ``str``, ``[Invoice]``, ``{str:any}``).
    """
    if not schema or not isinstance(schema, dict):
        return "any"

    if "enum" in schema:
        return _map_enum_inline(schema)

    oa_type = _get_type(schema)
    oa_format = schema.get("format")

    if oa_type == "array":
        return _map_array(schema, schema_names=schema_names)

    if oa_type == "object":
        return _map_object(schema, schema_names=schema_names)

    key = (oa_type, oa_format) if oa_format else (oa_type, None)
    lapis_type = _FORMAT_MAP.get(key)
    if lapis_type:
        return lapis_type

    if oa_format:
        return _FORMAT_MAP.get((oa_type, None), "any")

    return "any"


def map_type_with_nullable(
    schema: SchemaDict, *, schema_names: dict[str, str] | None = None
) -> str:
    """Like :func:`map_type` but appends ``?`` for nullable schemas."""
    base = map_type(schema, schema_names=schema_names)
    if _is_nullable(schema):
        return base + "?"
    return base


def _get_type(schema: SchemaDict) -> str:
    """Extract the primary type from a schema, handling OpenAPI 3.1 type arrays."""
    t = schema.get("type", "")
    if isinstance(t, list):
        non_null = [x for x in t if x != "null"]
        return non_null[0] if non_null else "any"
    return str(t) if t else ""


def _is_nullable(schema: SchemaDict) -> bool:
    """Check whether a schema allows null values."""
    if schema.get("nullable") is True:
        return True
    t = schema.get("type")
    if isinstance(t, list) and "null" in t:
        return True
    return False


def _map_enum_inline(schema: SchemaDict) -> str:
    """Map an inline enum to pipe-separated values for display only."""
    values = schema.get("enum", [])
    return " | ".join(str(v) for v in values)


def _map_array(schema: SchemaDict, *, schema_names: dict[str, str] | None = None) -> str:
    items = schema.get("items", {})
    if isinstance(items, dict):
        inner = map_type(items, schema_names=schema_names)
    else:
        inner = "any"
    return f"[{inner}]"


def _map_object(schema: SchemaDict, *, schema_names: dict[str, str] | None = None) -> str:
    additional = schema.get("additionalProperties")
    props = schema.get("properties")

    if additional and isinstance(additional, dict) and not props:
        value_type = map_type(additional, schema_names=schema_names)
        return "{str:" + value_type + "}"

    if additional is True and not props:
        return "{str:any}"

    if not props:
        return "any"

    return _inline_object(schema, schema_names=schema_names)


def _inline_object(schema: SchemaDict, *, schema_names: dict[str, str] | None = None) -> str:
    """Produce an inline object expression ``{field: type, ...}``."""
    props = schema.get("properties", {})
    required = set(schema.get("required", []))
    parts: list[str] = []
    for name, sub in props.items():
        if not isinstance(sub, dict):
            continue
        t = map_type(sub, schema_names=schema_names)
        opt = "?" if name not in required else ""
        parts.append(f"{name}{opt}: {t}")
    return "{" + ", ".join(parts) + "}"


def format_default_value(value: Any) -> str | None:
    """Format a default value for LAPIS output."""
    if value is None:
        return None
    if isinstance(value, bool):
        return "true" if value else "false"
    if isinstance(value, str):
        return f'"{value}"'
    return str(value)
