"""Bulk operations for Teams Phone CLI.

This module provides utilities for parsing and validating bulk operation
CSV files, such as bulk phone number assignments.
"""

from __future__ import annotations

import csv
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING

from teams_phone.exceptions import ConfigurationError, NotFoundError, ValidationError
from teams_phone.models import AssignmentStatus, NumberType

if TYPE_CHECKING:
    from teams_phone.services.location_service import LocationService
    from teams_phone.services.number_service import NumberService
    from teams_phone.services.user_service import UserService


@dataclass
class BulkAssignRow:
    """A single row from a bulk assignment CSV file.

    Attributes:
        row_number: 1-indexed row number for user-facing display.
        user: User identifier (UPN or ID).
        phone_number: Phone number to assign.
        location: Optional emergency location ID.
        errors: List of validation errors for this row.
        warnings: List of validation warnings for this row.
    """

    row_number: int
    user: str
    phone_number: str
    location: str | None
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)


@dataclass
class BulkValidationResult:
    """Result of validating a bulk operation CSV file.

    Attributes:
        rows: List of parsed and validated rows.
        valid_count: Number of rows without errors.
        error_count: Number of rows with at least one error.
        warning_count: Number of rows with at least one warning.
    """

    rows: list[BulkAssignRow]
    valid_count: int
    error_count: int
    warning_count: int


@dataclass
class BulkAssignResult:
    """Result of a single bulk assignment operation.

    Attributes:
        row_number: 1-indexed row number from the input CSV.
        user: User identifier from the input CSV.
        phone_number: Phone number from the input CSV.
        status: Operation result ('success', 'failed', 'skipped').
        error: Error message(s) if operation failed or was skipped.
    """

    row_number: int
    user: str
    phone_number: str
    status: str  # 'success' | 'failed' | 'skipped'
    error: str | None = None


@dataclass
class BulkPolicyRow:
    """A single row from a bulk policy assignment CSV file.

    Attributes:
        row_number: 1-indexed row number for user-facing display.
        user: User identifier (UPN or ID).
        policy_type: Policy type (resolved to API name).
        policy_name: Policy name.
        errors: List of validation errors for this row.
        warnings: List of validation warnings for this row.
    """

    row_number: int
    user: str
    policy_type: str
    policy_name: str
    errors: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)


@dataclass
class BulkPolicyValidationResult:
    """Result of validating a bulk policy operation CSV file.

    Attributes:
        rows: List of parsed and validated rows.
        valid_count: Number of rows without errors.
        error_count: Number of rows with at least one error.
        warning_count: Number of rows with at least one warning.
    """

    rows: list[BulkPolicyRow]
    valid_count: int
    error_count: int
    warning_count: int


@dataclass
class BulkPolicyResult:
    """Result of a single bulk policy assignment operation.

    Attributes:
        row_number: 1-indexed row number from the input CSV.
        user: User identifier from the input CSV.
        policy_type: Policy type from the input CSV.
        policy_name: Policy name from the input CSV.
        status: Operation result ('success', 'failed', 'skipped').
        error: Error message(s) if operation failed or was skipped.
    """

    row_number: int
    user: str
    policy_type: str
    policy_name: str
    status: str  # 'success' | 'failed' | 'skipped'
    error: str | None = None


def parse_assign_csv(file_path: Path) -> list[BulkAssignRow]:
    """Parse a bulk assignment CSV file.

    Reads a CSV file containing user/phone number/location data for bulk
    phone number assignments. Validates required columns exist and collects
    parsing errors and warnings per row.

    Args:
        file_path: Path to the CSV file to parse.

    Returns:
        List of BulkAssignRow objects, one per data row. Each row includes
        any errors or warnings encountered during parsing.

    Raises:
        ValidationError: If required columns ('user', 'phone_number') are
            missing from the CSV header.

    Note:
        Uses UTF-8 with BOM support (utf-8-sig encoding) to handle
        PowerShell exports which may include a byte order mark.
    """
    required_columns = {"user", "phone_number"}

    if not file_path.exists():
        raise ValidationError(
            f"CSV file not found: {file_path}",
            remediation="Ensure the file path is correct and the file exists.",
        )

    try:
        with file_path.open(encoding="utf-8-sig") as f:
            reader = csv.DictReader(f)

            # Validate header has required columns
            if reader.fieldnames is None:
                raise ValidationError(
                    "CSV file is empty or has no header row",
                    remediation="Provide a CSV file with a header row containing "
                    "'user' and 'phone_number' columns.",
                )

            header_columns = set(reader.fieldnames)
            missing_columns = required_columns - header_columns

            if missing_columns:
                raise ValidationError(
                    f"CSV missing required columns: {', '.join(sorted(missing_columns))}",
                    remediation="Ensure the CSV header includes 'user' and "
                    "'phone_number' columns.",
                )

            has_location_column = "location" in header_columns
            rows: list[BulkAssignRow] = []

            # Row number starts at 2 (1-indexed, accounting for header row)
            for row_number, row_data in enumerate(reader, start=2):
                errors: list[str] = []
                warnings: list[str] = []

                # Extract and validate required fields
                user = row_data.get("user", "").strip()
                phone_number = row_data.get("phone_number", "").strip()

                if not user:
                    errors.append("Missing required field: user")
                if not phone_number:
                    errors.append("Missing required field: phone_number")

                # Extract optional location field
                location: str | None = None
                if has_location_column:
                    location_value = row_data.get("location", "").strip()
                    if location_value:
                        location = location_value
                    else:
                        warnings.append("Empty optional field: location")

                rows.append(
                    BulkAssignRow(
                        row_number=row_number,
                        user=user,
                        phone_number=phone_number,
                        location=location,
                        errors=errors,
                        warnings=warnings,
                    )
                )

            return rows

    except csv.Error as e:
        raise ValidationError(
            f"CSV parsing error: {e}",
            remediation="Check the CSV file for formatting issues such as "
            "unbalanced quotes or invalid characters.",
        ) from e


