from __future__ import annotations

from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from typing import Literal, Protocol, runtime_checkable

from govoplan_core.core.access import (
    CAPABILITY_ACCESS_DIRECTORY,
    AccessDirectory,
    GroupRef,
    UserRef,
)


CAPABILITY_ACCESS_REFERENCE_OPTIONS = "access.reference_options"
ReferenceAvailability = Literal["available", "inactive", "unavailable"]


@dataclass(frozen=True, slots=True)
class ReferenceOption:
    """A stable identifier paired with presentation-only directory metadata."""

    value: str
    label: str
    description: str | None = None
    kind: str | None = None
    availability: ReferenceAvailability = "available"
    disabled: bool = False
    source_module: str | None = None
    provenance: Mapping[str, object] = field(default_factory=dict)

    def to_dict(self) -> dict[str, object]:
        return {
            "value": self.value,
            "label": self.label,
            "description": self.description,
            "kind": self.kind,
            "availability": self.availability,
            "disabled": self.disabled,
            "source_module": self.source_module,
            "provenance": dict(self.provenance),
        }


@dataclass(frozen=True, slots=True)
class ReferenceSearchRequest:
    kind: str
    tenant_id: str | None
    query: str = ""
    selected_values: tuple[str, ...] = ()
    limit: int = 50
    cursor: str | None = None
    context: Mapping[str, object] = field(default_factory=dict)


@dataclass(frozen=True, slots=True)
class ReferenceSearchPage:
    options: tuple[ReferenceOption, ...] = ()
    next_cursor: str | None = None
    has_more: bool = False


@runtime_checkable
class ReferenceOptionProvider(Protocol):
    """Optional module-neutral provider for typed reference selectors."""

    def search_reference_options(
        self,
        session: object,
        principal: object,
        *,
        request: ReferenceSearchRequest,
    ) -> ReferenceSearchPage:
        ...


def reference_option_provider(
    registry: object | None,
    capability_name: str,
) -> ReferenceOptionProvider | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not hasattr(registry, "capability")
        or not registry.has_capability(capability_name)
    ):
        return None
    provider = registry.capability(capability_name)
    return provider if isinstance(provider, ReferenceOptionProvider) else None


def access_scope_reference_options(
    registry: object | None,
    principal: object,
    *,
    scope_type: str,
    query: str = "",
    selected_values: Sequence[str] = (),
    limit: int = 50,
    administrative: bool = False,
    session: object | None = None,
    reference_kind: str | None = None,
) -> tuple[ReferenceOption, ...]:
    """Return canonical user-account or group references for definition scopes."""

    return access_scope_reference_page(
        registry,
        principal,
        scope_type=scope_type,
        query=query,
        selected_values=selected_values,
        limit=limit,
        administrative=administrative,
        session=session,
        reference_kind=reference_kind,
    ).options


def access_scope_reference_page(
    registry: object | None,
    principal: object,
    *,
    scope_type: str,
    query: str = "",
    selected_values: Sequence[str] = (),
    limit: int = 50,
    cursor: str | None = None,
    administrative: bool = False,
    session: object | None = None,
    reference_kind: str | None = None,
) -> ReferenceSearchPage:
    """Search bounded Access references while retaining selected values."""

    clean_type = str(scope_type or "").strip().casefold()
    if clean_type not in {"user", "group"}:
        raise ValueError("Scope reference type must be user or group.")
    clean_kind = str(reference_kind or clean_type).strip().casefold()
    if clean_kind not in (
        {"user", "membership"} if clean_type == "user" else {"group"}
    ):
        raise ValueError("Scope reference kind is incompatible with the scope type.")
    normalized_limit = max(1, min(int(limit), 200))
    selected = tuple(
        dict.fromkeys(
            str(value).strip() for value in selected_values if str(value).strip()
        )
    )[:200]
    tenant_id = str(getattr(principal, "tenant_id", "") or "")
    provider = reference_option_provider(
        registry,
        CAPABILITY_ACCESS_REFERENCE_OPTIONS,
    )
    if provider is not None:
        if not tenant_id:
            return ReferenceSearchPage(
                options=tuple(
                    _unavailable_option(value, kind=clean_kind)
                    for value in selected
                )
            )
        page = provider.search_reference_options(
            session,
            principal,
            request=ReferenceSearchRequest(
                kind=clean_kind,
                tenant_id=tenant_id,
                query=str(query or "").strip().casefold(),
                selected_values=selected,
                limit=normalized_limit,
                cursor=str(cursor).strip() if cursor else None,
                context={"administrative": administrative},
            ),
        )
        return ReferenceSearchPage(
            options=_bounded_page_options(
                page.options,
                selected_values=selected,
                limit=normalized_limit,
                kind=clean_kind,
            ),
            next_cursor=page.next_cursor,
            has_more=page.has_more,
        )

    directory = _access_directory(registry)
    if directory is None:
        return ReferenceSearchPage(
            options=_fallback_scope_options(
                principal,
                scope_type=clean_type,
                reference_kind=clean_kind,
                query=query,
                selected_values=selected,
                limit=normalized_limit,
            )
        )

    if not tenant_id:
        return ReferenceSearchPage(
            options=tuple(
                _unavailable_option(value, kind=clean_kind) for value in selected
            )
        )
    if clean_type == "user":
        candidates = _user_options(
            directory.users_for_tenant(tenant_id),
            principal=principal,
            administrative=administrative,
            value_kind=clean_kind,
        )
    else:
        candidates = _group_options(
            directory.groups_for_tenant(tenant_id),
            principal=principal,
            administrative=administrative,
        )
    return ReferenceSearchPage(
        options=_filter_and_retain_options(
            candidates,
            query=query,
            selected_values=selected,
            limit=normalized_limit,
            kind=clean_kind,
        )
    )


