"""Normalized, transport-independent read contract for the AroFlo users zone."""

from __future__ import annotations

from datetime import datetime, timezone
from time import monotonic
from typing import Any


FILTERS = {
    "user_id": {"eq", "in"},
    "display_name": {"eq", "contains"},
    "given_names": {"eq", "contains"},
    "surname": {"eq", "contains"},
    "username": {"eq", "contains"},
    "email": {"eq", "contains"},
    "phone": {"eq", "contains"},
    "mobile": {"eq", "contains"},
    "position": {"eq", "contains"},
    "archived": {"eq"},
    "created_at": {"eq", "gte", "lte", "between"},
    "updated_at": {"eq", "gte", "lte", "between"},
}
SORT_FIELDS = {"display_name", "given_names", "surname", "username", "created_at", "updated_at"}


class UsersContractError(ValueError):
    def __init__(self, code: str, message: str):
        self.code = code
        super().__init__(message)


def query_users(client, request: dict[str, Any]) -> dict[str, Any]:
    started = monotonic()
    operation = str(request.get("operation") or "search").strip().lower()
    if operation not in {"list", "get", "search"}:
        raise UsersContractError("unsupported_operation", "The users operation is not supported.")
    if str(request.get("transport") or "auto").lower() not in {"auto", "api"}:
        raise UsersContractError("transport_unavailable", "The users read contract currently requires the API transport.")

    filters = _validate_filters(request.get("filters") or [])
    record_id = str(request.get("record_id") or "").strip()
    if operation == "get":
        if not record_id:
            raise UsersContractError("invalid_input", "record_id is required for users.get.")
        filters = [{"field": "user_id", "operator": "eq", "value": record_id}]

    pagination = request.get("pagination") or {}
    page = _bounded_int(pagination.get("page"), default=1, minimum=1, maximum=10_000)
    page_size = _bounded_int(pagination.get("page_size"), default=25, minimum=1, maximum=100)
    sort = _validate_sort(request.get("sort") or [])
    max_provider_pages = _bounded_int(request.get("max_provider_pages"), default=50, minimum=1, maximum=100)

    matches, provider_pages, exhausted = _scan_users(
        client, filters=filters, needed=1 if operation == "get" else page * page_size,
        max_provider_pages=max_provider_pages,
    )
    for rule in reversed(sort):
        matches.sort(
            key=lambda item: _text(item.get(rule["field"])),
            reverse=rule["direction"] == "desc",
        )
    start, end = (page - 1) * page_size, page * page_size
    selected = matches[start:end]
    if operation == "get":
        selected = matches[:1]

    return {
        "outcome": "completed",
        "item": selected[0] if operation == "get" and selected else None,
        "items": [] if operation == "get" else selected,
        "affected": 0,
        "job_id": None,
        "pagination": None if operation == "get" else {
            "page": page,
            "page_size": page_size,
            "returned": len(selected),
            "matched_scanned": len(matches),
            "has_more": len(matches) > end or not exhausted,
        },
        "meta": {
            "zone": "users",
            "operation": operation,
            "transport": "api",
            "bot_id": None,
            "duration_ms": round((monotonic() - started) * 1000),
            "provider_checked": True,
            "provider_pages": provider_pages,
            "filters_applied": filters,
        },
    }


def _scan_users(client, *, filters: list[dict], needed: int, max_provider_pages: int):
    matches: list[dict] = []
    exhausted = False
    for provider_page in range(1, max_provider_pages + 1):
        response = client.request("", method="GET", params=[
            ("zone", "users"),
            ("where", "and|createdutc|>|2001-01-01"),
            ("page", str(provider_page)),
            ("pageSize", "100"),
        ])
        raw_users = _extract_users(response)
        if not raw_users:
            exhausted = True
            return matches, provider_page, exhausted
        matches.extend(user for raw in raw_users if _matches(user := _normalize_user(raw), filters))
        if len(matches) >= needed:
            return matches, provider_page, exhausted
    return matches, max_provider_pages, exhausted


def _extract_users(response: Any) -> list[dict]:
    if not isinstance(response, dict):
        return []
    zone = response.get("zoneresponse")
    if not isinstance(zone, dict):
        return []
    users = zone.get("users")
    if isinstance(users, dict):
        users = [users]
    return [item for item in (users or []) if isinstance(item, dict)]