def parse_policy_csv(file_path: Path) -> list[BulkPolicyRow]:
    """Parse a bulk policy assignment CSV file.

    Reads a CSV file containing user/policy_type/policy_name data for bulk
    policy assignments. Validates required columns exist and collects
    parsing errors and warnings per row.

    Args:
        file_path: Path to the CSV file to parse.

    Returns:
        List of BulkPolicyRow objects, one per data row. Each row includes
        any errors or warnings encountered during parsing.

    Raises:
        ValidationError: If required columns ('user', 'policy_type', 'policy_name')
            are missing from the CSV header.

    Note:
        Uses UTF-8 with BOM support (utf-8-sig encoding) to handle
        PowerShell exports which may include a byte order mark.
    """
    required_columns = {"user", "policy_type", "policy_name"}

    if not file_path.exists():
        raise ValidationError(
            f"CSV file not found: {file_path}",
            remediation="Ensure the file path is correct and the file exists.",
        )

    try:
        with file_path.open(encoding="utf-8-sig") as f:
            reader = csv.DictReader(f)

            # Validate header has required columns
            if reader.fieldnames is None:
                raise ValidationError(
                    "CSV file is empty or has no header row",
                    remediation="Provide a CSV file with a header row containing "
                    "'user', 'policy_type', and 'policy_name' columns.",
                )

            header_columns = set(reader.fieldnames)
            missing_columns = required_columns - header_columns

            if missing_columns:
                raise ValidationError(
                    f"CSV missing required columns: {', '.join(sorted(missing_columns))}",
                    remediation="Ensure the CSV header includes 'user', "
                    "'policy_type', and 'policy_name' columns.",
                )

            rows: list[BulkPolicyRow] = []

            # Row number starts at 2 (1-indexed, accounting for header row)
            for row_number, row_data in enumerate(reader, start=2):
                errors: list[str] = []
                warnings: list[str] = []

                # Extract and validate required fields
                user = row_data.get("user", "").strip()
                policy_type = row_data.get("policy_type", "").strip()
                policy_name = row_data.get("policy_name", "").strip()

                if not user:
                    errors.append("Missing required field: user")
                if not policy_type:
                    errors.append("Missing required field: policy_type")
                if not policy_name:
                    errors.append("Missing required field: policy_name")

                rows.append(
                    BulkPolicyRow(
                        row_number=row_number,
                        user=user,
                        policy_type=policy_type,
                        policy_name=policy_name,
                        errors=errors,
                        warnings=warnings,
                    )
                )

            return rows

    except csv.Error as e:
        raise ValidationError(
            f"CSV parsing error: {e}",
            remediation="Check the CSV file for formatting issues such as "
            "unbalanced quotes or invalid characters.",
        ) from e


