# Copyright (c) 2023 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
#
import os
import sys
import time
import uuid
from datetime import datetime, timedelta
from decimal import Decimal, InvalidOperation
from math import ceil
from typing import Any

import simplejson as json
from rich.console import Console
from rich.table import Table

from src.api.v4_anomalies import (
    get_anomalies_processing_status,
    get_reviewable_anomalies,
    update_anomalies_batch,
)
from src.utils.console import eprint
from src.utils.core import chunks, var2bool
from src.utils.csv import read_anomaly_csv_file_to_dict
from src.utils.data import JSONSanity
from src.utils.system import check_input_file, download_file
from src.utils.time import isodatetimenow, utc_datetime_from_anything

# Default namespace is 'live', but can be overridden with CZ_ANOMALIES_NAMESPACE env var
DEFAULT_NAMESPACE = "live"

# API Field Mapping Configuration
# Required fields that must be present in every anomaly
REQUIRED_FIELD_MAPPING = {
    "send?": "send_notification",
    "run_id": "anomaly_run_id",
    "cz_organization_id": "cz_organization_id",
    "anomaly_type": "anomaly_type",
    "anomaly_cost": "anomaly_cost",
    "dimension": "principal_dimension",  # Primary dimension name (e.g., services, product_family, costcontext:...)
    "element": "element",
    "account_id": "connection_id",
    "start_time": "start_time",
    "end_time": "end_time",
    # "url": "url"
}

# Optional fields that may be present
OPTIONAL_FIELD_MAPPING = {
    "view_id": "view_id",
    "view_name": "view_name",
    "product_name": "service",
    "product_family": "product_family",
    "is_ongoing": "is_ongoing",
    "real_cost": "real_cost",
    "anomaly_duration": "anomaly_duration",
    "customer_name": "customer_name",
    "context_key": "context_key",
}

# Valid anomaly types for the API
VALID_ANOMALY_TYPES = {"anomaly-views", "anomaly-accounts", "anomaly-accounts-services", "anomaly-accounts-services-product_family"}

# All allowed API fields (for filtering unknown columns)
ALLOWED_API_FIELDS = set(REQUIRED_FIELD_MAPPING.values()) | set(OPTIONAL_FIELD_MAPPING.values())

# Initialize Rich console for table display
console = Console()


def get(arguments):
    # Get namespace from environment variable or use default
    namespace = os.environ.get("CZ_ANOMALIES_NAMESPACE", DEFAULT_NAMESPACE)
    debug = arguments.get("--debug", False)

    print("Processing anomalies for retrieval...", end="", flush=True)

    # Start reviewable anomalies process using v4 API
    try:
        response = get_reviewable_anomalies(namespace=namespace, debug=debug)
        if not response.success or not response.s3_url:
            print(f"\nFailed to start anomalies processing: {response}")
            return
        s3_url = response.s3_url
    except Exception as e:
        print(f"\nError starting anomalies processing: {e}")
        return

    print(f"...running (Checking {s3_url})")

    print("Waiting for data", end="")
    download_url = None
    finished = False

    while not finished:
        try:
            status = get_anomalies_processing_status(s3_url=s3_url, debug=debug)
            print(".", end="", flush=True)
            finished = status.processing_complete
            download_url = status.download_url

            if status.error_message:
                print(f"\nProcessing error: {status.error_message}")
                return

        except Exception as e:
            print(f"\nError checking processing status: {e}")
            return

        time.sleep(5)

    print("Done")

    if download_url:
        if arguments.get("--output"):
            output_filename = arguments.get("--output")
        else:
            output_filename = f"{namespace}-reviewable-notable-anomalies-{isodatetimenow()}.csv"

        download_file(download_url, output_filename, True)
    else:
        print(f"Something went wrong, invalid download URL {download_url}")


def _parse_uuid(value: Any) -> str:
    if value is None:
        raise ValueError("cz_organization_id is required and must be a UUID")
    s = str(value).strip()
    try:
        uuid_obj = uuid.UUID(s)
        return str(uuid_obj)
    except Exception as e:
        raise ValueError(f"Invalid cz_organization_id UUID: {s}") from e


def _parse_decimal(value: Any) -> Decimal | None:
    if value is None or str(value).strip() == "":
        return None
    if isinstance(value, Decimal):
        return value
    try:
        s = str(value).replace(",", "").strip()
        return Decimal(s)
    except (InvalidOperation, ValueError):
        return None


