"""Field-value handlers.

Handlers dispatch by **source field type** (the field referenced by a
formula token) and produce the value that should be substituted into
the rendered expression string. Each handler operates on a per-call
cache passed in by the evaluator.
"""
from datetime import date, datetime
from typing import Any, Dict, List, Optional

from bson import ObjectId
from bson.errors import InvalidId
from motor.motor_asyncio import AsyncIOMotorDatabase

from .exceptions import MissingFieldError


class HandlerContext:
    """Per-call caches + DB handle. One instance per ``evaluate`` call."""

    def __init__(self, db: AsyncIOMotorDatabase):
        self.db = db
        self.field_cache: Dict[str, Dict[str, Any]] = {}
        self.record_cache: Dict[str, Dict[str, Any]] = {}

    async def get_field(self, field_id: str) -> Optional[Dict[str, Any]]:
        """Resolve a field definition from the ``fields`` collection with caching."""
        if not field_id or field_id == "_id":
            return None
        if field_id in self.field_cache:
            return self.field_cache[field_id]
        if self.db is None:
            return None
        try:
            oid = ObjectId(field_id)
        except (InvalidId, TypeError):
            return None
        doc = await self.db["fields"].find_one({"_id": oid})
        if doc:
            self.field_cache[field_id] = doc
        return doc

    async def get_record(self, entity_id: str, record_id: str) -> Optional[Dict[str, Any]]:
        """Resolve a record from the ``entity_<id>`` collection with caching."""
        if not entity_id or not record_id:
            return None
        key = f"{entity_id}_{record_id}"
        if key in self.record_cache:
            return self.record_cache[key]
        if self.db is None:
            return None
        try:
            oid = ObjectId(record_id)
        except (InvalidId, TypeError):
            return None
        doc = await self.db[f"entity_{entity_id}"].find_one({"_id": oid})
        if doc:
            self.record_cache[key] = doc
        return doc


def _resolve_options(raw: Any, field_def: Dict[str, Any]) -> Any:
    """Map dropdown/radio/checkbox UUID(s) to option label(s).

    Single-value field stores a string; multi-select stores a list.
    Returns a single label for scalar storage and a CSV-joined string
    for multi-select.
    """
    options = field_def.get("options") or []
    if not options:
        return raw
    value_to_label = {
        opt.get("value"): opt.get("label")
        for opt in options
        if opt.get("value") is not None
    }
    if isinstance(raw, list):
        labels: List[str] = []
        for v in raw:
            label = value_to_label.get(v)
            if label is None:
                continue
            labels.append(label)
        if not labels:
            return ""
        if len(labels) == 1:
            return labels[0]
        return ",".join(labels)
    return value_to_label.get(raw, raw)


def _parse_iso(val: Any) -> Any:
    """Best-effort parse for date/datetime strings; pass-through otherwise."""
    if isinstance(val, (datetime, date)):
        return val
    if isinstance(val, str):
        try:
            return datetime.fromisoformat(val.replace("Z", "+00:00"))
        except (ValueError, AttributeError):
            return val
    return val


async def handle_dropdown(field_value: Any, field_def: Dict[str, Any]) -> Any:
    """dropdown / radio / checkbox: UUID(s) → label(s)."""
    if field_value is None or field_value == "":
        return ""
    return _resolve_options(field_value, field_def)


async def handle_time_fields(field_value: Any, source_type: str) -> Any:
    """date / datetime / time / date_range / time_range."""
    if field_value is None:
        return None

    if source_type == "time_range":
        if not isinstance(field_value, dict):
            return ""
        start_time = field_value.get("start", "")
        end_time = field_value.get("end", "")
        return f"{start_time} - {end_time}"

    if source_type == "date_range":
        if not isinstance(field_value, dict):
            return ""
        from_date = field_value.get("from", "")
        to_date = field_value.get("to", "")
        return f"{from_date} - {to_date}"

    if source_type == "time":
        return field_value

    if source_type in {"date", "datetime"}:
        return _parse_iso(field_value)

    return field_value


async def handle_association(
    field_value: Any,
    field_def: Dict[str, Any],
    reference_field: Optional[str],
    ctx: HandlerContext,
) -> Any:
    """Association: optionally follow ``reference_field`` into the linked record."""
    if field_value is None or field_value == "":
        return ""

    if not reference_field:
        return field_value

    reference = field_def.get("reference") or {}
    referenced_entity_id = reference.get("entity_id")
    if not referenced_entity_id:
        return field_value

    referenced_ids = field_value if isinstance(field_value, list) else [field_value]
    resolved: List[Any] = []
    for rid in referenced_ids:
        if not rid:
            continue
        ref_record = await ctx.get_record(str(referenced_entity_id), str(rid))
        if not ref_record:
            continue
        inner_value = ref_record.get(reference_field)
        inner_field_def = await ctx.get_field(reference_field)
        if inner_field_def:
            inner_type = inner_field_def.get("type", "")
            inner_value = await dispatch_handler(
                inner_value, inner_field_def, inner_type, None, ctx
            )
        if inner_value is not None and inner_value != "":
            resolved.append(inner_value)

    if not resolved:
        return ""
    if len(resolved) == 1:
        return resolved[0]
    return resolved


async def handle_file(field_value: Any) -> Any:
    """File fields: return stored metadata shape verbatim (no blob fetch)."""
    if field_value is None:
        return []
    return field_value


async def handle_field_value(field_value: Any) -> Any:
    """Primitives + unknown types: pass through."""
    return field_value


async def dispatch_handler(
    field_value: Any,
    field_def: Optional[Dict[str, Any]],
    source_type: str,
    reference_field: Optional[str],
    ctx: HandlerContext,
) -> Any:
    """Route to the right handler based on the field's stored type."""
    if field_def is None:
        return await handle_field_value(field_value)

    if source_type in {"dropdown", "radio", "checkbox"}:
        return await handle_dropdown(field_value, field_def)

    if source_type in {"date", "datetime", "time", "date_range", "time_range"}:
        return await handle_time_fields(field_value, source_type)

    if source_type == "association":
        return await handle_association(field_value, field_def, reference_field, ctx)

    if source_type == "file":
        return await handle_file(field_value)

    if source_type in {"json", "location", "apo"}:
        return field_value if isinstance(field_value, (dict, list)) else {}

    return await handle_field_value(field_value)


async def resolve_field_token(
    token: Dict[str, Any],
    record: Dict[str, Any],
    ctx: HandlerContext,
) -> Any:
    """Resolve a single ``{is_field, field, reference_field?}`` token.

    Raises ``MissingFieldError`` when the record has no value for the field.
    """
    field_id = token.get("field") or ""
    if not field_id:
        raise ValueError("Formula field token has empty 'field' key")

    raw = record.get(field_id)
    if raw is None or raw == "":
        raise MissingFieldError(field_id=field_id)

    field_def = await ctx.get_field(field_id)
    source_type = field_def.get("type", "") if field_def else ""
    reference_field = token.get("reference_field")

    return await dispatch_handler(raw, field_def, source_type, reference_field, ctx)