def access_scope_reference_provider_available(registry: object | None) -> bool:
    return (
        reference_option_provider(
            registry,
            CAPABILITY_ACCESS_REFERENCE_OPTIONS,
        )
        is not None
        or _access_directory(registry) is not None
    )


def validate_access_scope_reference(
    registry: object | None,
    *,
    tenant_id: str | None,
    scope_type: str,
    scope_id: str | None,
    preserve_existing: str | None = None,
) -> str | None:
    """Validate new directory-backed scope references while preserving history."""

    clean_type = str(scope_type or "").strip().casefold()
    clean_id = str(scope_id or "").strip() or None
    if clean_type not in {"user", "group"}:
        return clean_id
    if not clean_id:
        raise ValueError(f"{clean_type.capitalize()} definitions require a scope ID.")
    if clean_id == preserve_existing:
        return clean_id

    directory = _access_directory(registry)
    if directory is None:
        return clean_id
    if not tenant_id:
        raise ValueError("Directory-backed scopes require an active tenant.")

    if clean_type == "group":
        group = directory.get_group(clean_id)
        if group is None or group.tenant_id != tenant_id:
            raise ValueError("The selected group is not available in the active tenant.")
        if group.status != "active":
            raise ValueError("Inactive groups cannot be selected for new definitions.")
        return group.id

    matching_user = next(
        (
            user
            for user in directory.users_for_tenant(tenant_id)
            if user.account_id == clean_id
        ),
        None,
    )
    if matching_user is None:
        raise ValueError("The selected user account is not available in the active tenant.")
    if matching_user.status != "active":
        raise ValueError("Inactive users cannot be selected for new definitions.")
    account = directory.get_account(matching_user.account_id)
    if account is not None and account.status != "active":
        raise ValueError("Inactive user accounts cannot be selected for new definitions.")
    return matching_user.account_id


def _access_directory(registry: object | None) -> AccessDirectory | None:
    if (
        registry is None
        or not hasattr(registry, "has_capability")
        or not hasattr(registry, "capability")
        or not registry.has_capability(CAPABILITY_ACCESS_DIRECTORY)
    ):
        return None
    provider = registry.capability(CAPABILITY_ACCESS_DIRECTORY)
    return provider if isinstance(provider, AccessDirectory) else None


def _user_options(
    users: Sequence[UserRef],
    *,
    principal: object,
    administrative: bool,
    value_kind: str = "user",
) -> tuple[ReferenceOption, ...]:
    principal_account_id = str(getattr(principal, "account_id", "") or "")
    options: dict[str, ReferenceOption] = {}
    for user in users:
        if not administrative and user.account_id != principal_account_id:
            continue
        inactive = user.status != "active"
        label = user.display_name or user.email or user.account_id
        detail_parts = [part for part in (user.email, "Inactive" if inactive else None) if part]
        value = user.id if value_kind == "membership" else user.account_id
        options[value] = ReferenceOption(
            value=value,
            label=label,
            description=" · ".join(detail_parts) or None,
            kind=value_kind,
            availability="inactive" if inactive else "available",
            disabled=inactive,
            source_module="access",
            provenance={
                "membership_id": user.id,
                "tenant_id": user.tenant_id,
                "account_id": user.account_id,
            },
        )
    return tuple(options.values())