def _normalize_anomaly_type(anomaly_type: Any) -> str:
    s = str(anomaly_type).strip().lower() if anomaly_type is not None else ""
    if s in VALID_ANOMALY_TYPES:
        return s
    # Legacy/misc values default to anomaly-accounts (most common type)
    return "anomaly-accounts"


def _transform_anomaly_for_v4(row: dict) -> dict:
    """
    Transform a CSV anomaly row to the strict format expected by the v4 API.
    - Build a new payload with only allowed fields
    - Validate required fields
    - Normalize types
    """
    payload: dict[str, Any] = {}

    # Required: anomaly_run_id
    anomaly_run_id = row.get("run_id")
    if not anomaly_run_id or str(anomaly_run_id).strip() == "":
        raise ValueError(f"Missing required run_id for row {row.get('_row_num', '?')}")
    payload["anomaly_run_id"] = str(anomaly_run_id).strip()

    # Required: cz_organization_id (UUID)
    payload["cz_organization_id"] = _parse_uuid(row.get("cz_organization_id"))

    # Required: anomaly_type (normalized)
    payload["anomaly_type"] = _normalize_anomaly_type(row.get("anomaly_type"))

    # Required: principal_dimension (from 'dimension' column)
    principal_dimension = row.get("dimension")
    if not principal_dimension or str(principal_dimension).strip() == "":
        raise ValueError(f"Missing required principal_dimension (dimension) for row {row.get('_row_num', '?')}")
    payload["principal_dimension"] = str(principal_dimension)

    # Required: element
    element = row.get("element")
    if not element or str(element).strip() == "":
        raise ValueError(f"Missing required element for row {row.get('_row_num', '?')}")
    payload["element"] = str(element)

    # Required: connection_id (from account_id) - accept ANY value and cast to string
    # If None, cast to string "None". ANY STRING IS VALID.
    connection_id = row.get("account_id")
    if connection_id is None:
        payload["connection_id"] = "None"
    elif connection_id == "":
        payload["connection_id"] = ""  # Empty string is also valid
    else:
        payload["connection_id"] = str(connection_id).strip()

    # NO VALIDATION - ANY STRING IS VALID FOR CONNECTION_ID

    # Required: start_time, end_time (accept datetimes and strings)
    start_time = utc_datetime_from_anything(row.get("start_time"))
    # Calculate end_time: use given end_time if parseable; else compute via anomaly_duration; else same as start
    end_time_raw = row.get("end_time")
    anomaly_duration_raw = row.get("anomaly_duration")
    end_time: datetime
    if end_time_raw not in (None, ""):
        try:
            end_time = utc_datetime_from_anything(row.get("end_time"))
        except Exception:
            # fallback compute
            if anomaly_duration_raw not in (None, ""):
                try:
                    end_time = start_time + timedelta(hours=float(str(anomaly_duration_raw)))
                except Exception:
                    end_time = start_time
            else:
                end_time = start_time
    else:
        if anomaly_duration_raw not in (None, ""):
            try:
                end_time = start_time + timedelta(hours=float(str(anomaly_duration_raw)))
            except Exception:
                end_time = start_time
        else:
            end_time = start_time

    payload["start_time"] = start_time
    payload["end_time"] = end_time

    # Optional mappings
    if row.get("view_id") not in (None, ""):
        payload["view_id"] = str(row.get("view_id"))
    if row.get("view_name") not in (None, ""):
        payload["view_name"] = str(row.get("view_name"))
    if row.get("product_name") not in (None, ""):
        payload["service"] = str(row.get("product_name"))
    if row.get("product_family") not in (None, ""):
        payload["product_family"] = str(row.get("product_family"))

    # Booleans
    payload["is_ongoing"] = var2bool(row.get("is_ongoing"), default=False)
    send_notification = row.get("send_notification")
    if send_notification is None and "send?" in row:
        send_notification = row.get("send?")
    payload["send_notification"] = var2bool(send_notification, default=False)

    # Costs
    anomaly_cost = row.get("anomaly_cost")
    if anomaly_cost not in (None, ""):
        try:
            payload["anomaly_cost"] = str(float(str(anomaly_cost).replace(",", "").strip()))
        except Exception:
            payload["anomaly_cost"] = "0"

    real_cost = _parse_decimal(row.get("real_cost"))
    if real_cost is not None:
        payload["real_cost"] = real_cost

    # Duration string
    if anomaly_duration_raw not in (None, ""):
        try:
            payload["anomaly_duration"] = str(float(str(anomaly_duration_raw).replace(",", "").strip()))
        except Exception:
            payload["anomaly_duration"] = "0"

    # Dashboard URL
    payload["url"] = str(row.get("url"))

    # Customer name
    if row.get("customer_name") not in (None, ""):
        payload["customer_name"] = str(row.get("customer_name"))

    # Context key
    if row.get("context_key") not in (None, ""):
        payload["context_key"] = str(row.get("context_key"))

    # Ensure only allowed fields are present (drop unknowns)
    payload = {k: v for k, v in payload.items() if k in ALLOWED_API_FIELDS}

    return payload


