"""
Access rule evaluator with Cedar-style permit/forbid semantics.

Evaluates EntityAccessSpec from BackendSpec at runtime, supporting:
- Cedar three-rule evaluation: FORBID > PERMIT > default-deny
- Role checks: role(admin)
- Persona scoping: restrict rules to specific personas
- Relationship traversal: owner.team_id
- Logical operators: AND/OR
- Comparison operators: =, !=, >, <, >=, <=, in, not in
"""

from typing import Any

from dazzle.core.access import AccessDecision, AccessRuntimeContext
from dazzle_back.runtime._comparison import eval_comparison_op, normalize_for_comparison
from dazzle_back.specs import (
    AccessComparisonKind,
    AccessConditionSpec,
    AccessLogicalKind,
    AccessOperationKind,
    AccessPolicyEffect,
    EntityAccessSpec,
)
from dazzle_back.specs.auth import AccessAuthContext, PermissionRuleSpec

# =============================================================================
# Condition Evaluation
# =============================================================================


def _resolve_dotted_path(
    record: dict[str, Any],
    path: str,
    context: AccessRuntimeContext,
) -> Any:
    """
    Resolve a dotted path like 'owner.team_id' to a value.

    For 'owner.team_id':
    1. Get record['owner'] or record['owner_id'] (the foreign key)
    2. Use entity_resolver to fetch the Owner entity
    3. Return owner_record['team_id']
    """
    parts = path.split(".")
    current = record

    for part in parts:
        if current is None:
            return None

        if isinstance(current, dict):
            if part in current:
                current = current[part]
            elif f"{part}_id" in current:
                fk_value = current[f"{part}_id"]
                if context.entity_resolver and fk_value:
                    entity_name = part.title()
                    resolved = context.entity_resolver(entity_name, fk_value)
                    current = resolved
                else:
                    current = fk_value
            else:
                return None
        else:
            return None

    return current


def _evaluate_comparison_condition(
    condition: AccessConditionSpec,
    record: dict[str, Any],
    context: AccessRuntimeContext,
) -> bool:
    """
    Evaluate a comparison condition.

    Supports:
    - Simple field comparisons: status = "active"
    - Special values: owner_id = current_user
    - Dotted paths: owner.team_id = current_team
    """
    field = condition.field
    op = condition.comparison_op
    value = condition.value
    value_list = condition.value_list

    if not field or not op:
        return True

    if "." in field:
        record_value = _resolve_dotted_path(record, field, context)
    else:
        record_value = record.get(field)

    resolved_value = value
    if value == "current_user":
        resolved_value = context.user_id
    elif value == "current_team":
        resolved_value = None

    # For ordered comparisons, use float coercion with explicit None guard
    if op in (
        AccessComparisonKind.GREATER_THAN,
        AccessComparisonKind.LESS_THAN,
        AccessComparisonKind.GREATER_EQUAL,
        AccessComparisonKind.LESS_EQUAL,
    ):
        try:
            if record_value is None or resolved_value is None:
                return False
            lf = float(record_value)
            rf = float(resolved_value)
        except (TypeError, ValueError):
            return False
        if op == AccessComparisonKind.GREATER_THAN:
            return lf > rf
        if op == AccessComparisonKind.LESS_THAN:
            return lf < rf
        if op == AccessComparisonKind.GREATER_EQUAL:
            return lf >= rf
        if op == AccessComparisonKind.LESS_EQUAL:
            return lf <= rf

    # IS / IS_NOT are identity checks — not covered by eval_comparison_op
    if op == AccessComparisonKind.IS:
        if resolved_value is None:
            return record_value is None
        return bool(record_value == resolved_value)
    if op == AccessComparisonKind.IS_NOT:
        if resolved_value is None:
            return record_value is not None
        return bool(record_value != resolved_value)

    # Normalize both sides to strings for equality and membership tests
    record_val_str = normalize_for_comparison(record_value)
    resolved_val_str = normalize_for_comparison(resolved_value)

    if op == AccessComparisonKind.EQUALS:
        return eval_comparison_op("eq", record_val_str, resolved_val_str)
    if op == AccessComparisonKind.NOT_EQUALS:
        return eval_comparison_op("ne", record_val_str, resolved_val_str)
    if op == AccessComparisonKind.IN:
        normalized_list = [normalize_for_comparison(v) for v in value_list] if value_list else None
        return eval_comparison_op(
            "in", record_val_str, resolved_val_str, value_list=normalized_list
        )
    if op == AccessComparisonKind.NOT_IN:
        normalized_list = [normalize_for_comparison(v) for v in value_list] if value_list else None
        return eval_comparison_op(
            "not_in", record_val_str, resolved_val_str, value_list=normalized_list
        )

    return False