def _normalize_user(raw: dict) -> dict:
    given_names = _first(raw, "givennames", "givenNames", "firstname", "firstName")
    surname = _first(raw, "surname", "lastName", "lastname")
    display_name = _first(raw, "displayname", "displayName", "name") or f"{given_names} {surname}".strip()
    return {
        "user_id": _first(raw, "userid", "userId", "UserID", "id"),
        "display_name": display_name or _first(raw, "username", "UserName"),
        "given_names": given_names,
        "surname": surname,
        "username": _first(raw, "username", "UserName"),
        "email": _first(raw, "email", "emailaddress", "emailAddress"),
        "alternate_email": _first(raw, "email2", "alternateemailaddress", "alternateEmailAddress"),
        "phone": _first(raw, "phone", "phonenumber", "phoneNumber"),
        "mobile": _first(raw, "mobile", "mobilenumber", "mobileNumber"),
        "position": _first(raw, "position", "positionname", "positionName"),
        "archived": _boolean(_first(raw, "archived", "isArchived")),
        "created_at": _first(raw, "createdutc", "createdUtc", "created"),
        "updated_at": _first(raw, "lastupdatedutc", "lastUpdatedUtc", "updatedutc", "updatedUtc"),
    }


def _validate_filters(value: Any) -> list[dict]:
    if not isinstance(value, list) or len(value) > 20:
        raise UsersContractError("invalid_input", "filters must be an array with at most 20 entries.")
    output = []
    for item in value:
        if not isinstance(item, dict):
            raise UsersContractError("invalid_input", "Every filter must be an object.")
        field, operator = str(item.get("field") or ""), str(item.get("operator") or "")
        if field not in FILTERS or operator not in FILTERS[field]:
            raise UsersContractError("unsupported_filter", f"Unsupported users filter: {field}.{operator}")
        filter_value = item.get("value")
        if operator == "in" and (not isinstance(filter_value, list) or not filter_value):
            raise UsersContractError("invalid_input", "The in operator requires a non-empty array.")
        if operator == "between" and (not isinstance(filter_value, list) or len(filter_value) != 2):
            raise UsersContractError("invalid_input", "The between operator requires exactly two values.")
        output.append({"field": field, "operator": operator, "value": filter_value})
    return output


def _validate_sort(value: Any) -> list[dict]:
    if not isinstance(value, list) or len(value) > 3:
        raise UsersContractError("invalid_input", "sort must be an array with at most three entries.")
    output = []
    for item in value:
        if not isinstance(item, dict) or item.get("field") not in SORT_FIELDS or item.get("direction") not in {"asc", "desc"}:
            raise UsersContractError("invalid_input", "The users sort definition is invalid.")
        output.append({"field": item["field"], "direction": item["direction"]})
    return output


def _matches(user: dict, filters: list[dict]) -> bool:
    for rule in filters:
        actual, expected, operator = user.get(rule["field"]), rule["value"], rule["operator"]
        if operator == "eq" and _comparable(actual) != _comparable(expected):
            return False
        if operator == "contains" and _text(expected) not in _text(actual):
            return False
        if operator == "in" and _comparable(actual) not in {_comparable(item) for item in expected}:
            return False
        if operator in {"gte", "lte", "between"} and not _date_match(actual, expected, operator):
            return False
    return True


def _date_match(actual: Any, expected: Any, operator: str) -> bool:
    actual_date = _date(actual)
    if actual_date is None:
        return False
    if operator == "between":
        lower, upper = _date(expected[0]), _date(expected[1])
        return lower is not None and upper is not None and lower <= actual_date <= upper
    boundary = _date(expected)
    return boundary is not None and (actual_date >= boundary if operator == "gte" else actual_date <= boundary)


def _date(value: Any):
    text = str(value or "").strip().replace("Z", "+00:00")
    if not text:
        return None
    try:
        parsed = datetime.fromisoformat(text)
        return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
    except ValueError:
        try:
            return datetime.strptime(text[:10], "%Y-%m-%d").replace(tzinfo=timezone.utc)
        except ValueError:
            return None


def _first(raw: dict, *names: str) -> Any:
    for name in names:
        if name in raw and raw[name] is not None:
            return raw[name]
    lowered = {str(key).lower(): value for key, value in raw.items()}
    for name in names:
        if name.lower() in lowered and lowered[name.lower()] is not None:
            return lowered[name.lower()]
    return ""


def _boolean(value: Any) -> bool:
    return value is True or str(value).strip().lower() in {"1", "true", "yes", "y"}


def _text(value: Any) -> str:
    return " ".join(str(value or "").strip().lower().split())


def _comparable(value: Any):
    return value if isinstance(value, bool) else _text(value)


def _bounded_int(value: Any, *, default: int, minimum: int, maximum: int) -> int:
    try:
        number = int(value if value is not None else default)
    except (TypeError, ValueError) as exc:
        raise UsersContractError("invalid_input", "Pagination values must be integers.") from exc
    if number < minimum or number > maximum:
        raise UsersContractError("invalid_input", "A pagination value is outside its allowed range.")
    return number
