"""Collect and deduplicate error responses into the LAPIS [errors] section."""

from __future__ import annotations

import json
from typing import Any

from lapis_spec.models.lapis import LapisError, LapisField
from lapis_spec.models.openapi import OpenAPIDict, SchemaDict
from lapis_spec.type_mapper import map_type
from lapis_spec.utils import to_snake_case


def convert_errors(
    doc: OpenAPIDict, schema_name_map: dict[str, str]
) -> tuple[list[LapisError], str | None]:
    """Collect all 4xx/5xx responses and deduplicate them.

    Returns
    -------
    tuple
        ``(errors, error_base_type)`` where *error_base_type* is the name
        of a common error schema extracted to [types], or ``None``.
    """
    paths = doc.get("paths", {})
    collected: dict[str, _ErrorEntry] = {}
    op_names: dict[str, str] = {}
    total_ops = 0

    for _path, path_item in paths.items():
        if not isinstance(path_item, dict):
            continue
        for method in ("get", "post", "put", "patch", "delete"):
            op = path_item.get(method)
            if not isinstance(op, dict):
                continue
            total_ops += 1
            op_name = op.get("operationId") or ""
            responses = op.get("responses", {})
            for code_str, resp in responses.items():
                if not isinstance(resp, dict):
                    continue
                code = _parse_code(code_str)
                if code is None or code < 400:
                    continue
                schema = _extract_error_schema(resp)
                key = _error_key(code, schema)
                if key not in collected:
                    collected[key] = _ErrorEntry(
                        code=code,
                        schema=schema,
                        description=resp.get("description"),
                        ops=set(),
                    )
                if op_name:
                    collected[key].ops.add(op_name)
                    op_names[op_name] = op_name

    error_base_type = _detect_common_error_type(collected, schema_name_map)
    errors = _build_errors(
        collected, total_ops, schema_name_map, has_base_type=error_base_type is not None
    )
    return errors, error_base_type


class _ErrorEntry:
    __slots__ = ("code", "schema", "description", "ops")

    def __init__(
        self,
        code: int,
        schema: SchemaDict | None,
        description: str | None,
        ops: set[str],
    ) -> None:
        self.code = code
        self.schema = schema
        self.description = description
        self.ops = ops


def _parse_code(code_str: str) -> int | None:
    try:
        return int(code_str)
    except ValueError:
        return None


def _extract_error_schema(resp: dict[str, Any]) -> SchemaDict | None:
    content = resp.get("content", {})
    json_content = content.get("application/json", {})
    schema = json_content.get("schema")
    if isinstance(schema, dict):
        return schema
    return None


def _error_key(code: int, schema: SchemaDict | None) -> str:
    if schema is None:
        return f"{code}:none"
    props = sorted(schema.get("properties", {}).keys())
    return f"{code}:{json.dumps(props, sort_keys=True)}"


def _detect_common_error_type(
    collected: dict[str, _ErrorEntry], schema_name_map: dict[str, str]
) -> str | None:
    """Check if all errors share the same schema structure."""
    schemas: list[SchemaDict] = []
    for entry in collected.values():
        if entry.schema and "properties" in entry.schema:
            schemas.append(entry.schema)

    if len(schemas) < 2:
        return None

    first_props = frozenset(schemas[0].get("properties", {}).keys())
    if all(frozenset(s.get("properties", {}).keys()) == first_props for s in schemas):
        for name in schema_name_map:
            if "error" in name.lower():
                return name
    return None


def _build_errors(
    collected: dict[str, _ErrorEntry],
    total_ops: int,
    schema_name_map: dict[str, str],
    has_base_type: bool = False,
) -> list[LapisError]:
    errors: list[LapisError] = []
    for entry in sorted(collected.values(), key=lambda e: e.code):
        name = _generate_error_name(entry)
        ops_list: list[str] = []
        if entry.ops and len(entry.ops) < total_ops:
            ops_list = sorted(entry.ops)

        fields: list[LapisField] = []
        if not has_base_type and entry.schema and "properties" in entry.schema:
            props = entry.schema.get("properties", {})
            for field_name, field_schema in props.items():
                if not isinstance(field_schema, dict):
                    continue
                type_expr = map_type(field_schema)
                oa_type = field_schema.get("type")
                nullable = (
                    field_schema.get("nullable", False)
                    or (isinstance(oa_type, list) and "null" in oa_type)
                )
                if nullable:
                    type_expr += "?"
                fields.append(LapisField(name=field_name, type_expr=type_expr))

        errors.append(
            LapisError(
                code=entry.code,
                name=name,
                description=entry.description,
                ops=ops_list,
                fields=fields,
            )
        )
    return errors


def _generate_error_name(entry: _ErrorEntry) -> str:
    desc = entry.description or ""
    desc_lower = desc.lower()

    name_map: dict[int, dict[str, str]] = {
        400: {"default": "validation_error"},
        401: {"default": "unauthorized"},
        403: {"default": "forbidden"},
        404: {"default": "not_found"},
        409: {"default": "conflict"},
        422: {"default": "unprocessable_entity"},
        429: {"default": "rate_limited"},
        500: {"default": "internal_error"},
        502: {"default": "bad_gateway"},
        503: {"default": "service_unavailable"},
        504: {"default": "gateway_timeout"},
    }

    code_names = name_map.get(entry.code, {})
    for keyword, name in code_names.items():
        if keyword == "default":
            continue
        if keyword in desc_lower:
            return name

    if code_names.get("default"):
        return code_names["default"]

    if desc:
        return to_snake_case(desc[:40])
    return f"error_{entry.code}"
