"""Voice policy management commands for Teams Phone CLI.

This module provides CLI commands for listing, showing, and assigning
voice policies from the CSV cache and via Graph API.
"""

from __future__ import annotations

import asyncio
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional

import typer

from teams_phone.cli.context import get_context
from teams_phone.exceptions import ConfigurationError, TeamsPhoneError, ValidationError
from teams_phone.infrastructure import CacheManager, GraphClient
from teams_phone.models import EffectivePolicyAssignment, Policy
from teams_phone.services import AuthService
from teams_phone.services.bulk_operations import (
    BulkPolicyResult,
    BulkPolicyRow,
    BulkPolicyValidationResult,
    parse_policy_csv,
    write_policy_results_csv,
)
from teams_phone.services.policy_service import PolicyService
from teams_phone.services.user_service import UserService

if TYPE_CHECKING:
    from teams_phone.infrastructure import OutputFormatter

policies_app = typer.Typer(
    name="policies",
    help="Manage voice policies.",
    no_args_is_help=True,
)


# Mapping from CLI policy type names to Graph API policy type names
POLICY_TYPE_MAP = {
    "calling": "TeamsCallingPolicy",
    "voicemail": "TeamsVoicemailPolicy",
    "caller-id": "CallingLineIdentity",
    "dial-plan": "TenantDialPlan",
    "voice-routing": "OnlineVoiceRoutingPolicy",
    "emergency": "TeamsEmergencyCallingPolicy",
    "emergency-routing": "TeamsEmergencyCallRoutingPolicy",
}

# Reverse mapping for display
POLICY_TYPE_DISPLAY = {v: k for k, v in POLICY_TYPE_MAP.items()}


def _resolve_policy_type(policy_type: str | None) -> str | None:
    """Resolve a CLI policy type name to the Graph API policy type name.

    If policy_type is already a valid API name (e.g., TeamsCallingPolicy),
    returns it as-is. If it's a CLI alias (e.g., calling), maps it to the API name.
    """
    if policy_type is None:
        return None

    # Check if it's a CLI alias
    policy_type_lower = policy_type.lower()
    if policy_type_lower in POLICY_TYPE_MAP:
        return POLICY_TYPE_MAP[policy_type_lower]

    # Check if it's already a valid API name (case-insensitive)
    for api_name in POLICY_TYPE_MAP.values():
        if policy_type_lower == api_name.lower():
            return api_name

    # Return as-is for unknown types (service will handle validation)
    return policy_type


def _format_is_global(is_global: bool) -> str:
    """Format is_global status for display."""
    return "Yes" if is_global else "No"


def _format_assignment_type(assignment_type: str) -> str:
    """Format assignment type for display."""
    display_names = {
        "direct": "Direct",
        "group": "Group",
        "global": "Global (Default)",
    }
    return display_names.get(assignment_type.lower(), assignment_type)


def _format_policy_type_display(policy_type: str) -> str:
    """Format policy type for CLI display, using short names when available."""
    return POLICY_TYPE_DISPLAY.get(policy_type, policy_type)


def _policy_to_list_dict(policy: Policy) -> dict[str, Any]:
    """Convert Policy to dict for list table display."""
    return {
        "policy_type": _format_policy_type_display(policy.policy_type),
        "policy_name": policy.policy_name,
        "description": policy.description or "—",
        "is_global": _format_is_global(policy.is_global),
    }


def _policy_to_json_dict(policy: Policy) -> dict[str, Any]:
    """Convert Policy to dict for JSON output."""
    return {
        "policy_type": policy.policy_type,
        "policy_name": policy.policy_name,
        "policy_id": policy.policy_id,
        "description": policy.description,
        "is_global": policy.is_global,
    }