def evaluate_access_condition(
    condition: AccessConditionSpec,
    record: dict[str, Any],
    context: AccessRuntimeContext,
) -> bool:
    """
    Evaluate an AccessConditionSpec against a record.

    Handles:
    - comparison: field op value
    - role_check: role(name)
    - logical: left AND/OR right
    """
    if condition.kind == "comparison":
        return _evaluate_comparison_condition(condition, record, context)

    elif condition.kind == "role_check":
        role_name = condition.role_name
        if role_name:
            return context.has_role(role_name)
        return False

    elif condition.kind == "grant_check":
        # Grant checks require runtime grant resolution.
        # When active_grants are available on the context, evaluate them;
        # otherwise fall through to False (deny) — the grant isn't proven.
        active_grants: list[dict[str, Any]] = getattr(context, "active_grants", [])
        if not active_grants:
            return False
        scope_value = record.get(condition.grant_scope_field or "")
        if not scope_value:
            return False
        for grant in active_grants:
            if grant.get("relation") == condition.grant_relation and grant.get(
                "scope_value"
            ) == str(scope_value):
                return True
        return False

    elif condition.kind == "logical":
        left_result = True
        right_result = True

        if condition.logical_left:
            left_result = evaluate_access_condition(condition.logical_left, record, context)

        if condition.logical_right:
            right_result = evaluate_access_condition(condition.logical_right, record, context)

        if condition.logical_op == AccessLogicalKind.AND:
            return left_result and right_result
        elif condition.logical_op == AccessLogicalKind.OR:
            return left_result or right_result

    return True  # Unknown condition type = allow


# =============================================================================
# Rule Matching
# =============================================================================


def _rule_matches(
    rule: PermissionRuleSpec,
    operation: AccessOperationKind,
    record: dict[str, Any] | None,
    context: AccessRuntimeContext,
) -> bool:
    """
    Check if a permission rule matches the current request.

    A rule matches if:
    1. Its operation matches the requested operation
    2. Its persona scope matches (empty = any, otherwise user must have one)
    3. Its auth requirement is met
    4. Its condition evaluates to true against the record
    """
    if rule.operation != operation:
        return False

    # Check persona scope
    if rule.personas:
        if not any(context.has_role(p) for p in rule.personas):
            return False

    # Check auth requirement
    if rule.require_auth and not context.is_authenticated:
        return False

    # If no condition, the rule matches
    if rule.condition is None:
        return True

    # For create operations, evaluate the condition without a record.
    # Permit blocks only contain role/grant/logical conditions (no field
    # comparisons), so they are safe to evaluate with an empty record.
    if record is None and operation == AccessOperationKind.CREATE:
        return evaluate_access_condition(rule.condition, {}, context)

    return evaluate_access_condition(rule.condition, record or {}, context)


def _describe_rule(rule: PermissionRuleSpec) -> str:
    """Generate a human-readable description of a rule for audit logging."""
    parts = [f"{rule.effect.value} {rule.operation.value}"]
    if rule.personas:
        parts.append(f"for [{', '.join(rule.personas)}]")
    if rule.condition:
        parts.append(f"when {rule.condition.kind}")
    return " ".join(parts)


# =============================================================================
# Visibility Evaluation
# =============================================================================


def evaluate_visibility(
    access_spec: EntityAccessSpec,
    record: dict[str, Any],
    context: AccessRuntimeContext,
) -> bool:
    """
    Evaluate if a record is visible to the user.

    Checks visibility rules based on authentication context.
    """
    if context.is_superuser:
        return True

    auth_context = (
        AccessAuthContext.AUTHENTICATED if context.is_authenticated else AccessAuthContext.ANONYMOUS
    )

    for rule in access_spec.visibility:
        if rule.context == auth_context:
            return evaluate_access_condition(rule.condition, record, context)

    if access_spec.visibility:
        return False

    return True


# =============================================================================
# Cedar-Style Permission Evaluation
# =============================================================================


def _emit_audit(
    decision: AccessDecision,
    operation: AccessOperationKind,
    record: dict[str, Any] | None,
    context: AccessRuntimeContext,
    entity: str = "",
) -> None:
    """Emit an access decision to the audit sink."""
    import uuid as _uuid
    from datetime import UTC, datetime

    from dazzle.rbac.audit import AccessDecisionRecord, get_audit_sink

    audit_record = AccessDecisionRecord(
        timestamp=datetime.now(UTC).isoformat(),
        request_id=str(_uuid.uuid4()),
        user_id=context.user_id or "",
        roles=sorted(context.roles),
        entity=entity,
        operation=operation.value if hasattr(operation, "value") else str(operation),
        allowed=decision.allowed,
        effect=decision.effect,
        matched_rule=decision.matched_policy,
        record_id=record.get("id", "") if record else None,
        tier="gate" if record is None else "row_filter",
    )
    get_audit_sink().emit(audit_record)


