from collections import Counter
from dataclasses import dataclass
from datetime import datetime
from functools import cache
from typing import Annotated, ClassVar

from loguru import logger
from pydantic import BaseModel, ConfigDict, TypeAdapter, ValidationError, model_validator
from pydantic.fields import FieldInfo

from bb_integrations_lib.gravitate.sd_api import GravitateSDAPI
from bb_integrations_lib.shared.model import GetOrderBolsAndDropsRequest


@cache
def _make_adapter(annotation) -> TypeAdapter:
    return TypeAdapter(annotation)


@dataclass
class ContractRequired:
    """Field is expected in every S&D response for this endpoint."""


class SDContractModel(BaseModel):
    model_config = ConfigDict(extra="allow")
    _endpoint: ClassVar[str]
    _drift_counts: ClassVar[Counter] = Counter()

    @model_validator(mode="before")
    @classmethod
    def _check_contract(cls, data: dict) -> dict:
        if not isinstance(data, dict):
            return data

        for name, field_info in cls.model_fields.items():
            if not any(isinstance(m, ContractRequired) for m in field_info.metadata):
                continue

            endpoint = getattr(cls, "_endpoint", "unknown")

            if name not in data:
                SDContractModel._drift_counts[(endpoint, name, "missing")] += 1
                continue

            if not cls._valid_for_type(data[name], field_info):
                SDContractModel._drift_counts[(endpoint, name, "type_mismatch")] += 1

        return data

    @classmethod
    def _valid_for_type(cls, value: object, field_info: FieldInfo) -> bool:
        """Returns True if value is acceptable for this field per Pydantic's coercion rules."""
        try:
            _make_adapter(field_info.annotation).validate_python(value)
            return True
        except ValidationError:
            return False


class SDAllocatedBol(SDContractModel):
    _endpoint = "get_bols_and_drops"

    bol_number: Annotated[str | None, ContractRequired()] = None
    bol_id: Annotated[str | None, ContractRequired()] = None
    bol_gross_volume_original: Annotated[float | None, ContractRequired()] = None
    bol_date: Annotated[datetime | None, ContractRequired()] = None
    bol_supplier_id: Annotated[str | None, ContractRequired()] = None
    bol_terminal_id: str | None = None
    bol_product_id: str | None = None
    bol_this_field_does_not_exist: Annotated[float | None, ContractRequired()] = None  # TEST: simulate missing field


class SDBolsAndDropsOrder(SDContractModel):
    _endpoint = "get_bols_and_drops"

    order_number: Annotated[str | int | None, ContractRequired()] = None
    allocated_bols: list[SDAllocatedBol] = []
    bols: list[dict] = []
    drops: list[dict] = []


class SDOrder(SDContractModel):
    _endpoint = "get_orders_by_numbers"

    loads: list[dict] = []
    drops: list[dict] = []
    order_this_field_does_not_exist: Annotated[str | None, ContractRequired()] = None  # TEST: simulate missing field


class SDAdapter:
    """
    Typed adapter over GravitateSDAPI.

    Wraps S&D endpoints and validates responses against declared Pydantic models
    (SDContractModel subclasses). Contract drift — missing or wrong-typed fields —
    is accumulated during batch processing and logged as a single summary per call,
    keyed as `sd_api_contract_drift` for log monitoring.

    Add new endpoint methods here as additional S&D data domains are needed.
    """

    def __init__(self, sd_api: GravitateSDAPI):
        self._api = sd_api

    async def get_bols_and_drops(self, request: GetOrderBolsAndDropsRequest) -> list[SDBolsAndDropsOrder]:
        resp = await self._api.get_bols_and_drops(request)
        result = [SDBolsAndDropsOrder(**item) for item in resp.json()]
        self._flush_drift_log()
        return result

    async def get_orders_by_numbers(self, order_numbers: list[str]) -> dict[str, SDOrder]:
        raw = await self._api.get_orders_by_numbers(order_numbers)
        result = {k: SDOrder(**v) for k, v in raw.items()}
        self._flush_drift_log()
        return result

    def _flush_drift_log(self) -> None:
        counts = SDContractModel._drift_counts.copy()
        SDContractModel._drift_counts.clear()
        for (endpoint, field, reason), count in counts.items():
            logger.warning(
                f"event=sd_api_contract_drift endpoint={endpoint} field={field} "
                f"reason={reason} count={count}"
            )