def write_policy_results_csv(
    results: list[BulkPolicyResult], output_path: Path
) -> None:
    """Write bulk policy operation results to a CSV file.

    Creates a CSV file containing the results of a bulk policy operation, with
    columns for row number, user, policy type, policy name, status, and any error messages.

    Args:
        results: List of BulkPolicyResult objects to write.
        output_path: Path where the results CSV should be written.

    Raises:
        ConfigurationError: If the file cannot be written due to permission
            issues or if the parent directory cannot be created.

    Note:
        Uses UTF-8 encoding without BOM for output. Multiple error messages
        are concatenated with semicolon delimiter.
    """
    # Ensure parent directory exists
    try:
        output_path.parent.mkdir(parents=True, exist_ok=True)
    except PermissionError as e:
        raise ConfigurationError(
            f"Cannot create output directory {output_path.parent}: {e}",
            remediation=(
                f"Check that you have write permissions to {output_path.parent}.\n"
                "You may need to create the directory manually or run with "
                "elevated privileges."
            ),
        ) from e

    # Write results to CSV
    fieldnames = ["row", "user", "policy_type", "policy_name", "status", "error"]

    try:
        with output_path.open("w", encoding="utf-8", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()

            for result in results:
                writer.writerow(
                    {
                        "row": result.row_number,
                        "user": result.user,
                        "policy_type": result.policy_type,
                        "policy_name": result.policy_name,
                        "status": result.status,
                        "error": result.error or "",
                    }
                )
    except PermissionError as e:
        raise ConfigurationError(
            f"Cannot write to output file {output_path}: {e}",
            remediation=(
                f"Check that you have write permissions to {output_path}.\n"
                "The file may be open in another application or you may need "
                "elevated privileges."
            ),
        ) from e


def write_results_csv(results: list[BulkAssignResult], output_path: Path) -> None:
    """Write bulk operation results to a CSV file.

    Creates a CSV file containing the results of a bulk operation, with
    columns for row number, user, phone number, status, and any error messages.

    Args:
        results: List of BulkAssignResult objects to write.
        output_path: Path where the results CSV should be written.

    Raises:
        ConfigurationError: If the file cannot be written due to permission
            issues or if the parent directory cannot be created.

    Note:
        Uses UTF-8 encoding without BOM for output. Multiple error messages
        are concatenated with semicolon delimiter.
    """
    # Ensure parent directory exists
    try:
        output_path.parent.mkdir(parents=True, exist_ok=True)
    except PermissionError as e:
        raise ConfigurationError(
            f"Cannot create output directory {output_path.parent}: {e}",
            remediation=(
                f"Check that you have write permissions to {output_path.parent}.\n"
                "You may need to create the directory manually or run with "
                "elevated privileges."
            ),
        ) from e

    # Write results to CSV
    fieldnames = ["row", "user", "phone_number", "status", "error"]

    try:
        with output_path.open("w", encoding="utf-8", newline="") as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()

            for result in results:
                writer.writerow(
                    {
                        "row": result.row_number,
                        "user": result.user,
                        "phone_number": result.phone_number,
                        "status": result.status,
                        "error": result.error or "",
                    }
                )
    except PermissionError as e:
        raise ConfigurationError(
            f"Cannot write to output file {output_path}: {e}",
            remediation=(
                f"Check that you have write permissions to {output_path}.\n"
                "The file may be open in another application or you may need "
                "elevated privileges."
            ),
        ) from e


class BulkOperations:
    """Bulk operations for phone number management.

    Provides validation and execution of bulk operations such as
    phone number assignments from CSV files.

    Attributes:
        user_service: Service for user resolution and lookup.
        number_service: Service for phone number operations.
        location_service: Service for emergency location validation.
    """

    def __init__(
        self,
        user_service: UserService,
        number_service: NumberService,
        location_service: LocationService,
    ) -> None:
        """Initialize BulkOperations with required services.

        Args:
            user_service: Service for resolving users by UPN, name, or ID.
            number_service: Service for phone number lookup and operations.
            location_service: Service for emergency location validation.
        """
        self.user_service = user_service
        self.number_service = number_service
        self.location_service = location_service

    async def validate_assign_csv(
        self, rows: list[BulkAssignRow]
    ) -> BulkValidationResult:
        """Validate bulk assignment rows against live services.

        Validates each row by checking:
        - User exists and can be resolved
        - Phone number is in inventory and unassigned
        - Location is valid (if provided)

        Also generates warnings for:
        - Stale location cache
        - Calling Plan numbers without emergency location

        Args:
            rows: List of BulkAssignRow objects to validate.

        Returns:
            BulkValidationResult with validated rows and counts.
        """
        # Check cache staleness once at start
        cache_stale = self.location_service.is_cache_stale()

        for row in rows:
            # Skip validation if row already has errors from parsing
            # (missing user or phone_number)
            if row.errors:
                continue

            # Add stale cache warning to each row
            if cache_stale:
                row.warnings.append(
                    "Location cache is stale; location validation may be inaccurate"
                )

            # Validate user existence
            try:
                await self.user_service.resolve_user(row.user)
            except NotFoundError:
                row.errors.append(f"User '{row.user}' not found")
            except ValidationError as e:
                row.errors.append(f"Ambiguous user: {e.message}")

            # Validate phone number availability
            number = None
            try:
                number = await self.number_service.get_number(row.phone_number)
                if number.assignment_status != AssignmentStatus.UNASSIGNED:
                    row.errors.append(
                        f"Number '{row.phone_number}' is already assigned"
                    )
            except NotFoundError:
                row.errors.append(f"Number '{row.phone_number}' not in inventory")

            # Validate location (if provided)
            if row.location:
                try:
                    self.location_service.validate_location(row.location)
                except NotFoundError:
                    row.errors.append(f"Location '{row.location}' not found")

            # Warn about Calling Plan numbers without location
            if (
                number is not None
                and number.number_type == NumberType.CALLING_PLAN
                and not row.location
            ):
                row.warnings.append("Calling Plan number without emergency location")

        # 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 BulkValidationResult(
            rows=rows,
            valid_count=valid_count,
            error_count=error_count,
            warning_count=warning_count,
        )