def _display_batch_anomalies_table(batch_num: int, anomalies: list, dry_run: bool = False):
    """Display anomalies for a batch in a simplified Rich table."""
    if not anomalies:
        return

    # Create table with NO TRUNCATION - disable Rich's automatic width management
    table = Table(
        title=f"Batch {batch_num:02d} - {'DRY-RUN ' if dry_run else ''}Processed Anomalies",
        show_header=True,
        header_style="bold blue",
        show_lines=False,  # Remove lines between rows for cleaner look
        padding=(0, 1),  # Minimal padding
        box=None,  # Remove box borders for lighter appearance
        width=None,  # Don't constrain table width
        expand=False,  # Don't expand to fill terminal
        collapse_padding=False,  # Keep padding as specified
    )

    # All required columns with NO width restrictions to prevent truncation
    table.add_column("✓", style="green", justify="center", no_wrap=True, width=2)  # Status
    table.add_column("📧", style="blue", justify="center", no_wrap=True, width=2)  # Sent
    table.add_column("Run ID", style="cyan", no_wrap=False)
    table.add_column("Customer", style="green", no_wrap=False)
    table.add_column("Cost", style="red", justify="right", no_wrap=False)
    table.add_column("Start Time", style="blue", no_wrap=False)
    table.add_column("End Time", style="blue", no_wrap=False)
    table.add_column("Duration", style="magenta", no_wrap=False)
    table.add_column("Account ID", style="yellow", no_wrap=False)
    table.add_column("Product Name", style="bright_green", no_wrap=False)
    table.add_column("Product Family", style="bright_cyan", no_wrap=False)
    table.add_column("Type", justify="center", style="white", no_wrap=False)

    # Add rows with complete data - NO TRUNCATION
    for anomaly in anomalies:
        table.add_row(
            str(anomaly.get("status", "✅")),  # Status icon
            str(anomaly.get("sent", "📭")),  # Sent icon
            str(anomaly["run_id"]),
            str(anomaly["customer"]),
            str(anomaly["cost"]),
            str(anomaly.get("start_time", "N/A")),
            str(anomaly.get("end_time", "N/A")),
            str(anomaly.get("duration", "N/A")),
            str(anomaly.get("account_id", "N/A")),
            str(anomaly.get("product_name", "N/A")),
            str(anomaly.get("product_family", "N/A")),
            str(anomaly["type"]),
        )

    console.print(table)


def _create_duplicate_key(row):
    """Create a comprehensive composite key for unique anomaly identification.

    This key is used to prevent duplicate anomalies from being processed or displayed
    by identifying anomalies that represent the same logical business event.

    The uniqueness is determined by the combination of:
    - cz_organization_id: Organization identifier
    - customer_name: Customer name
    - anomaly_cost: Cost amount
    - start_time: Anomaly start time
    - account_id: Account/connection identifier
    - product_name: Product or service name
    - product_family: Product family/category

    Note: run_id is NOT included as it should be unique per anomaly record,
    but multiple records might represent the same logical anomaly.

    Args:
        row: Dictionary containing anomaly data

    Returns:
        str: Composite key for duplicate detection
    """
    org_id = str(row.get("cz_organization_id", "N/A"))
    customer_name = str(row.get("customer_name", "Unknown"))
    cost = str(row.get("anomaly_cost", "0"))
    start_time = str(row.get("start_time", "N/A"))
    account_id = str(row.get("account_id", "N/A"))
    product_name = str(row.get("product_name", "N/A"))
    product_family = str(row.get("product_family", "N/A"))
    view_id = str(row.get("view_id", "N/A"))
    element = str(row.get("element", "N/A"))

    return f"{org_id}|{customer_name}|{cost}|{start_time}|{account_id}|{product_name}|{product_family}|{view_id}|{element}"


