# Copyright (c) 2020 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 csv
import re
from datetime import datetime, timedelta
from decimal import Decimal, InvalidOperation

import polars as pl

from src.lib.anomalies import get_url_for_row
from src.utils.console import eprint
from src.utils.core import var2bool
from src.utils.time import utc_datetime_from_anything


def _count_header_comment_rows_in_file_object(csv_file_object):
    skip_rows = 0
    for line in csv_file_object.readlines():
        if line.decode()[0] != "#":
            break
        skip_rows += 1
    csv_file_object.seek(0)
    return skip_rows


def _read_csv_file(file_path):
    """
    Read a CSV file and return a DataFrame.
    Attempts to read the file with multiple encodings if the default fails.

    Args:
        file_path: Path to the CSV file

    Returns:
        Polars DataFrame

    Raises:
        FileNotFoundError: If the file doesn't exist
        ValueError: If the file is not a valid CSV with any supported encoding
    """
    # The file existence and permission checks are now done in check_input_file
    # Here we assume the file exists and is readable

    # List of encodings to try, in order of preference
    encodings = ["utf-8", "latin1", "iso-8859-1", "cp1252"]

    # Store errors for diagnostic purposes
    encoding_errors = []

    # Open the file
    csv_file_object = open(file_path, "rb")

    # Common schema overrides to prevent dtype inference errors on numeric-like columns
    schema_overrides = {
        "account_id": pl.String,
        # Treat cost fields as strings to avoid inference errors (user requested)
        "cost": pl.String,
        "Cost": pl.String,  # handle possible capitalized header
        "real_cost": pl.String,
        "anomaly_cost": pl.String,
    }

    # Helper to extract friendly context from a polars error message
    def _polars_error_hint(err_msg: str) -> str:
        import re as _re

        # Typical message: could not parse `95.39` as dtype `i64` at column 'cost' (column number 16)
        m = _re.search(r"could not parse `(.*?)` as dtype `[^`]+` at column '([^']+)'", err_msg)
        if not m:
            return ""
        bad_value, bad_column = m.group(1), m.group(2)

        # Try to find likely line numbers containing this value in the indicated column
        likely_lines = []
        try:
            # Count commented header rows to align with our reporting (header at line 1, first data row at 2)
            with open(file_path, "rb") as _f:
                _count_header_comment_rows_in_file_object(_f)  # Just count to skip comment lines
            # Open as text, ignoring undecodable bytes to best-effort scan
            with open(file_path, encoding="utf-8", errors="ignore") as fh:
                # Skip comment lines
                header = None
                for line in fh:
                    if line.startswith("#"):
                        continue
                    header = line.rstrip("\n")
                    break
                if header:
                    # Re-open to iterate from beginning and feed DictReader
                    fh.seek(0)

                    # Build an iterator that skips comment lines
                    def _iter_lines():
                        for ln in fh:
                            if ln.startswith("#"):
                                continue
                            yield ln

                    reader = csv.DictReader(_iter_lines())
                    # Data rows are reported as header line (1) + data row index (starting at 1) => offset 1
                    row_num = 2  # first data row per our convention
                    for r in reader:
                        try:
                            val = r.get(bad_column)
                        except Exception:
                            val = None
                        if val is not None and bad_value in str(val):
                            likely_lines.append(row_num)
                            if len(likely_lines) >= 5:
                                break
                        row_num += 1
        except Exception:
            pass

        hint = [
            f"Column: '{bad_column}' contains a value that doesn't match the inferred type.",
            f"Problematic value example: '{bad_value}'.",
        ]
        if likely_lines:
            hint.append(
                "Likely affected CSV line numbers (1-based, header is line 1): "
                + ", ".join(str(x) for x in likely_lines)
            )
        hint.append("Suggested fixes:")
        hint.append("- Clean or correct the values in that column for the listed rows, or")
        hint.append("- Specify explicit dtypes for the column (we now treat cost fields as strings).")
        return "\n".join(hint)

    try:
        # Count rows to skip (comment lines)
        skip_rows = _count_header_comment_rows_in_file_object(csv_file_object)
        csv_file_object.close()

        # First try the default method without specifying encoding
        try:
            df = pl.read_csv(
                file_path,
                skip_rows=skip_rows,
                schema_overrides=schema_overrides,
            )
            return df
        except Exception:
            # If default fails, try with specific encodings
            for encoding in encodings:
                try:
                    df = pl.read_csv(
                        file_path,
                        skip_rows=skip_rows,
                        schema_overrides=schema_overrides,
                        encoding=encoding,
                    )
                    print(f"Note: Used '{encoding}' encoding to read the CSV file")
                    return df
                except Exception as enc_error:
                    # Enhance the error with actionable hints when possible
                    hint = _polars_error_hint(str(enc_error))
                    if hint:
                        encoding_errors.append(f"{encoding}: {str(enc_error)}\n{hint}")
                    else:
                        encoding_errors.append(f"{encoding}: {str(enc_error)}")
                    continue

        # If we get here, all encodings failed
        error_details = "\n".join(encoding_errors)
        raise ValueError(
            f"The file '{file_path}' could not be read with any supported encoding.\nTried encodings: {', '.join(encodings)}\nErrors:\n{error_details}"
        ) from None

    except Exception as e:
        if csv_file_object and not csv_file_object.closed:
            csv_file_object.close()
        # Check if it's a CSV format error
        if "polars" in str(e) or "CSV" in str(e) or "codec" in str(e):
            raise ValueError(
                f"The file '{file_path}' is not a valid CSV file or has encoding issues.\nError: {str(e)}"
            ) from e
        raise