def _assignment_to_list_dict(assignment: EffectivePolicyAssignment) -> dict[str, Any]:
    """Convert EffectivePolicyAssignment to dict for show table display."""
    return {
        "policy_type": _format_policy_type_display(assignment.policy_type),
        "policy_name": assignment.policy_assignment.display_name or "Global (Default)",
        "assignment_type": _format_assignment_type(
            assignment.policy_assignment.assignment_type
        ),
    }


def _assignment_to_json_dict(assignment: EffectivePolicyAssignment) -> dict[str, Any]:
    """Convert EffectivePolicyAssignment to dict for JSON output."""
    return {
        "policy_type": assignment.policy_type,
        "policy_name": assignment.policy_assignment.display_name,
        "assignment_type": assignment.policy_assignment.assignment_type,
        "policy_id": assignment.policy_assignment.policy_id,
        "group_id": assignment.policy_assignment.group_id,
    }


def _get_sync_service() -> PolicyService:
    """Create a PolicyService instance for sync-only operations (list).

    Note: This creates a service with a MagicMock graph_client since list_policies
    only uses the cache_manager. For async operations, use _run_async instead.
    """
    from unittest.mock import MagicMock

    cache_manager = CacheManager()
    mock_graph = MagicMock()
    return PolicyService(mock_graph, cache_manager)


def _check_staleness(service: PolicyService, formatter: OutputFormatter) -> None:
    """Check for staleness warning and display if needed."""
    staleness_warning = service.get_staleness_warning()
    if staleness_warning:
        formatter.warning(staleness_warning)


def _run_async(
    ctx: typer.Context,
    coro_factory: Any,
) -> tuple[Any, "OutputFormatter"]:
    """Run an async operation with PolicyService and UserService.

    Creates the required infrastructure (GraphClient, services) and runs
    the async operation using asyncio.run().

    Args:
        ctx: Typer context with CLIContext.
        coro_factory: A callable that takes (PolicyService, UserService)
            and returns a coroutine.

    Returns:
        Tuple of (result, OutputFormatter) where result is the coroutine's return value.
    """
    cli_ctx = get_context(ctx)
    formatter = cli_ctx.get_output_formatter()
    config_manager = cli_ctx.get_config_manager()
    cache_manager = CacheManager()
    auth_service = AuthService(config_manager, cache_manager)

    async def _execute() -> Any:
        async with GraphClient(auth_service) as graph_client:
            policy_service = PolicyService(graph_client, cache_manager)
            user_service = UserService(graph_client)
            return await coro_factory(policy_service, user_service)

    result = asyncio.run(_execute())
    return result, formatter


@policies_app.command("list")
def list_policies(
    ctx: typer.Context,
    policy_type: Optional[str] = typer.Argument(
        None,
        help="Filter by policy type: calling, voicemail, emergency, emergency-routing, caller-id, dial-plan, voice-routing.",
    ),
) -> None:
    """List available voice policies from CSV cache.

    Shows all voice policies exported via PowerShell, optionally filtered by type.
    Use the policy type argument to filter by a specific policy type.

    Examples:

        teams-phone policies list

        teams-phone policies list calling

        teams-phone policies list emergency --json
    """
    cli_ctx = get_context(ctx)
    formatter = cli_ctx.get_output_formatter()

    try:
        service = _get_sync_service()

        # Check for staleness before output
        _check_staleness(service, formatter)

        # Resolve policy type (CLI alias to API name)
        resolved_type = _resolve_policy_type(policy_type)
        policies = service.list_policies(policy_type=resolved_type)

        if cli_ctx.json_output:
            formatter.json([_policy_to_json_dict(p) for p in policies])
        else:
            if not policies:
                if policy_type:
                    formatter.info(f"No policies found for type '{policy_type}'")
                else:
                    formatter.info("No policies found")
            else:
                title = "Voice Policies"
                if policy_type:
                    title = f"Voice Policies ({policy_type})"
                formatter.table(
                    data=[_policy_to_list_dict(p) for p in policies],
                    columns=[
                        "policy_type",
                        "policy_name",
                        "description",
                        "is_global",
                    ],
                    title=title,
                )
    except TeamsPhoneError as e:
        formatter.error(e.message, remediation=e.remediation)
        raise typer.Exit(e.exit_code)