def _detect_and_flag_duplicates(anomalies):
    """Detect duplicates and flag them in the anomaly data.

    This function assumes all input anomalies are already marked for sending.

    Args:
        anomalies: List of anomaly dictionaries (all marked for sending)

    Returns:
        tuple: (processed_anomalies, duplicate_count, duplicate_info)
            - processed_anomalies: List with duplicates flagged
            - duplicate_count: Number of duplicate records found
            - duplicate_info: List of duplicate details for reporting
    """
    seen_keys = {}
    duplicates_info = []
    duplicate_count = 0

    for i, anomaly in enumerate(anomalies):
        duplicate_key = _create_duplicate_key(anomaly)

        if duplicate_key in seen_keys:
            # Mark as duplicate
            anomaly["_is_duplicate"] = True
            anomaly["_duplicate_of_row"] = seen_keys[duplicate_key]
            duplicate_count += 1

            # Add to duplicates info for reporting
            duplicates_info.append(
                {
                    "current_row": anomaly.get("_row_num", i + 1),
                    "original_row": seen_keys[duplicate_key],
                    "run_id": anomaly.get("run_id", "N/A"),
                    "customer": anomaly.get("customer_name", "N/A"),
                    "cost": anomaly.get("anomaly_cost", "N/A"),
                }
            )
        else:
            # First occurrence - mark as original and not duplicate
            anomaly["_is_duplicate"] = False
            seen_keys[duplicate_key] = anomaly.get("_row_num", i + 1)

    return anomalies, duplicate_count, duplicates_info


def _get_anomaly_name(row):
    """Generate a unique name for an anomaly based on available fields."""
    # Use run_id for uniqueness if available, otherwise build descriptive name
    if row.get("run_id"):
        run_id_short = str(row["run_id"])[-8:]  # Last 8 chars for display
        return f"{run_id_short}"

    # Fallback to building name from available fields
    SK_COLUMNS = ["product_name", "product_family", "feature", "dimension", "element"]
    name_parts = [str(row[x]) for x in SK_COLUMNS if row.get(x) and str(row[x]) != "None"]
    return "-".join(name_parts) if name_parts else "unknown"


def _create_positional_mapping(valid_pairs: list[tuple[dict, dict]]) -> list[dict]:
    """
    Create a positional list of original CSV row data that matches the order sent to API.

    This replaces run_id-based mapping since run_ids are not unique across anomalies.
    The API returns results in the same order as the input, so we can map by position.

    Args:
        valid_pairs: List of (original_row, transformed_row) tuples in order sent to API

    Returns:
        list: List of original row data in the same order as sent to API
    """
    return [orig_row for orig_row, _ in valid_pairs]


def _calculate_duration(orig_row: dict) -> str:
    """
    Calculate duration string from anomaly data.

    Args:
        orig_row: Original CSV row data

    Returns:
        str: Human-readable duration or "N/A"
    """
    if orig_row.get("start_time") and orig_row.get("end_time"):
        try:
            from datetime import datetime

            start = datetime.fromisoformat(str(orig_row["start_time"]).replace("Z", "+00:00"))
            end = datetime.fromisoformat(str(orig_row["end_time"]).replace("Z", "+00:00"))
            duration_days = (end - start).days
            return f"{duration_days} days" if duration_days > 0 else "<1 day"
        except (ValueError, TypeError):
            pass

    if orig_row.get("anomaly_duration"):
        try:
            hours = float(str(orig_row["anomaly_duration"]).strip())
            return f"{int(hours / 24)} days" if hours >= 24 else f"{hours:.1f} hours"
        except (ValueError, TypeError):
            pass

    return "N/A"