def validate_decimal(value, field_name=None, row_index=None, validation_errors=None):
    """
    Validate and convert a value to a decimal. Returns 0 if the value cannot be converted.
    Since cost fields are now read as strings from CSV, this handles string conversion.

    Args:
        value: The value to convert to a decimal (typically a string after CSV parsing)
        field_name: Name of the field being validated (for error reporting)
        row_index: Index of the row in the DataFrame (for error reporting)
        validation_errors: Dictionary to track validation errors

    Returns:
        The validated decimal value or 0 if invalid
    """

    # If validation tracking is enabled, record any original string that doesn't parse
    def record_error():
        if validation_errors is not None and field_name is not None and row_index is not None:
            if row_index not in validation_errors:
                validation_errors[row_index] = []
            validation_errors[row_index].append(
                {"field": field_name, "original_value": str(value), "corrected_value": 0}
            )

    if value is None:
        return 0

    # If it's already a number, just return it
    if isinstance(value, int | float | Decimal):
        return value

    # If it's a string, try to clean and convert it
    if isinstance(value, str):
        # Non-numeric looking strings should be flagged immediately
        if any(x in value.lower() for x in ["invalid", "nan", "null", "none"]) or not any(c.isdigit() for c in value):
            record_error()
            return 0

        # Remove any currency symbols, commas, spaces, etc.
        clean_value = re.sub(r"[^\d.-]", "", value.strip())

        # Handle empty string
        if not clean_value:
            record_error()
            return 0

        try:
            return Decimal(clean_value)
        except InvalidOperation:
            # Track the error if tracking is enabled
            record_error()
            return 0

    # Any other type, return 0
    record_error()
    return 0


def read_anomaly_csv_file_to_dict(file_path):
    """
    Read and process anomaly data from a CSV file.

    Args:
        file_path: Path to the CSV file containing anomaly data

    Returns:
        Tuple (dict, line count, count of lines removed, validation errors)

    Raises:
        ValueError: If the CSV file cannot be read or parsed
    """
    try:
        df = _read_csv_file(file_path)
    except ValueError as e:
        # Re-raise with additional context if needed
        error_msg = str(e)
        # Try to enhance the error message with more specific guidance
        if "could not parse" in error_msg.lower():
            raise ValueError(
                f"CSV parsing error: {error_msg}\n\n"
                "💡 Tips to fix this issue:\n"
                "1. Check that numeric columns contain only valid numbers\n"
                "2. Ensure date columns are in a recognizable format\n"
                "3. Remove any special characters from data values\n"
                "4. Verify the CSV file is properly formatted with consistent delimiters"
            ) from e
        raise

    len_before = len(df)

    # Remove blank lines from CSV - in polars, filter out rows where all values are null
    df = df.filter(~pl.all_horizontal(pl.all().is_null()))
    lines_removed = len_before - len(df)

    # Track validation errors to identify problematic data
    validation_errors = {}

    # Add row number column for tracking (1-based indexing, +1 for header)
    df = df.with_row_index("_row_num", offset=2)

    # Convert to list of dictionaries for processing (polars doesn't have apply like pandas)
    rows = df.to_dicts()

    # Process each row for validation and transformation
    processed_rows = []
    for row in rows:
        row_num = row["_row_num"]

        # Validate numeric fields
        numeric_fields = ["real_cost", "anomaly_cost", "cost"]
        for field in numeric_fields:
            if field in row and row[field] is not None:
                row[field] = validate_decimal(
                    row[field], field_name=field, row_index=row_num, validation_errors=validation_errors
                )

        # Convert start_time with error handling
        if "start_time" in row and row["start_time"] is not None:
            try:
                row["start_time"] = utc_datetime_from_anything(row["start_time"])
            except ValueError:
                # Keep original string if parsing fails
                pass

        # Calculate end_time
        if "start_time" in row and "anomaly_duration" in row:
            if isinstance(row["start_time"], datetime) and row["anomaly_duration"] is not None:
                try:
                    row["end_time"] = row["start_time"] + timedelta(hours=float(row["anomaly_duration"]))
                except (ValueError, TypeError):
                    row["end_time"] = row["start_time"]
            else:
                row["end_time"] = row["start_time"]

        # Convert send notification
        if "send?" in row:
            row["send_notification"] = var2bool(row["send?"])

        # Validate JSON
        if "view_filter_json" in row and row["view_filter_json"] is not None:
            json_str = row["view_filter_json"]

            # Already a dict, no need to validate
            if isinstance(json_str, dict):
                pass
            # Handle 'None' string without reporting as error (will be fixed in transformation)
            elif str(json_str).strip().lower() == "none" or str(json_str).strip() == "":
                row["view_filter_json"] = "{}"
            else:
                try:
                    # Try to parse as JSON to validate
                    import json

                    json.loads(str(json_str))
                except (ValueError, TypeError):
                    # Track the error only for actual invalid JSON, not 'None' values
                    if row_num not in validation_errors:
                        validation_errors[row_num] = []
                    validation_errors[row_num].append(
                        {
                            "field": "view_filter_json",
                            "original_value": str(json_str)[:100] + ("..." if len(str(json_str)) > 100 else ""),
                            "corrected_value": "{}",
                        }
                    )
                    row["view_filter_json"] = "{}"

        # Generate URL
        try:
            row["url"] = get_url_for_row(row)
        except Exception as e:
            eprint(f"Warning: Error generating URL for row {row_num}: {e}")
            row["url"] = None

        processed_rows.append(row)

    return processed_rows, len(processed_rows), lines_removed, validation_errors