@policies_app.command("show")
def show_user_policies(
    ctx: typer.Context,
    user: str = typer.Argument(
        ...,
        help="User identifier: UPN (email), display name, or object ID.",
    ),
) -> None:
    """Show all policy assignments for a user.

    Displays the effective policies assigned to a user, including
    how each policy was assigned (Direct, Group, or Global).

    Examples:

        teams-phone policies show john.doe@contoso.com

        teams-phone policies show "John Doe"

        teams-phone policies show 12345678-1234-1234-1234-123456789012
    """
    cli_ctx = get_context(ctx)
    formatter = cli_ctx.get_output_formatter()

    try:

        async def _get_policies(
            policy_service: PolicyService, user_service: UserService
        ) -> tuple[list[EffectivePolicyAssignment], str, str]:
            # Resolve user to get user_id
            user_config = await user_service.resolve_user(user)
            # Get user's effective policy assignments
            assignments = await policy_service.get_user_policies(user_config.id)
            return assignments, user_config.display_name or user, user_config.id

        (assignments, display_name, user_id), _ = _run_async(ctx, _get_policies)

        if cli_ctx.json_output:
            formatter.json(
                {
                    "user_id": user_id,
                    "display_name": display_name,
                    "policies": [_assignment_to_json_dict(a) for a in assignments],
                }
            )
        else:
            formatter.info(f"Policy Assignments for {display_name}")
            formatter.info("")

            if not assignments:
                formatter.info("No policy assignments found")
            else:
                formatter.table(
                    data=[_assignment_to_list_dict(a) for a in assignments],
                    columns=[
                        "policy_type",
                        "policy_name",
                        "assignment_type",
                    ],
                    title="Effective Policies",
                )
    except TeamsPhoneError as e:
        formatter.error(e.message, remediation=e.remediation)
        raise typer.Exit(e.exit_code)


