"""Policy service for voice policy operations.

This module provides the PolicyService class for listing, filtering, and
validating voice policies from the CSV cache.
"""

from __future__ import annotations

from datetime import timedelta
from typing import TYPE_CHECKING, Any

from teams_phone.constants import CSV_STALE_DAYS
from teams_phone.exceptions import NotFoundError, ValidationError
from teams_phone.models import EffectivePolicyAssignment, Policy, UserConfiguration

if TYPE_CHECKING:
    from teams_phone.infrastructure import CacheManager, GraphClient


def _strip_odata_metadata(data: dict[str, Any]) -> dict[str, Any]:
    """Strip OData metadata fields from a response dict.

    Graph API responses include @odata.context and other @ prefixed fields
    that are not part of the actual data contract. This helper removes them
    before Pydantic validation with extra="forbid".
    """
    return {k: v for k, v in data.items() if not k.startswith("@")}


# Supported policy types for reference (not enforced in list_policies)
SUPPORTED_POLICY_TYPES = [
    "TeamsCallingPolicy",
    "TeamsVoicemailPolicy",
    "CallingLineIdentity",
    "TenantDialPlan",
    "OnlineVoiceRoutingPolicy",
    "TeamsEmergencyCallingPolicy",
    "TeamsEmergencyCallRoutingPolicy",
]

# Policy types that support batch assignment via Graph API
# Note: TeamsVoicemailPolicy is NOT supported - use Grant-CsTeamsVoicemailPolicy PowerShell
ASSIGNABLE_POLICY_TYPES = [
    "TeamsCallingPolicy",
    "CallingLineIdentity",
    "TenantDialPlan",
    "OnlineVoiceRoutingPolicy",
    "TeamsEmergencyCallingPolicy",
    "TeamsEmergencyCallRoutingPolicy",
]