def _group_options(
    groups: Sequence[GroupRef],
    *,
    principal: object,
    administrative: bool,
) -> tuple[ReferenceOption, ...]:
    permitted = {
        str(value)
        for value in getattr(principal, "group_ids", ())
        if str(value)
    }
    return tuple(
        ReferenceOption(
            value=group.id,
            label=group.name or group.id,
            description="Inactive" if group.status != "active" else None,
            kind="group",
            availability=(
                "inactive" if group.status != "active" else "available"
            ),
            disabled=group.status != "active",
            source_module="access",
            provenance={"tenant_id": group.tenant_id, "group_id": group.id},
        )
        for group in groups
        if administrative or group.id in permitted
    )


def _filter_and_retain_options(
    options: Sequence[ReferenceOption],
    *,
    query: str,
    selected_values: Sequence[str],
    limit: int,
    kind: str,
) -> tuple[ReferenceOption, ...]:
    by_value = {option.value: option for option in options}
    needle = str(query or "").strip().casefold()
    matches = [
        option
        for option in options
        if not needle
        or needle
        in " ".join(
            (
                option.value,
                option.label,
                option.description or "",
            )
        ).casefold()
    ][:limit]
    returned = {option.value for option in matches}
    for value in selected_values:
        if value in returned:
            continue
        matches.append(by_value.get(value) or _unavailable_option(value, kind=kind))
        returned.add(value)
    return tuple(matches)


def _bounded_page_options(
    options: Sequence[ReferenceOption],
    *,
    selected_values: Sequence[str],
    limit: int,
    kind: str,
) -> tuple[ReferenceOption, ...]:
    by_value = {option.value: option for option in options}
    selected = set(selected_values)
    bounded = [
        option for option in options if option.value not in selected
    ][:limit]
    returned = {option.value for option in bounded}
    for value in selected_values:
        if value in returned:
            continue
        bounded.append(by_value.get(value) or _unavailable_option(value, kind=kind))
        returned.add(value)
    return tuple(bounded)


def _fallback_scope_options(
    principal: object,
    *,
    scope_type: str,
    reference_kind: str | None = None,
    query: str,
    selected_values: Sequence[str],
    limit: int,
) -> tuple[ReferenceOption, ...]:
    candidates: list[ReferenceOption] = []
    if scope_type == "user":
        kind = str(reference_kind or "user")
        value = str(
            (
                getattr(principal, "membership_id", "")
                if kind == "membership"
                else getattr(principal, "account_id", "")
            )
            or ""
        )
        if value:
            candidates.append(
                ReferenceOption(
                    value=value,
                    label=(
                        str(getattr(principal, "display_name", "") or "")
                        or str(getattr(principal, "email", "") or "")
                        or value
                    ),
                    description="Current user · Access directory unavailable",
                    kind=kind,
                    source_module="core",
                    provenance={"fallback": True},
                )
            )
    else:
        candidates.extend(
            ReferenceOption(
                value=str(group_id),
                label=str(group_id),
                description="Permitted group · Access directory unavailable",
                kind="group",
                source_module="core",
                provenance={"fallback": True},
            )
            for group_id in getattr(principal, "group_ids", ())
            if str(group_id)
        )
    return _filter_and_retain_options(
        candidates,
        query=query,
        selected_values=selected_values,
        limit=limit,
        kind=scope_type,
    )


def _unavailable_option(value: str, *, kind: str) -> ReferenceOption:
    return ReferenceOption(
        value=value,
        label=f"Unavailable {kind}",
        description=value,
        kind=kind,
        availability="unavailable",
        disabled=True,
        source_module=None,
        provenance={"retained_reference": True},
    )


__all__ = [
    "CAPABILITY_ACCESS_REFERENCE_OPTIONS",
    "ReferenceAvailability",
    "ReferenceOption",
    "ReferenceOptionProvider",
    "ReferenceSearchPage",
    "ReferenceSearchRequest",
    "access_scope_reference_page",
    "access_scope_reference_options",
    "access_scope_reference_provider_available",
    "reference_option_provider",
    "validate_access_scope_reference",
]