@policies_app.command("assign")
def assign_policy(
    ctx: typer.Context,
    user: str = typer.Argument(
        ...,
        help="User identifier: UPN (email), display name, or object ID.",
    ),
    policy_type: str = typer.Argument(
        ...,
        help="Policy type: calling, voicemail, emergency, emergency-routing, caller-id, dial-plan, voice-routing.",
    ),
    policy_name: str = typer.Argument(
        ...,
        help="Name of the policy to assign (use 'Global' for default).",
    ),
    dry_run: bool = typer.Option(
        False,
        "--dry-run",
        help="Preview assignment without making changes.",
    ),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Skip confirmation prompt.",
    ),
) -> None:
    """Assign a policy to a user.

    Validates the policy exists and assigns it to the specified user.
    Use --dry-run to preview the assignment without making changes.

    Examples:

        teams-phone policies assign john@contoso.com calling AllowCalling

        teams-phone policies assign "John Doe" emergency "High Priority" --dry-run

        teams-phone policies assign john@contoso.com dial-plan Global --force
    """
    cli_ctx = get_context(ctx)
    formatter = cli_ctx.get_output_formatter()

    # Resolve policy type from CLI alias to API name
    resolved_type = _resolve_policy_type(policy_type)
    if resolved_type is None:
        formatter.error(
            f"Invalid policy type: {policy_type}",
            remediation=(
                "Valid policy types are: calling, voicemail, emergency, "
                "emergency-routing, caller-id, dial-plan, voice-routing"
            ),
        )
        raise typer.Exit(5)  # ValidationError exit code

    try:
        # First resolve the user
        async def _resolve_user(
            policy_service: PolicyService, user_service: UserService
        ) -> tuple[str, str, str]:
            user_config = await user_service.resolve_user(user)
            return (
                user_config.id,
                user_config.display_name or user,
                user_config.user_principal_name,
            )

        (user_id, display_name, upn), _ = _run_async(ctx, _resolve_user)

        # Validate policy exists in cache (for dry-run and confirmation)
        sync_service = _get_sync_service()
        policy = sync_service.validate_policy(resolved_type, policy_name)

        # Handle dry-run output
        if dry_run:
            if cli_ctx.json_output:
                formatter.json(
                    {
                        "dry_run": True,
                        "user_id": user_id,
                        "user_principal_name": upn,
                        "display_name": display_name,
                        "policy_type": resolved_type,
                        "policy_name": policy_name,
                        "policy_id": policy.policy_id,
                    }
                )
            else:
                formatter.info("Dry Run Preview:")
                formatter.info(f"  User: {display_name}")
                formatter.info(f"  UPN: {upn}")
                formatter.info(
                    f"  Policy Type: {_format_policy_type_display(resolved_type)}"
                )
                formatter.info(f"  Policy Name: {policy_name}")
                formatter.info("")
                formatter.info("No changes made (dry run)")
            raise typer.Exit(0)

        # Confirm before executing (unless --force or --json)
        if not force and not cli_ctx.json_output:
            formatter.info("Assignment Details:")
            formatter.info(f"  User: {display_name}")
            formatter.info(
                f"  Policy Type: {_format_policy_type_display(resolved_type)}"
            )
            formatter.info(f"  Policy Name: {policy_name}")
            formatter.info("")
            confirm = typer.confirm("Proceed with assignment?")
            if not confirm:
                formatter.info("Assignment cancelled")
                raise typer.Exit(0)

        # Execute assignment
        async def _assign(
            policy_service: PolicyService, user_service: UserService
        ) -> None:
            await policy_service.assign_policy(user_id, resolved_type, policy_name)

        _run_async(ctx, _assign)

        # Success output
        if cli_ctx.json_output:
            formatter.json(
                {
                    "success": True,
                    "user_id": user_id,
                    "user_principal_name": upn,
                    "display_name": display_name,
                    "policy_type": resolved_type,
                    "policy_name": policy_name,
                }
            )
        else:
            formatter.success(
                f"Assigned {_format_policy_type_display(resolved_type)} policy "
                f"'{policy_name}' to {display_name}"
            )

    except TeamsPhoneError as e:
        formatter.error(e.message, remediation=e.remediation)
        raise typer.Exit(e.exit_code)


def _validate_policy_rows(
    rows: list[BulkPolicyRow],
    sync_service: PolicyService,
) -> BulkPolicyValidationResult:
    """Validate parsed policy CSV rows against the policy cache.

    Performs validation on each row:
    - Resolves policy type from CLI alias to API name
    - Rejects TeamsVoicemailPolicy (not supported for batch assignment)
    - Validates policy exists in cache

    Args:
        rows: List of BulkPolicyRow objects to validate.
        sync_service: PolicyService instance for cache-only validation.

    Returns:
        BulkPolicyValidationResult with validated rows and counts.
    """
    # Build set of valid API names for type validation
    valid_api_names = set(POLICY_TYPE_MAP.values())

    for row in rows:
        # Skip validation if row already has errors from parsing
        if row.errors:
            continue

        # Resolve policy type (CLI alias to API name)
        resolved_type = _resolve_policy_type(row.policy_type)

        # Check if the resolved type is a known API name
        if resolved_type is None or resolved_type not in valid_api_names:
            row.errors.append(
                f"Invalid policy type: '{row.policy_type}'. "
                "Valid types: calling, voicemail, emergency, emergency-routing, "
                "caller-id, dial-plan, voice-routing"
            )
            continue

        # Update the row with resolved policy type
        row.policy_type = resolved_type

        # Check for unsupported TeamsVoicemailPolicy
        if resolved_type == "TeamsVoicemailPolicy":
            row.errors.append(
                "TeamsVoicemailPolicy is not supported for batch assignment. "
                "Use PowerShell cmdlet Grant-CsTeamsVoicemailPolicy instead."
            )
            continue

        # Validate policy exists in cache
        try:
            sync_service.validate_policy(resolved_type, row.policy_name)
        except TeamsPhoneError as e:
            row.errors.append(e.message)

    # Compute counts
    valid_count = sum(1 for r in rows if not r.errors)
    error_count = sum(1 for r in rows if r.errors)
    warning_count = sum(1 for r in rows if r.warnings)

    return BulkPolicyValidationResult(
        rows=rows,
        valid_count=valid_count,
        error_count=error_count,
        warning_count=warning_count,
    )