def _format_anomaly_for_display(orig_row: dict, status: str = "processed", notification_sent: bool = False, dry_run: bool = False) -> dict:
    """
    Format an original CSV row for display in the results table.

    Args:
        orig_row: Original CSV row data
        status: Processing status (processed, invalid, failed)
        notification_sent: Whether notification was sent
        dry_run: Whether this is a dry run

    Returns:
        dict: Formatted anomaly data for display
    """
    run_id = orig_row.get("run_id", "N/A")

    # Create status indicators using emojis
    if status == "processed":
        status_icon = "✅"
        sent_icon = "📧" if notification_sent and not dry_run else "⏸️" if dry_run else "📭"
    elif status == "invalid":
        status_icon = "⚠️"
        sent_icon = "❌"
    else:  # failed
        status_icon = "❌"
        sent_icon = "❌"

    return {
        "status": status_icon,
        "sent": sent_icon,
        "run_id": run_id[-8:] if run_id != "N/A" else "N/A",  # Last 8 chars for display
        "customer": orig_row.get("customer_name", "Unknown"),
        "cost": f"${float(str(orig_row.get('anomaly_cost', '0')).replace(',', '')):.2f}",
        "start_time": str(orig_row.get("start_time", "N/A"))[:19] if orig_row.get("start_time") else "N/A",
        "end_time": str(orig_row.get("end_time", "N/A"))[:19] if orig_row.get("end_time") else "N/A",
        "duration": _calculate_duration(orig_row),
        "account_id": str(orig_row.get("account_id", "N/A")),
        "product_name": str(orig_row.get("product_name", "N/A")),
        "product_family": str(orig_row.get("product_family", "N/A")),
        "service": str(orig_row.get("product_name", orig_row.get("dimension", "N/A"))),
        "type": (str(orig_row.get("anomaly_type") or "cost")).lower(),
        "dry_run": dry_run,
    }


def _should_display_anomaly(orig_row: dict, dry_run: bool) -> bool:
    """
    Determine if an anomaly should be displayed in the results.

    Args:
        orig_row: Original CSV row data
        dry_run: Whether this is a dry run

    Returns:
        bool: True if the anomaly should be displayed
    """
    # In dry-run mode, show all processed anomalies
    # In regular mode, show all anomalies (both sent and not sent)
    # This fixes the issue where anomalies weren't showing up
    return True


class BatchResultProcessor:
    """
    Handles processing and display of batch results.

    This class encapsulates the complex logic for matching API results
    to original CSV data and preparing them for display.
    """

    def __init__(self):
        self.batch_anomalies: dict[int, list[dict]] = {}

    def process_batch_results(self, response, original_rows: list[dict], batch_count: int, dry_run: bool, quiet: bool) -> tuple[int, int, int, int]:
        """
        Process API response results and prepare for display using positional mapping.

        Args:
            response: API response object
            original_rows: List of original CSV data in the same order as sent to API
            batch_count: Current batch number
            dry_run: Whether this is a dry run
            quiet: Whether to suppress display output

        Returns:
            tuple: (processed_count, sent_count, invalid_count, failed_count)
        """
        batch_processed = 0
        batch_sent = 0
        batch_invalid = 0
        batch_failed = 0

        if not response.results:
            # Fallback to summary counts if no individual results
            return (
                response.successfully_processed,
                response.notifications_sent if not dry_run else 0,
                response.validation_errors,
                response.processing_errors,
            )

        # Initialize batch collection
        if batch_count not in self.batch_anomalies:
            self.batch_anomalies[batch_count] = []

        # Process results using positional mapping
        # API returns results in the same order as input, so we can map by index
        for i, result in enumerate(response.results):
            # Get original row by position (safer than run_id mapping)
            orig_row = original_rows[i] if i < len(original_rows) else None

            if result.status == "processed":
                batch_processed += 1
                if result.notification_sent:
                    batch_sent += 1

                # Collect anomalies for display (all processed anomalies)
                if not quiet and orig_row and _should_display_anomaly(orig_row, dry_run):
                    formatted_anomaly = _format_anomaly_for_display(
                        orig_row, status="processed", notification_sent=result.notification_sent, dry_run=dry_run
                    )
                    self.batch_anomalies[batch_count].append(formatted_anomaly)

            elif result.status == "invalid":
                batch_invalid += 1
                if not quiet and orig_row:
                    # Add to display with error status
                    formatted_anomaly = _format_anomaly_for_display(orig_row, status="invalid", notification_sent=False, dry_run=dry_run)
                    self.batch_anomalies[batch_count].append(formatted_anomaly)

                    name = _get_anomaly_name(orig_row)
                    eprint(f"{batch_count:02}: ⚠️  INVALID: {orig_row.get('customer_name', '')} - {name}")
                    if result.error_message:
                        eprint(f"      Reason: {result.error_message}")

            elif result.status == "failed":
                batch_failed += 1
                if not quiet and orig_row:
                    # Add to display with failed status
                    formatted_anomaly = _format_anomaly_for_display(orig_row, status="failed", notification_sent=False, dry_run=dry_run)
                    self.batch_anomalies[batch_count].append(formatted_anomaly)

                    name = _get_anomaly_name(orig_row)
                    eprint(f"{batch_count:02}: ❌ FAILED: {orig_row.get('customer_name', '')} - {name}")
                    if result.error_message:
                        eprint(f"      Error: {result.error_message}")

        return batch_processed, batch_sent, batch_invalid, batch_failed

    def display_batch_results(self, batch_count: int, quiet: bool, dry_run: bool):
        """
        Display the results table for a batch.

        Args:
            batch_count: Batch number to display
            quiet: Whether to suppress output
            dry_run: Whether this is a dry run
        """
        if batch_count in self.batch_anomalies and self.batch_anomalies[batch_count] and not quiet:
            _display_batch_anomalies_table(batch_count, self.batch_anomalies[batch_count], dry_run)


