from __future__ import annotations

import asyncio
from typing import Any
from urllib.parse import quote

from fastmcp.exceptions import ToolError
from mcp.types import ToolAnnotations

from mcp_eregistrations_keycloak.audit.logger import KcAuditLogger
from mcp_eregistrations_keycloak.audit.models import KcObjectType
from mcp_eregistrations_keycloak.keycloak_client import (
    KeycloakAdminClient,
    KeycloakClientError,
    translate_error,
)
from mcp_eregistrations_keycloak.server import mcp
from mcp_eregistrations_keycloak.tools.validation import (
    _ROLE_UPDATABLE_FIELDS,
    _extract_items,
    _filter_update_fields,
    _resolve_realm,
    _validate_role_name,
    _validate_user_id,
)

# --- Role Definition Tools ---


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def kc_list_realm_roles(
    realm: str | None = None,
    first: int = 0,
    max: int = 100,
    search: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """List realm-level roles.

    Args:
        realm: Realm name, or None for instance default.
        first: Pagination offset (default: 0).
        max: Page size (default: 100).
        search: Filter by role name substring.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with roles, count.
    """
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            params: dict[str, Any] = {"first": first, "max": max}
            if search:
                params["search"] = search
            roles = await client.get(f"/realms/{r}/roles", **params)
            items = _extract_items(roles)
            return {
                "roles": [
                    {
                        "id": role["id"],
                        "name": role["name"],
                        "description": role.get("description"),
                        "composite": role.get("composite", False),
                    }
                    for role in items
                ],
                "count": len(items),
            }
    except KeycloakClientError as e:
        raise translate_error(e, resource_type="realm role") from e


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def kc_get_realm_role(
    role_name: str,
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Get a realm role by name.

    Args:
        role_name: Role name.
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with role details.
    """
    _validate_role_name(role_name)
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            path = f"/realms/{r}/roles/{quote(role_name, safe='')}"
            result: dict[str, Any] = await client.get(path)
            return result
    except KeycloakClientError as e:
        raise translate_error(
            e, resource_type="realm role", resource_id=role_name
        ) from e


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def kc_list_role_users(
    role_name: str,
    realm: str | None = None,
    first: int = 0,
    max: int = 20,
    instance: str | None = None,
) -> dict[str, Any]:
    """List users assigned to a realm role.

    Args:
        role_name: Role name.
        realm: Realm name, or None for instance default.
        first: Pagination offset (default: 0).
        max: Page size (default: 20).
        instance: Instance name, or None for auto-select.

    Returns:
        dict with users, role_name.
    """
    _validate_role_name(role_name)
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            users = await client.get(
                f"/realms/{r}/roles/{quote(role_name, safe='')}/users",
                first=first,
                max=max,
            )
            items = _extract_items(users)
            return {
                "role_name": role_name,
                "users": [
                    {
                        "id": u["id"],
                        "username": u["username"],
                        "email": u.get("email"),
                    }
                    for u in items
                ],
            }
    except KeycloakClientError as e:
        raise translate_error(
            e, resource_type="role users", resource_id=role_name
        ) from e


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def kc_list_client_roles(
    client_id: str,
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """List roles for a specific client.

    Args:
        client_id: Keycloak client UUID (not clientId string).
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with roles, client_id.
    """
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            roles = await client.get(f"/realms/{r}/clients/{client_id}/roles")
            items = _extract_items(roles)
            return {
                "client_id": client_id,
                "roles": [
                    {
                        "id": role["id"],
                        "name": role["name"],
                        "description": role.get("description"),
                        "composite": role.get("composite", False),
                    }
                    for role in items
                ],
            }
    except KeycloakClientError as e:
        raise translate_error(
            e, resource_type="client role", resource_id=client_id
        ) from e


@mcp.tool()
async def kc_create_realm_role(
    role_name: str,
    description: str | None = None,
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Create a realm role. Audited write operation.

    Args:
        role_name: Unique role name.
        description: Role description.
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with role_name, status.
    """
    _validate_role_name(role_name)
    logger = KcAuditLogger.for_instance(instance)
    audit_id: str | None = None
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            audit_id = await logger.record_pending(
                None, "create", KcObjectType.ROLE, {"realm": r, "role_name": role_name}
            )
            role_data: dict[str, Any] = {
                "name": role_name,
                "attributes": {"managed-by": ["keycloak-mcp"]},
            }
            if description:
                role_data["description"] = description
            result = await client.post(f"/realms/{r}/roles", data=role_data)
            await logger.save_rollback_state(
                audit_id, KcObjectType.ROLE, role_name, role_data
            )
            outcome = {
                "role_name": role_name,
                "realm": r,
                "status": "created",
                **result,
            }
            await logger.mark_success(audit_id, outcome)
            return outcome
    except KeycloakClientError as e:
        if audit_id is not None:
            await logger.mark_failed(audit_id, str(e))
        raise translate_error(
            e, resource_type="realm role", resource_id=role_name
        ) from e


@mcp.tool()
async def kc_update_realm_role(
    role_name: str,
    updates: dict[str, Any],
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Update a realm role. Audited write operation.

    Args:
        role_name: Role name to update.
        updates: Fields to update (name, description, attributes).
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with role_name, updated_fields, status.
    """
    _validate_role_name(role_name)
    filtered = _filter_update_fields(updates, _ROLE_UPDATABLE_FIELDS, "role")
    logger = KcAuditLogger.for_instance(instance)
    audit_id: str | None = None
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            audit_id = await logger.record_pending(
                None,
                "update",
                KcObjectType.ROLE,
                {"role_name": role_name, "fields": list(filtered.keys())},
            )
            current = await client.get(f"/realms/{r}/roles/{quote(role_name, safe='')}")
            await logger.save_rollback_state(
                audit_id, KcObjectType.ROLE, role_name, current
            )
            merged = {**current, **filtered}
            await client.put(
                f"/realms/{r}/roles/{quote(role_name, safe='')}",
                data=merged,
            )
            outcome = {
                "role_name": role_name,
                "realm": r,
                "updated_fields": list(filtered.keys()),
                "status": "updated",
            }
            await logger.mark_success(audit_id, outcome)
            return outcome
    except KeycloakClientError as e:
        if audit_id is not None:
            await logger.mark_failed(audit_id, str(e))
        raise translate_error(
            e, resource_type="realm role", resource_id=role_name
        ) from e


@mcp.tool()
async def kc_delete_realm_role(
    role_name: str,
    force: bool = False,
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Delete a realm role. Audited write operation.

    Only allows deletion of MCP-created roles unless force=True.

    Args:
        role_name: Role name to delete.
        force: If True, delete even non-MCP-managed roles (default: False).
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with role_name, status.
    """
    _validate_role_name(role_name)
    logger = KcAuditLogger.for_instance(instance)
    audit_id = await logger.record_pending(
        None, "delete", KcObjectType.ROLE, {"role_name": role_name}
    )
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            path = f"/realms/{r}/roles/{quote(role_name, safe='')}"
            if not force:
                role = await client.get(path)
                managed_by = role.get("attributes", {}).get("managed-by", [])
                if "keycloak-mcp" not in managed_by:
                    raise ToolError(
                        f"Role '{role_name}' was not created by MCP. "
                        f"Set force=True to delete non-MCP-managed roles."
                    )
                await logger.save_rollback_state(
                    audit_id, KcObjectType.ROLE, role_name, role
                )
            else:
                role = await client.get(path)
                await logger.save_rollback_state(
                    audit_id, KcObjectType.ROLE, role_name, role
                )
            await client.delete(path)
            outcome = {"role_name": role_name, "realm": r, "status": "deleted"}
            await logger.mark_success(audit_id, outcome)
            return outcome
    except KeycloakClientError as e:
        await logger.mark_failed(audit_id, str(e))
        raise translate_error(
            e, resource_type="realm role", resource_id=role_name
        ) from e


# --- Composite Role Tools ---


async def _resolve_roles(
    client: KeycloakAdminClient,
    realm: str,
    roles: list[dict[str, Any]],
) -> list[dict[str, Any]]:
    """Resolve role names to full representations (with id) in parallel."""

    async def _resolve_one(role: dict[str, Any]) -> dict[str, Any]:
        if "id" in role:
            return role
        name = role.get("name")
        if not name:
            raise ToolError("Each role must have 'name' or 'id'.")
        resolved: dict[str, Any] = await client.get(
            f"/realms/{realm}/roles/{quote(name, safe='')}"
        )
        return resolved

    return list(await asyncio.gather(*[_resolve_one(r) for r in roles]))


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def kc_get_composite_roles(
    role_name: str,
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Get child roles of a composite role.

    Args:
        role_name: Composite role name.
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with role_name, composites.
    """
    _validate_role_name(role_name)
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            composites = await client.get(
                f"/realms/{r}/roles/{quote(role_name, safe='')}/composites"
            )
            return {
                "role_name": role_name,
                "composites": composites or [],
            }
    except KeycloakClientError as e:
        raise translate_error(
            e,
            resource_type="composite role",
            resource_id=role_name,
        ) from e


@mcp.tool()
async def kc_add_composite_roles(
    role_name: str,
    roles: list[dict[str, Any]],
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Add child roles to a composite role. Audited write operation.

    Args:
        role_name: Parent composite role name.
        roles: List of roles to add (each with 'name' or 'id').
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with role_name, added_count, status.
    """
    _validate_role_name(role_name)
    logger = KcAuditLogger.for_instance(instance)
    audit_id: str | None = None
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            audit_id = await logger.record_pending(
                None,
                "create",
                KcObjectType.COMPOSITE_ROLE,
                {"realm": r, "role_name": role_name},
            )
            resolved = await _resolve_roles(client, r, roles)
            await logger.save_rollback_state(
                audit_id, KcObjectType.COMPOSITE_ROLE, role_name, {"roles": resolved}
            )
            await client.post(
                f"/realms/{r}/roles/{quote(role_name, safe='')}/composites",
                data=resolved,
            )
            outcome = {
                "role_name": role_name,
                "realm": r,
                "added_count": len(resolved),
                "status": "composites_added",
            }
            await logger.mark_success(audit_id, outcome)
            return outcome
    except KeycloakClientError as e:
        if audit_id is not None:
            await logger.mark_failed(audit_id, str(e))
        raise translate_error(
            e,
            resource_type="composite role",
            resource_id=role_name,
        ) from e


@mcp.tool()
async def kc_remove_composite_roles(
    role_name: str,
    roles: list[dict[str, Any]],
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Remove child roles from a composite role. Audited write operation.

    Args:
        role_name: Parent composite role name.
        roles: List of roles to remove (each with 'name' or 'id').
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with role_name, removed_count, status.
    """
    _validate_role_name(role_name)
    logger = KcAuditLogger.for_instance(instance)
    audit_id: str | None = None
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            audit_id = await logger.record_pending(
                None,
                "delete",
                KcObjectType.COMPOSITE_ROLE,
                {"realm": r, "role_name": role_name},
            )
            resolved = await _resolve_roles(client, r, roles)
            await logger.save_rollback_state(
                audit_id, KcObjectType.COMPOSITE_ROLE, role_name, {"roles": resolved}
            )
            await client.delete(
                f"/realms/{r}/roles/{quote(role_name, safe='')}/composites",
                data=resolved,
            )
            outcome = {
                "role_name": role_name,
                "realm": r,
                "removed_count": len(resolved),
                "status": "composites_removed",
            }
            await logger.mark_success(audit_id, outcome)
            return outcome
    except KeycloakClientError as e:
        if audit_id is not None:
            await logger.mark_failed(audit_id, str(e))
        raise translate_error(
            e,
            resource_type="composite role",
            resource_id=role_name,
        ) from e


# --- User Role Mapping Tools ---


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def kc_get_user_role_mappings(
    user_id: str,
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Get all role mappings for a user.

    Args:
        user_id: User UUID.
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with user_id, realm_mappings, client_mappings.
    """
    _validate_user_id(user_id)
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            mappings = await client.get(f"/realms/{r}/users/{user_id}/role-mappings")
            return {
                "user_id": user_id,
                "realm_mappings": mappings.get("realmMappings", []),
                "client_mappings": mappings.get("clientMappings", {}),
            }
    except KeycloakClientError as e:
        raise translate_error(
            e,
            resource_type="user role mapping",
            resource_id=user_id,
        ) from e


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def kc_get_effective_realm_roles(
    user_id: str,
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Get effective (direct + inherited) realm roles for a user.

    Args:
        user_id: User UUID.
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with user_id, roles.
    """
    _validate_user_id(user_id)
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            roles = await client.get(
                f"/realms/{r}/users/{user_id}/role-mappings/realm/composite"
            )
            return {"user_id": user_id, "roles": roles or []}
    except KeycloakClientError as e:
        raise translate_error(
            e,
            resource_type="effective roles",
            resource_id=user_id,
        ) from e


@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def kc_get_available_realm_roles(
    user_id: str,
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Get realm roles available to assign to a user.

    Args:
        user_id: User UUID.
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with user_id, roles.
    """
    _validate_user_id(user_id)
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            roles = await client.get(
                f"/realms/{r}/users/{user_id}/role-mappings/realm/available"
            )
            return {"user_id": user_id, "roles": roles or []}
    except KeycloakClientError as e:
        raise translate_error(
            e,
            resource_type="available roles",
            resource_id=user_id,
        ) from e


@mcp.tool()
async def kc_assign_realm_roles(
    user_id: str,
    roles: list[dict[str, Any]],
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Assign realm roles to a user. Audited write operation.

    Args:
        user_id: User UUID.
        roles: List of roles to assign (each with 'name' or 'id').
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with user_id, assigned_count, status.
    """
    _validate_user_id(user_id)
    logger = KcAuditLogger.for_instance(instance)
    audit_id: str | None = None
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            audit_id = await logger.record_pending(
                None,
                "create",
                KcObjectType.USER_ROLE_MAPPING,
                {"realm": r, "user_id": user_id},
            )
            resolved = await _resolve_roles(client, r, roles)
            await logger.save_rollback_state(
                audit_id, KcObjectType.USER_ROLE_MAPPING, user_id, {"roles": resolved}
            )
            await client.post(
                f"/realms/{r}/users/{user_id}/role-mappings/realm",
                data=resolved,
            )
            outcome = {
                "user_id": user_id,
                "realm": r,
                "assigned_count": len(resolved),
                "status": "roles_assigned",
            }
            await logger.mark_success(audit_id, outcome)
            return outcome
    except KeycloakClientError as e:
        if audit_id is not None:
            await logger.mark_failed(audit_id, str(e))
        raise translate_error(
            e,
            resource_type="user role assignment",
            resource_id=user_id,
        ) from e


@mcp.tool()
async def kc_remove_user_realm_roles(
    user_id: str,
    roles: list[dict[str, Any]],
    realm: str | None = None,
    instance: str | None = None,
) -> dict[str, Any]:
    """Remove realm roles from a user. Audited write operation.

    Args:
        user_id: User UUID.
        roles: List of roles to remove (each with 'name' or 'id').
        realm: Realm name, or None for instance default.
        instance: Instance name, or None for auto-select.

    Returns:
        dict with user_id, removed_count, status.
    """
    _validate_user_id(user_id)
    logger = KcAuditLogger.for_instance(instance)
    audit_id: str | None = None
    try:
        async with KeycloakAdminClient.for_instance(instance) as client:
            r = _resolve_realm(realm, instance, client)
            audit_id = await logger.record_pending(
                None,
                "delete",
                KcObjectType.USER_ROLE_MAPPING,
                {"realm": r, "user_id": user_id},
            )
            resolved = await _resolve_roles(client, r, roles)
            await logger.save_rollback_state(
                audit_id, KcObjectType.USER_ROLE_MAPPING, user_id, {"roles": resolved}
            )
            await client.delete(
                f"/realms/{r}/users/{user_id}/role-mappings/realm",
                data=resolved,
            )
            outcome = {
                "user_id": user_id,
                "realm": r,
                "removed_count": len(resolved),
                "status": "roles_removed",
            }
            await logger.mark_success(audit_id, outcome)
            return outcome
    except KeycloakClientError as e:
        if audit_id is not None:
            await logger.mark_failed(audit_id, str(e))
        raise translate_error(
            e,
            resource_type="user role mapping",
            resource_id=user_id,
        ) from e