@policies_app.command("bulk-assign")
def bulk_assign(
    ctx: typer.Context,
    csv_file: Path = typer.Argument(
        ...,
        help="Path to CSV file with user, policy_type, and policy_name columns.",
        exists=True,
        readable=True,
    ),
    dry_run: bool = typer.Option(
        False,
        "--dry-run",
        help="Validate and preview all assignments without executing.",
    ),
    continue_on_error: bool = typer.Option(
        False,
        "--continue-on-error",
        help="Continue processing after individual failures.",
    ),
    output: Optional[Path] = typer.Option(
        None,
        "--output",
        "-o",
        help="Path to write results CSV file.",
    ),
    yes: bool = typer.Option(
        False,
        "--yes",
        "-y",
        help="Skip confirmation prompt.",
    ),
) -> None:
    """Bulk assign policies from a CSV file.

    Reads a CSV file containing user, policy_type, and policy_name columns.
    Validates all rows against the policy cache, displays a summary, and executes
    assignments (unless --dry-run is specified).

    CSV format:
        user,policy_type,policy_name
        john@contoso.com,calling,AllowCalling
        jane@contoso.com,dial-plan,US-DialPlan

    Examples:
        # Preview bulk assignment (validation only)
        teams-phone policies bulk-assign assignments.csv --dry-run

        # Execute bulk assignment
        teams-phone policies bulk-assign assignments.csv

        # Continue on errors and save results to file
        teams-phone policies bulk-assign assignments.csv --continue-on-error -o results.csv
    """
    cli_ctx = get_context(ctx)
    formatter = cli_ctx.get_output_formatter()

    try:
        # Step 1: Parse CSV file
        rows = parse_policy_csv(csv_file)

        if not rows:
            if cli_ctx.json_output:
                formatter.json(
                    {
                        "status": "no_rows",
                        "message": "No data rows found in CSV file",
                        "file": str(csv_file),
                    }
                )
            else:
                formatter.info("No data rows found in CSV file")
            raise typer.Exit(0)

        # Step 2: Validate all rows against policy cache
        sync_service = _get_sync_service()

        # Check for staleness before validation
        _check_staleness(sync_service, formatter)

        validation_result = _validate_policy_rows(rows, sync_service)

        # Step 3: Display validation summary
        if cli_ctx.json_output:
            # JSON output for validation results
            validation_json = {
                "dry_run": dry_run,
                "file": str(csv_file),
                "total_rows": len(validation_result.rows),
                "valid_count": validation_result.valid_count,
                "error_count": validation_result.error_count,
                "warning_count": validation_result.warning_count,
                "rows": [
                    {
                        "row_number": r.row_number,
                        "user": r.user,
                        "policy_type": r.policy_type,
                        "policy_name": r.policy_name,
                        "errors": r.errors,
                        "warnings": r.warnings,
                        "valid": len(r.errors) == 0,
                    }
                    for r in validation_result.rows
                ],
            }
            if dry_run:
                formatter.json(validation_json)
                # Exit with code 1 if there are validation errors, 0 otherwise
                raise typer.Exit(1 if validation_result.error_count > 0 else 0)
        else:
            # Human-readable validation summary
            formatter.info("Validation Summary:")
            formatter.info(f"  File: {csv_file}")
            formatter.info(f"  Total rows: {len(validation_result.rows)}")
            formatter.info(f"  Valid: {validation_result.valid_count}")
            formatter.info(f"  Errors: {validation_result.error_count}")
            formatter.info(f"  Warnings: {validation_result.warning_count}")
            formatter.info("")

            # Show rows with errors
            if validation_result.error_count > 0:
                formatter.info("Validation Errors:")
                for row in validation_result.rows:
                    if row.errors:
                        error_str = "; ".join(row.errors)
                        formatter.info(
                            f"  Row {row.row_number}: {row.user} - {error_str}"
                        )
                formatter.info("")

            # Show rows with warnings (if any valid rows have warnings)
            warning_rows = [
                r for r in validation_result.rows if r.warnings and not r.errors
            ]
            if warning_rows:
                formatter.info("Warnings:")
                for row in warning_rows:
                    warning_str = "; ".join(row.warnings)
                    formatter.info(
                        f"  Row {row.row_number}: {row.user} - {warning_str}"
                    )
                formatter.info("")

        # Step 4: Handle dry-run exit
        if dry_run:
            formatter.info("Dry run complete. No changes made.")
            raise typer.Exit(1 if validation_result.error_count > 0 else 0)

        # Step 5: Handle validation errors without --continue-on-error
        if validation_result.error_count > 0 and not continue_on_error:
            raise ValidationError(
                f"Validation failed for {validation_result.error_count} row(s)",
                remediation=(
                    "Fix the errors in the CSV file and try again, or use\n"
                    "--continue-on-error to process only valid rows."
                ),
            )

        # Step 6: Check if there are any valid rows to process
        if validation_result.valid_count == 0:
            formatter.info("No valid rows to process")
            raise typer.Exit(1)

        # Step 7: Confirmation prompt (unless --yes or JSON mode)
        if not yes and not cli_ctx.json_output:
            formatter.info(
                f"Ready to assign {validation_result.valid_count} policy/policies"
            )
            if validation_result.error_count > 0:
                formatter.info(
                    f"  ({validation_result.error_count} row(s) will be skipped due to errors)"
                )
            formatter.info("")
            confirm = typer.confirm("Proceed with bulk assignment?")
            if not confirm:
                formatter.info("Bulk assignment cancelled")
                raise typer.Exit(0)

        # Step 8: Execute bulk assignments with progress bar
        # Initialize counters and results list
        success_count = 0
        fail_count = 0
        results: list[BulkPolicyResult] = []

        # Get valid rows only (rows without errors)
        valid_rows = [r for r in validation_result.rows if not r.errors]

        # Add skipped results for rows with validation errors
        for row in validation_result.rows:
            if row.errors:
                error_msg = "; ".join(row.errors)
                results.append(
                    BulkPolicyResult(
                        row_number=row.row_number,
                        user=row.user,
                        policy_type=row.policy_type,
                        policy_name=row.policy_name,
                        status="skipped",
                        error=error_msg,
                    )
                )

        # Execute with progress bar
        try:
            total = len(valid_rows)
            with formatter.progress(total, "Processing") as progress:
                # Get the task_id (first and only task added by progress context)
                task_id = list(progress.task_ids)[0]

                async def _execute_with_progress(
                    policy_service: PolicyService, user_service: UserService
                ) -> list[BulkPolicyResult]:
                    nonlocal success_count, fail_count

                    execution_results: list[BulkPolicyResult] = []

                    for idx, row in enumerate(valid_rows, start=1):
                        # Update progress description with current counts
                        desc = f"Processing [{idx}/{total}] OK:{success_count} FAIL:{fail_count}"
                        progress.update(task_id, description=desc)

                        try:
                            # Resolve user to get user_id
                            resolved_user = await user_service.resolve_user(row.user)

                            # Assign policy (synchronous - no polling needed)
                            await policy_service.assign_policy(
                                resolved_user.id, row.policy_type, row.policy_name
                            )

                            success_count += 1
                            execution_results.append(
                                BulkPolicyResult(
                                    row_number=row.row_number,
                                    user=row.user,
                                    policy_type=row.policy_type,
                                    policy_name=row.policy_name,
                                    status="success",
                                )
                            )

                        except TeamsPhoneError as e:
                            fail_count += 1
                            execution_results.append(
                                BulkPolicyResult(
                                    row_number=row.row_number,
                                    user=row.user,
                                    policy_type=row.policy_type,
                                    policy_name=row.policy_name,
                                    status="failed",
                                    error=e.message,
                                )
                            )
                            # Stop on first error if --continue-on-error not set
                            if not continue_on_error:
                                progress.update(task_id, advance=1)
                                break

                        # Advance progress bar
                        progress.update(task_id, advance=1)

                    return execution_results

                execution_results, _ = _run_async(ctx, _execute_with_progress)
                results.extend(execution_results)

                # Mark remaining rows as skipped if stopped early (not continue-on-error)
                processed_row_numbers = {r.row_number for r in results}
                for row in valid_rows:
                    if row.row_number not in processed_row_numbers:
                        results.append(
                            BulkPolicyResult(
                                row_number=row.row_number,
                                user=row.user,
                                policy_type=row.policy_type,
                                policy_name=row.policy_name,
                                status="skipped",
                                error="Stopped due to previous error",
                            )
                        )

        except KeyboardInterrupt:
            # Handle Ctrl+C gracefully - add remaining rows as skipped
            formatter.info("\nInterrupted. Processing partial results...")
            processed_row_numbers = {r.row_number for r in results}
            for row in valid_rows:
                if row.row_number not in processed_row_numbers:
                    results.append(
                        BulkPolicyResult(
                            row_number=row.row_number,
                            user=row.user,
                            policy_type=row.policy_type,
                            policy_name=row.policy_name,
                            status="skipped",
                            error="Cancelled by user",
                        )
                    )

        # Step 9: Write results to output file
        # Determine output path: use --output if provided, else generate timestamp-based name
        if output:
            output_path = output
        else:
            timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
            output_path = Path(f"bulk-policy-results-{timestamp}.csv")

        # Write results file
        try:
            write_policy_results_csv(results, output_path)
            results_written = True
        except ConfigurationError as e:
            # Warn but don't fail if assignments succeeded
            results_written = False
            if not cli_ctx.json_output:
                formatter.warning(f"Could not write results file: {e.message}")

        # Step 10: Display execution summary
        skipped_count = len(results) - success_count - fail_count

        if cli_ctx.json_output:
            # JSON output with full results
            json_output = {
                "status": "completed",
                "summary": {
                    "total": len(results),
                    "successful": success_count,
                    "failed": fail_count,
                    "skipped": skipped_count,
                },
                "results_file": str(output_path) if results_written else None,
                "results": [
                    {
                        "row": r.row_number,
                        "user": r.user,
                        "policy_type": r.policy_type,
                        "policy_name": r.policy_name,
                        "status": r.status,
                        "error": r.error,
                    }
                    for r in results
                ],
            }
            formatter.json(json_output)
        else:
            formatter.info("")
            formatter.info("Execution Summary:")
            formatter.info(f"  Successful: {success_count}")
            formatter.info(f"  Failed: {fail_count}")
            formatter.info(f"  Skipped: {skipped_count}")
            if results_written:
                formatter.info(f"  Results file: {output_path}")

        # Exit with appropriate code
        if fail_count > 0:
            raise typer.Exit(1)
        raise typer.Exit(0)

    except TeamsPhoneError as e:
        formatter.error(e.message, remediation=e.remediation)
        raise typer.Exit(e.exit_code)