def update(arguments):
    # Get namespace from environment variable or use default
    namespace = os.environ.get("CZ_ANOMALIES_NAMESPACE", DEFAULT_NAMESPACE)
    input_file = arguments.get("--file")
    quiet = arguments.get("--quiet")
    dry_run = arguments.get("--dryrun")
    raw = arguments.get("--raw")
    debug = arguments.get("--debug", False)

    # Validate the input file exists and is a CSV file
    input_file = check_input_file(input_file, file_type="CSV file", required_extension=".csv")
    print(f"Processing anomalies for namespace {namespace} from {input_file}...")
    # Handle parsing errors with user-friendly messages
    try:
        # Get anomaly data along with any validation errors from CSV parsing
        anomalies, lines, lines_filtered, validation_errors = read_anomaly_csv_file_to_dict(input_file)

        # Process the loaded data
        file_size = sys.getsizeof(json.dumps(anomalies, cls=JSONSanity))

        # STEP 1: Filter out anomalies that are NOT marked for sending
        send_anomalies = [x for x in anomalies if x.get("send_notification", False)]
        non_send_count = len(anomalies) - len(send_anomalies)

        # STEP 2: Detect and flag duplicates ONLY among anomalies marked for sending
        send_anomalies, duplicate_count, duplicates_info = _detect_and_flag_duplicates(send_anomalies)

        # STEP 3: Get final processing list (send anomalies minus duplicates)
        processing_anomalies = [x for x in send_anomalies if not x.get("_is_duplicate", False)]

        # Display loading summary
        print(f"Loaded {len(anomalies)} anomalies from {input_file} ({lines} lines, {file_size:,} bytes)")
        if non_send_count > 0:
            print(f"Filtered out {non_send_count} anomalies not marked for sending")
        if duplicate_count > 0:
            print(f"Found {duplicate_count} duplicate(s) among anomalies marked for sending")
        print(f"Processing {len(processing_anomalies)} unique anomalies for sending")
    except ValueError as e:
        # Handle CSV format errors with enhanced messages
        error_str = str(e)
        eprint("\n" + "=" * 80)
        eprint("❌ CSV File Error")
        eprint("=" * 80)

        # Extract specific error details if available
        if "could not parse" in error_str:
            # Parse out column and value info if present
            import re

            match = re.search(r"could not parse `([^`]+)` as dtype `[^`]+` at column '([^']+)'", error_str)
            if match:
                value, column = match.groups()
                eprint("\n📍 Problem Found:")
                eprint(f"   Column: '{column}'")
                eprint(f"   Invalid value: '{value}'")
                eprint("\n💡 How to fix:")
                eprint(f"   1. Open your CSV file: {input_file}")
                eprint(f"   2. Search for '{value}' in the '{column}' column")
                eprint("   3. Correct or remove the invalid value")
                eprint("   4. Save the file and try again")
            else:
                eprint(f"\nError details: {error_str}")
        elif "Likely affected CSV line numbers" in error_str:
            # If we've already enhanced the error with line numbers, display it cleanly
            eprint(f"\n{error_str}")
        else:
            eprint(f"\nError: {error_str}")
            eprint("\n💡 General tips to fix CSV issues:")
            eprint("   • Ensure all cost columns contain valid numbers (or are empty)")
            eprint("   • Check date formats are consistent (e.g., YYYY-MM-DD)")
            eprint("   • Remove any special characters or extra commas")
            eprint("   • Verify the file is saved as UTF-8 encoded CSV")

        eprint("=" * 80 + "\n")
        sys.exit(1)
    except Exception as e:
        # Handle any other unexpected errors
        eprint(f"\n❌ Unexpected error reading CSV file: {e}")
        eprint("\n💡 If this issue persists, please check:")
        eprint(f"   • File exists and is readable: {input_file}")
        eprint("   • File is a valid CSV format")
        eprint("   • You have permission to read the file")
        sys.exit(1)

    # Report duplicates if found
    if duplicate_count > 0:
        print(f"\n⚠️ Found {duplicate_count} duplicate record(s):")
        for dup in duplicates_info[:10]:  # Show first 10 duplicates
            print(f"   Row {dup['current_row']} duplicates Row {dup['original_row']}: {dup['customer']} - {dup['run_id']} (${dup['cost']})")
        if len(duplicates_info) > 10:
            print(f"   ... and {len(duplicates_info) - 10} more duplicates")
        print("   ⚠️ Duplicates will be flagged and NOT sent for processing\n")

    # processing_anomalies is already set above with the simplified logic

    CHUNK_SIZE = 100
    if dry_run:
        print("\n⚠️ DRY RUN ONLY: Anomalies will be processed, but not actually sent to customers\n")
    else:
        print(f"✅ Updating {len(processing_anomalies)} unique anomalies in {ceil(len(processing_anomalies) / CHUNK_SIZE)} batches\n")

    batch_count = 1
    total_update_and_send_count = 0
    total_update_count = 0
    total_validation_errors = 0
    total_processing_errors = 0
    total_attempted = 0
    total_non_send_filtered = non_send_count
    total_duplicates_skipped = duplicate_count

    # For raw mode, collect all responses
    raw_responses = [] if raw else None

    # Initialize result processor to handle batch display
    result_processor = BatchResultProcessor()

    for chunk in chunks(processing_anomalies, CHUNK_SIZE):
        # Transform anomalies to v4 format before sending to API (drop unknown columns)
        transformed_chunk: list[dict] = []
        valid_pairs: list[tuple[dict, dict]] = []
        transform_errors: list[str] = []
        for idx, anomaly in enumerate(chunk):
            try:
                transformed = _transform_anomaly_for_v4(anomaly)
                transformed_chunk.append(transformed)
                valid_pairs.append((anomaly, transformed))
            except Exception as te:
                # Record transform error but continue with other rows
                rn = anomaly.get("_row_num", "?")
                transform_errors.append(f"Row {rn}: {te}")

        # Report local validation errors before hitting the API
        if transform_errors:
            eprint("\n⚠️ Local validation errors (these rows will be skipped in this batch):")
            for err in transform_errors[:10]:
                eprint(f"   - {err}")
            if len(transform_errors) > 10:
                eprint(f"   ... and {len(transform_errors) - 10} more")

        # Use V4 API for batch processing
        try:
            debug = arguments.get("--debug", False)
            response = update_anomalies_batch(anomalies=transformed_chunk, dry_run=dry_run, debug=debug, raw=raw)

            # If raw mode, collect the response and continue to next batch
            if raw:
                raw_responses.append(response)
                batch_count += 1
                continue

            # Process individual results from the API
            # Create positional mapping (safer than run_id since run_ids may not be unique)
            original_rows = _create_positional_mapping(valid_pairs)

            # Process batch results and get counts
            batch_processed, batch_sent, batch_invalid, batch_failed = result_processor.process_batch_results(
                response, original_rows, batch_count, dry_run, quiet
            )

            # Update totals
            total_update_and_send_count += batch_sent
            total_update_count += batch_processed
            total_validation_errors += batch_invalid
            total_processing_errors += batch_failed
            total_attempted += len(transformed_chunk)

            # Show batch summary
            status_parts = []
            if batch_processed > 0:
                status_parts.append(f"{batch_processed} processed")
            if batch_sent > 0:
                status_parts.append(f"{batch_sent} sent")
            if batch_invalid > 0:
                status_parts.append(f"{batch_invalid} invalid")
            if batch_failed > 0:
                status_parts.append(f"{batch_failed} failed")

            status_msg = ", ".join(status_parts) if status_parts else "No anomalies processed"

            # Display anomalies table for this batch if we have collected data
            result_processor.display_batch_results(batch_count, quiet, dry_run)

            # Add dry-run indicator to batch summary
            dry_run_prefix = "🔎 DRY-RUN " if dry_run else ""
            if response.success:
                print(f"{batch_count:02}: ✅ {dry_run_prefix}Batch completed: {status_msg}\n")
            else:
                print(f"{batch_count:02}: ⚠️  {dry_run_prefix}Batch completed with issues: {status_msg}\n")
                if response.error_summary:
                    eprint(f"   Error summary: {response.error_summary}")

                # Show additional details for failed anomalies if not in quiet mode
                if not quiet and response.results:
                    failed_anomalies = [r for r in response.results if r.status in ("invalid", "failed")]
                    if failed_anomalies and len(failed_anomalies) <= 10:  # Show details for up to 10 failures
                        eprint("\n   Failed anomalies:")
                        for i, result in enumerate(failed_anomalies):
                            # Use positional mapping instead of run_id lookup
                            orig_row = original_rows[i] if i < len(original_rows) else None
                            if orig_row:
                                name = _get_anomaly_name(orig_row)
                                status_icon = "⚠️" if result.status == "invalid" else "❌"
                                eprint(f"     {status_icon} {orig_row.get('customer_name', '')[:20]}: {result.error_message or 'No error message'}")

            # Add debug output for dry-run troubleshooting
            if dry_run and debug:
                print(f"\n[DEBUG] Raw API response for batch {batch_count}:")
                print(f"  Total received: {response.total_received}")
                print(f"  Successfully processed: {response.successfully_processed}")
                print(f"  Notifications sent: {response.notifications_sent}")
                print(f"  Validation errors: {response.validation_errors}")
                print(f"  Processing errors: {response.processing_errors}")
                print(f"  Individual results: {len(response.results)}")

        except Exception as e:
            eprint(f"\n❌ Error processing batch {batch_count}: {e}")
            eprint("The following anomalies were not transmitted:\n")
            for row in chunk:
                name = _get_anomaly_name(row)
                eprint(
                    f"{batch_count:02}: ❌ {row['cz_organization_id']} {row['customer_name']:25} ${row['anomaly_cost']:12}  {row['start_time']} {name}"
                )
            eprint(" ")

        batch_count += 1

    # If raw mode, output all collected responses and return
    if raw:
        for response in raw_responses:
            print(json.dumps(response, indent=2, cls=JSONSanity))
        return

    # Final summary
    print("\n" + "=" * 70)
    dry_run_title = "🔎 DRY-RUN " if dry_run else ""
    print(f"📦 {dry_run_title}PROCESSING SUMMARY")
    print("=" * 70)
    print(f"File processed: {input_file}")
    print(f"Total anomalies loaded: {len(anomalies)}")
    if total_non_send_filtered > 0:
        print(f"Not marked for sending: {total_non_send_filtered}")
    if total_duplicates_skipped > 0:
        print(f"Duplicates skipped: {total_duplicates_skipped}")
    print(f"Unique anomalies attempted: {total_attempted}")
    print(f"Successfully processed: {total_update_count}")
    print(f"Notifications sent: {total_update_and_send_count}")

    if total_validation_errors > 0 or total_processing_errors > 0:
        print(f"Validation errors: {total_validation_errors}")
        print(f"Processing errors: {total_processing_errors}")

    # Calculate success rate
    if total_attempted > 0:
        success_rate = (total_update_count / total_attempted) * 100
        print(f"Success rate: {success_rate:.1f}%")

    print("=" * 70)

    # Report any CSV validation corrections
    if validation_errors:
        print("\n📋 CSV Data Corrections:")
        correction_count = 0
        for row_num, errors in sorted(validation_errors.items()):
            for error in errors:
                if correction_count < 5:  # Show first 5 corrections
                    print(f"  Line {row_num}: {error['field']} value '{error['original_value']}' → {error['corrected_value']}")
                correction_count += 1

        if correction_count > 5:
            print(f"  ... and {correction_count - 5} more corrections")
        print(f"  Total: {correction_count} fields corrected across {len(validation_errors)} rows")

    # Result processor automatically cleans up its own data

    # Final status message
    if dry_run:
        print("\n⚠️  DRY RUN COMPLETED - No actual updates were made\n")
    elif total_validation_errors > 0 or total_processing_errors > 0:
        print("\n⚠️  Processing completed with some errors - Review the details above\n")
    elif total_update_count == 0:
        print("\n❌ No anomalies were successfully processed\n")
    else:
        print("\n✅ Anomaly processing completed successfully!\n")