class PolicyService:
    """Service for voice policy operations.

    Provides methods for listing, filtering, and validating voice policies
    from the CSV cache.

    Attributes:
        graph_client: GraphClient instance for API access (used in future subtasks).
        cache_manager: CacheManager instance for CSV access.
    """

    def __init__(self, graph_client: GraphClient, cache_manager: CacheManager) -> None:
        """Initialize the PolicyService.

        Args:
            graph_client: GraphClient instance for Graph API operations.
            cache_manager: CacheManager instance for CSV reading.
        """
        self.graph_client = graph_client
        self.cache_manager = cache_manager

    def list_policies(self, policy_type: str | None = None) -> list[Policy]:
        """List policies from the cache, optionally filtered by type.

        Args:
            policy_type: Optional policy type to filter by (case-insensitive).
                If None, returns all policies.

        Returns:
            List of Policy objects. Empty list if CSV doesn't exist or is malformed.
        """
        raw_data = self.cache_manager.read_policies_csv()
        policies: list[Policy] = []

        for row in raw_data:
            # Create a new dict with boolean conversion for isGlobal field
            # CacheManager returns dict[str, str], but Pydantic needs bool for isGlobal
            validated_row: dict[str, Any] = dict(row)
            validated_row["isGlobal"] = row.get("isGlobal", "").lower() == "true"
            policies.append(Policy.model_validate(validated_row))

        # Filter by policy_type if provided (case-insensitive)
        if policy_type is not None:
            policy_type_lower = policy_type.lower()
            policies = [
                p for p in policies if p.policy_type.lower() == policy_type_lower
            ]

        return policies

    def validate_policy(self, policy_type: str, policy_name: str) -> Policy:
        """Validate and retrieve a policy by type and name.

        Finds a policy matching both the type and name (case-insensitive).

        Args:
            policy_type: The policy type (e.g., "TeamsCallingPolicy").
            policy_name: The policy name.

        Returns:
            Policy for the matched policy.

        Raises:
            NotFoundError: If no policy matches the type and name.
        """
        policies = self.list_policies()
        policy_type_lower = policy_type.lower()
        policy_name_lower = policy_name.lower()

        for policy in policies:
            if (
                policy.policy_type.lower() == policy_type_lower
                and policy.policy_name.lower() == policy_name_lower
            ):
                return policy

        raise NotFoundError(
            f"Policy '{policy_name}' of type '{policy_type}' not found",
            remediation=(
                "Verify the policy type and name are correct.\n"
                "Use 'teams-phone policies list' to see available policies."
            ),
        )

    async def get_user_policies(
        self, user_id: str, policy_type: str | None = None
    ) -> list[EffectivePolicyAssignment]:
        """Get a user's effective policy assignments.

        Retrieves the effective policy assignments for a user from their
        UserConfiguration via the Graph API.

        Args:
            user_id: The user's Entra object ID (GUID).
            policy_type: Optional policy type to filter by (case-insensitive).
                If None, returns all effective policy assignments.

        Returns:
            List of EffectivePolicyAssignment objects.

        Raises:
            NotFoundError: If the user is not found.
            ContractError: If API response doesn't match expected schema.
            AuthenticationError: If authentication fails.
        """
        response = await self.graph_client.get(
            f"/admin/teams/userConfigurations/{user_id}"
        )
        user_config = UserConfiguration.model_validate(_strip_odata_metadata(response))
        assignments = user_config.effective_policy_assignments

        if policy_type is not None:
            policy_type_lower = policy_type.lower()
            assignments = [
                a for a in assignments if a.policy_type.lower() == policy_type_lower
            ]

        return assignments

    def get_cache_age(self) -> timedelta | None:
        """Get the age of the policy cache.

        Returns:
            timedelta representing the cache age, or None if no cache exists.
        """
        age_days = self.cache_manager.get_cache_age_days()

        if age_days is None:
            return None

        return timedelta(days=age_days)

    def is_cache_stale(self) -> bool:
        """Check if the policy cache is stale.

        Returns:
            True if cache doesn't exist or is older than CSV_STALE_DAYS (30 days).
        """
        return self.cache_manager.is_cache_stale()

    def get_staleness_warning(self) -> str | None:
        """Get a warning message if the cache is stale.

        Returns:
            Warning message string if cache is stale, None otherwise.
        """
        if not self.is_cache_stale():
            return None

        age_days = self.cache_manager.get_cache_age_days()

        if age_days is None:
            return (
                "Policy cache not found. "
                "Run the PowerShell export script to create the cache."
            )

        return (
            f"Policy cache is {age_days} days old (threshold: {CSV_STALE_DAYS} days). "
            "Consider refreshing by running the PowerShell export script."
        )

    async def resolve_policy_id(self, policy_type: str, policy_name: str) -> str:
        """Resolve a policy's actual Graph API policyId from tenant user assignments.

        The CSV cache stores policy names, but the Graph API assignment endpoint
        requires the actual base64-encoded policyId. This method queries user
        configurations to find the real policyId for a given policy type and name.

        Args:
            policy_type: The policy type (e.g., "TeamsCallingPolicy").
            policy_name: The policy display name (e.g., "AllowCalling").

        Returns:
            The actual policyId (base64-encoded string) for use in Graph API calls.

        Raises:
            NotFoundError: If no user in the tenant has this policy assigned,
                meaning we cannot determine the policyId.
        """
        policy_type_lower = policy_type.lower()
        policy_name_lower = policy_name.lower()

        # Query user configurations to find policy assignments
        # We need to iterate through users until we find one with the target policy
        params: dict[str, Any] = {"$top": "100"}

        async for item in self.graph_client.get_paginated(
            "/admin/teams/userConfigurations",
            params=params,
            max_items=500,  # Check up to 500 users
        ):
            user = UserConfiguration.model_validate(item)
            for assignment in user.effective_policy_assignments:
                if (
                    assignment.policy_type.lower() == policy_type_lower
                    and assignment.policy_assignment.display_name
                    and assignment.policy_assignment.display_name.lower()
                    == policy_name_lower
                ):
                    return assignment.policy_assignment.policy_id

        raise NotFoundError(
            f"Could not resolve policyId for '{policy_name}' ({policy_type})",
            remediation=(
                "No user in the tenant currently has this policy assigned.\n"
                "The Graph API requires the actual policyId, which can only be\n"
                "determined from existing user assignments.\n\n"
                "Options:\n"
                "1. Assign this policy to a user via PowerShell first\n"
                "2. Use a different policy that is already assigned to someone"
            ),
        )

    async def assign_policy(
        self, user_id: str, policy_type: str, policy_name: str
    ) -> None:
        """Assign a policy to a user via Graph API.

        Validates the policy exists in the CSV cache, resolves the actual
        policyId from tenant user assignments, then POSTs to the Graph API
        to assign the policy to the specified user.

        Args:
            user_id: The user's Entra object ID (GUID).
            policy_type: The policy type (e.g., "TeamsCallingPolicy").
            policy_name: The policy name.

        Returns:
            None on success (204 No Content response).

        Raises:
            ValidationError: If policy_type is not supported for batch assignment
                (e.g., TeamsVoicemailPolicy).
            NotFoundError: If the policy doesn't exist in the cache or if we
                cannot resolve the policyId from existing user assignments.
            AuthenticationError: If authentication fails.
            AuthorizationError: If authorization fails.
        """
        # Validate policy type is supported for batch assignment
        if policy_type == "TeamsVoicemailPolicy":
            raise ValidationError(
                f"Policy type '{policy_type}' is not supported for batch assignment",
                remediation=(
                    "Use PowerShell cmdlet Grant-CsTeamsVoicemailPolicy instead:\n"
                    f"  Grant-CsTeamsVoicemailPolicy -Identity <user> "
                    f"-PolicyName '{policy_name}'"
                ),
            )

        # Validate the policy exists in cache (raises NotFoundError if not found)
        self.validate_policy(policy_type, policy_name)

        # Resolve the actual policyId from tenant user assignments
        # The CSV stores policy names, but Graph API needs the real base64 policyId
        policy_id = await self.resolve_policy_id(policy_type, policy_name)

        # Build request body per Graph API contract
        body: dict[str, Any] = {
            "value": [
                {
                    "@odata.type": (
                        "#microsoft.graph.teamsAdministration.teamsPolicyUserAssignment"
                    ),
                    "userId": user_id,
                    "policyType": policy_type,
                    "policyId": policy_id,
                }
            ]
        }

        # POST to the policy assignment endpoint
        # Response is 204 No Content, so use post_with_response to avoid JSON parse error
        await self.graph_client.post_with_response(
            "/admin/teams/policy/userAssignments/assign",
            body,
        )