def evaluate_permission(
    access_spec: EntityAccessSpec,
    operation: AccessOperationKind,
    record: dict[str, Any] | None,
    context: AccessRuntimeContext,
    *,
    entity_name: str = "",
) -> AccessDecision:
    """
    Cedar-style three-rule permission evaluation.

    1. If ANY matching FORBID rule fires -> DENY (return forbid rule description)
    2. If ANY matching PERMIT rule fires -> ALLOW (return permit rule description)
    3. No matching rules -> DENY (default-deny)

    Superusers bypass all checks.

    Args:
        access_spec: Entity access specification
        operation: The operation being attempted
        record: Entity record (None for create operations)
        context: Runtime context with user info

    Returns:
        AccessDecision with allowed flag and matched policy description
    """
    # Superusers bypass all checks
    if context.is_superuser:
        _result = AccessDecision(
            allowed=True,
            matched_policy="superuser_bypass",
            effect="permit",
        )
        _emit_audit(_result, operation, record, context, entity=entity_name)
        return _result

    # Collect matching rules
    matching_forbids: list[PermissionRuleSpec] = []
    matching_permits: list[PermissionRuleSpec] = []

    for rule in access_spec.permissions:
        if _rule_matches(rule, operation, record, context):
            if rule.effect == AccessPolicyEffect.FORBID:
                matching_forbids.append(rule)
            else:
                matching_permits.append(rule)

    # Rule 1: Any FORBID match -> DENY
    if matching_forbids:
        rule = matching_forbids[0]
        _result = AccessDecision(
            allowed=False,
            matched_policy=_describe_rule(rule),
            effect="forbid",
        )
        _emit_audit(_result, operation, record, context, entity=entity_name)
        return _result

    # Rule 2: Any PERMIT match -> ALLOW
    if matching_permits:
        rule = matching_permits[0]
        _result = AccessDecision(
            allowed=True,
            matched_policy=_describe_rule(rule),
            effect="permit",
        )
        _emit_audit(_result, operation, record, context, entity=entity_name)
        return _result

    # Rule 3: No matching rules -> default behavior
    # If there are any rules defined for this entity, default-deny
    if access_spec.permissions:
        _result = AccessDecision(
            allowed=False,
            matched_policy="default_deny",
            effect="default",
        )
        _emit_audit(_result, operation, record, context, entity=entity_name)
        return _result

    # No permission rules at all = allow if authenticated (backward compat)
    if operation in (
        AccessOperationKind.CREATE,
        AccessOperationKind.UPDATE,
        AccessOperationKind.DELETE,
    ):
        if context.is_authenticated:
            _result = AccessDecision(
                allowed=True,
                matched_policy="no_rules_authenticated",
                effect="permit",
            )
            _emit_audit(_result, operation, record, context, entity=entity_name)
            return _result
        _result = AccessDecision(
            allowed=False,
            matched_policy="no_rules_unauthenticated",
            effect="default",
        )
        _emit_audit(_result, operation, record, context, entity=entity_name)
        return _result

    _result = AccessDecision(
        allowed=True,
        matched_policy="no_rules_default_allow",
        effect="permit",
    )
    _emit_audit(_result, operation, record, context, entity=entity_name)
    return _result


def evaluate_permission_bool(
    access_spec: EntityAccessSpec,
    operation: AccessOperationKind,
    record: dict[str, Any] | None,
    context: AccessRuntimeContext,
) -> bool:
    """
    Backward-compatible boolean wrapper around evaluate_permission.

    Returns True if operation is permitted, False otherwise.
    """
    return evaluate_permission(access_spec, operation, record, context).allowed


# =============================================================================
# Convenience Functions
# =============================================================================


def can_read(
    access_spec: EntityAccessSpec,
    record: dict[str, Any],
    context: AccessRuntimeContext,
) -> bool:
    """Check if user can read a record (uses visibility rules + READ permissions)."""
    # First check visibility rules (backward compat)
    if not evaluate_visibility(access_spec, record, context):
        return False
    # Also check READ permission rules if any exist
    has_read_rules = any(r.operation == AccessOperationKind.READ for r in access_spec.permissions)
    if has_read_rules:
        return evaluate_permission(access_spec, AccessOperationKind.READ, record, context).allowed
    return True


def can_create(
    access_spec: EntityAccessSpec,
    context: AccessRuntimeContext,
) -> bool:
    """Check if user can create a new record."""
    return evaluate_permission(
        access_spec,
        AccessOperationKind.CREATE,
        None,
        context,
    ).allowed


def can_update(
    access_spec: EntityAccessSpec,
    record: dict[str, Any],
    context: AccessRuntimeContext,
) -> bool:
    """Check if user can update a record."""
    return evaluate_permission(
        access_spec,
        AccessOperationKind.UPDATE,
        record,
        context,
    ).allowed


def can_delete(
    access_spec: EntityAccessSpec,
    record: dict[str, Any],
    context: AccessRuntimeContext,
) -> bool:
    """Check if user can delete a record."""
    return evaluate_permission(
        access_spec,
        AccessOperationKind.DELETE,
        record,
        context,
    ).allowed


def filter_visible_records(
    access_spec: EntityAccessSpec,
    records: list[dict[str, Any]],
    context: AccessRuntimeContext,
) -> list[dict[str, Any]]:
    """Filter a list of records to only those visible to user."""
    return [r for r in records if can_read(access_spec, r, context)]
