"""
CSV diff utilities for g-gremlin.
"""

from __future__ import annotations

import csv
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple

from g_gremlin.models import CellChange, DiffSummary, RowDiff, SheetDiff


# =============================================================================
# Cross-Tab Diff (compare same metrics across tabs within a spreadsheet)
# =============================================================================


@dataclass
class CrossTabRowDiff:
    """A row with differences between tabs."""

    key: str
    row_index: int
    tab1_value: Optional[str]
    tab2_value: Optional[str]
    delta: Optional[str] = None  # e.g., "+10%", "-3pp", "+$250K"


@dataclass
class CrossTabDiff:
    """Result of comparing two tabs within a spreadsheet."""

    tab1_name: str
    tab2_name: str
    key_column: str
    value_column: str
    rows: List[CrossTabRowDiff] = field(default_factory=list)
    rows_only_in_tab1: List[str] = field(default_factory=list)
    rows_only_in_tab2: List[str] = field(default_factory=list)


def compute_cross_tab_diff(
    tab1_data: List[List[Any]],
    tab2_data: List[List[Any]],
    tab1_name: str,
    tab2_name: str,
    key_column: str,
    value_column: str,
) -> CrossTabDiff:
    """
    Compare the same metric (column) across two tabs, aligned by key column.

    Args:
        tab1_data: 2D grid from first tab (including headers)
        tab2_data: 2D grid from second tab (including headers)
        tab1_name: Name of first tab
        tab2_name: Name of second tab
        key_column: Column letter or name to use as the row key (e.g., "A" or "Metric")
        value_column: Column letter or name to compare (e.g., "C" or "Value")

    Returns:
        CrossTabDiff with comparison results
    """
    # Parse headers and find column indices
    tab1_header = tab1_data[0] if tab1_data else []
    tab2_header = tab2_data[0] if tab2_data else []

    key_idx1 = _resolve_column_index(tab1_header, key_column)
    key_idx2 = _resolve_column_index(tab2_header, key_column)
    val_idx1 = _resolve_column_index(tab1_header, value_column)
    val_idx2 = _resolve_column_index(tab2_header, value_column)

    if key_idx1 is None:
        raise ValueError(f"Key column '{key_column}' not found in {tab1_name}")
    if key_idx2 is None:
        raise ValueError(f"Key column '{key_column}' not found in {tab2_name}")
    if val_idx1 is None:
        raise ValueError(f"Value column '{value_column}' not found in {tab1_name}")
    if val_idx2 is None:
        raise ValueError(f"Value column '{value_column}' not found in {tab2_name}")

    # Build key -> (row_index, value) maps
    tab1_map = _build_value_map(tab1_data[1:], key_idx1, val_idx1)
    tab2_map = _build_value_map(tab2_data[1:], key_idx2, val_idx2)

    # Find differences
    rows: List[CrossTabRowDiff] = []
    all_keys = set(tab1_map.keys()) | set(tab2_map.keys())

    for key in sorted(all_keys, key=lambda k: tab1_map.get(k, {}).get("row_index", 9999)):
        tab1_entry = tab1_map.get(key)
        tab2_entry = tab2_map.get(key)

        if tab1_entry and tab2_entry:
            val1 = tab1_entry["value"]
            val2 = tab2_entry["value"]
            delta = _compute_delta(val1, val2)

            rows.append(
                CrossTabRowDiff(
                    key=key,
                    row_index=tab1_entry["row_index"],
                    tab1_value=_format_display_value(val1),
                    tab2_value=_format_display_value(val2),
                    delta=delta,
                )
            )

    rows_only_in_tab1 = [k for k in tab1_map if k not in tab2_map]
    rows_only_in_tab2 = [k for k in tab2_map if k not in tab1_map]

    return CrossTabDiff(
        tab1_name=tab1_name,
        tab2_name=tab2_name,
        key_column=key_column,
        value_column=value_column,
        rows=rows,
        rows_only_in_tab1=rows_only_in_tab1,
        rows_only_in_tab2=rows_only_in_tab2,
    )


def format_cross_tab_table(diff: CrossTabDiff, show_all: bool = False) -> str:
    """
    Format cross-tab diff as a nice ASCII table.

    Args:
        diff: CrossTabDiff result
        show_all: If True, show all rows. If False, only show rows with differences.

    Returns:
        Formatted table string
    """
    lines = []
    lines.append(f"Cross-Tab Comparison: {diff.tab1_name} vs {diff.tab2_name}")
    lines.append(f"Column: {diff.value_column} | Key: {diff.key_column}")
    lines.append("")

    # Filter to only rows with differences (unless show_all)
    display_rows = diff.rows
    if not show_all:
        display_rows = [r for r in diff.rows if r.tab1_value != r.tab2_value]

    if not display_rows:
        lines.append("No differences found.")
        return "\n".join(lines)

    # Calculate column widths
    key_width = max(len("Metric"), max(len(r.key) for r in display_rows) if display_rows else 0)
    tab1_width = max(len(diff.tab1_name), max(len(r.tab1_value or "") for r in display_rows) if display_rows else 0)
    tab2_width = max(len(diff.tab2_name), max(len(r.tab2_value or "") for r in display_rows) if display_rows else 0)
    delta_width = max(len("Delta"), max(len(r.delta or "") for r in display_rows) if display_rows else 0)

    # Header
    header = f"{'Metric':<{key_width}} | {diff.tab1_name:<{tab1_width}} | {diff.tab2_name:<{tab2_width}} | {'Delta':<{delta_width}}"
    separator = "-" * len(header)
    lines.append(header)
    lines.append(separator)

    # Data rows
    for row in display_rows:
        val1 = row.tab1_value or ""
        val2 = row.tab2_value or ""
        delta = row.delta or ""
        lines.append(f"{row.key:<{key_width}} | {val1:<{tab1_width}} | {val2:<{tab2_width}} | {delta:<{delta_width}}")

    # Summary for rows only in one tab
    if diff.rows_only_in_tab1:
        lines.append("")
        lines.append(f"Rows only in {diff.tab1_name}: {', '.join(diff.rows_only_in_tab1[:5])}")
        if len(diff.rows_only_in_tab1) > 5:
            lines.append(f"  ... and {len(diff.rows_only_in_tab1) - 5} more")

    if diff.rows_only_in_tab2:
        lines.append("")
        lines.append(f"Rows only in {diff.tab2_name}: {', '.join(diff.rows_only_in_tab2[:5])}")
        if len(diff.rows_only_in_tab2) > 5:
            lines.append(f"  ... and {len(diff.rows_only_in_tab2) - 5} more")

    return "\n".join(lines)


def _resolve_column_index(header: List[Any], column: str) -> Optional[int]:
    """
    Resolve column reference to an index.

    Column can be:
    - A letter like "A", "B", "C"
    - A column name from the header
    """
    # Try as letter first (A=0, B=1, etc.)
    if len(column) <= 2 and column.isalpha():
        col_upper = column.upper()
        idx = 0
        for c in col_upper:
            idx = idx * 26 + (ord(c) - ord("A") + 1)
        idx -= 1  # 0-based
        if idx < len(header):
            return idx

    # Try as column name
    col_lower = column.lower()
    for idx, name in enumerate(header):
        if str(name).lower() == col_lower:
            return idx

    return None


def _build_value_map(
    rows: List[List[Any]], key_idx: int, val_idx: int
) -> Dict[str, Dict[str, Any]]:
    """Build a map from key -> {row_index, value}."""
    result: Dict[str, Dict[str, Any]] = {}
    for row_idx, row in enumerate(rows):
        if key_idx >= len(row):
            continue
        key = str(row[key_idx]).strip()
        if not key:
            continue
        value = row[val_idx] if val_idx < len(row) else None
        result[key] = {"row_index": row_idx, "value": value}
    return result


def _compute_delta(val1: Any, val2: Any) -> Optional[str]:
    """
    Compute a human-readable delta between two values.

    Handles:
    - Percentages: +3pp / -3pp
    - Currencies: +$250K
    - Numbers: +10% / -5%
    """
    num1 = _parse_number(val1)
    num2 = _parse_number(val2)

    if num1 is None or num2 is None:
        # Non-numeric - just show if different
        if str(val1) != str(val2):
            return "changed"
        return None

    if num1 == num2:
        return None

    is_pct1 = _is_percentage(val1)
    is_pct2 = _is_percentage(val2)

    # Both percentages - show percentage point difference
    if is_pct1 and is_pct2:
        diff = num2 - num1
        sign = "+" if diff > 0 else ""
        return f"{sign}{diff:.0f}pp"

    # Regular numbers - show percentage change
    if num1 == 0:
        return "∞" if num2 > 0 else "-∞"

    pct_change = ((num2 - num1) / abs(num1)) * 100
    sign = "+" if pct_change > 0 else ""

    if abs(pct_change) >= 1:
        return f"{sign}{pct_change:.0f}%"
    else:
        return f"{sign}{pct_change:.1f}%"


def _parse_number(val: Any) -> Optional[float]:
    """Parse a value as a number, stripping currency symbols and commas."""
    if val is None:
        return None
    if isinstance(val, (int, float)):
        return float(val)

    s = str(val).strip()
    # Remove currency symbols and commas
    s = s.replace("$", "").replace(",", "").replace("%", "").strip()

    try:
        return float(s)
    except ValueError:
        return None


def _is_percentage(val: Any) -> bool:
    """Check if a value looks like a percentage."""
    if val is None:
        return False
    s = str(val).strip()
    return s.endswith("%") or (isinstance(val, (int, float)) and 0 <= val <= 1)


def _format_display_value(val: Any) -> str:
    """Format a value for display."""
    if val is None:
        return ""
    if isinstance(val, float):
        # Check if it looks like a percentage (0-1)
        if 0 <= val <= 1:
            return f"{val*100:.0f}%"
        # Format large numbers
        if abs(val) >= 1_000_000:
            return f"${val/1_000_000:.1f}M"
        if abs(val) >= 1_000:
            return f"${val/1_000:.0f}K"
        return f"{val:,.0f}"
    return str(val)


# =============================================================================
# CSV Diff (compare two CSV files)
# =============================================================================


def compute_csv_diff(
    original_path: str,
    modified_path: str,
    key_col: Optional[str] = None,
) -> SheetDiff:
    """
    Compute a cell-level diff between two CSV files.
    """
    orig_header, orig_rows = _read_csv(Path(original_path))
    mod_header, mod_rows = _read_csv(Path(modified_path))

    key_info = _resolve_key_column(orig_header, mod_header, key_col)
    merged_header = _merge_headers(orig_header, mod_header)
    orig_col_index = {name: idx for idx, name in enumerate(orig_header)}
    mod_col_index = {name: idx for idx, name in enumerate(mod_header)}

    orig_key_idx = key_info[1] if key_info else None
    mod_key_idx = key_info[2] if key_info else None
    key_name = key_info[0] if key_info else None

    orig_map = _build_row_map(orig_rows, orig_key_idx, key_name)
    mod_map = _build_row_map(mod_rows, mod_key_idx, key_name)

    added_keys = [k for k in mod_map if k not in orig_map]
    removed_keys = [k for k in orig_map if k not in mod_map]

    row_diffs: List[RowDiff] = []
    changed_cells = 0

    for key, orig_entry in orig_map.items():
        if key not in mod_map:
            continue

        mod_entry = mod_map[key]
        changes: List[CellChange] = []

        for col_idx, col_name in enumerate(merged_header):
            old_val = _get_cell(orig_entry["values"], orig_col_index, col_name)
            new_val = _get_cell(mod_entry["values"], mod_col_index, col_name)
            if old_val == new_val:
                continue
            changes.append(
                CellChange(
                    row_key=key,
                    row_index=orig_entry["row_index"],
                    column_name=col_name,
                    column_index=col_idx,
                    old=old_val,
                    new=new_val,
                )
            )

        if changes:
            changed_cells += len(changes)
            row_diffs.append(
                RowDiff(row_key=key, row_index=orig_entry["row_index"], changes=changes)
            )

    summary = DiffSummary(
        added_rows=len(added_keys),
        removed_rows=len(removed_keys),
        changed_rows=len(row_diffs),
        changed_cells=changed_cells,
    )

    return SheetDiff(
        summary=summary,
        row_diffs=row_diffs,
        added_keys=added_keys,
        removed_keys=removed_keys,
    )


def _read_csv(path: Path) -> Tuple[List[str], List[List[str]]]:
    with path.open(newline="", encoding="utf-8-sig") as csv_file:
        reader = csv.reader(csv_file)
        rows = list(reader)

    if not rows:
        return [], []

    header = rows[0]
    data_rows = [_normalize_row(row, len(header)) for row in rows[1:]]
    return header, data_rows


def _normalize_row(row: Sequence[str], length: int) -> List[str]:
    if len(row) < length:
        return list(row) + [""] * (length - len(row))
    return list(row)


def _resolve_key_column(
    orig_header: List[str],
    mod_header: List[str],
    requested: Optional[str],
) -> Optional[Tuple[str, int, int]]:
    """
    Determine which column to use as the row key.
    """
    if requested:
        idx = _find_column(orig_header, requested)
        if idx is None:
            raise ValueError(f"Key column '{requested}' not found in original file.")
        mod_idx = _find_column(mod_header, requested)
        if mod_idx is None:
            raise ValueError(f"Key column '{requested}' not found in modified file.")
        return orig_header[idx], idx, mod_idx

    inferred_idx = _infer_id_column(orig_header)
    if inferred_idx is not None:
        col_name = orig_header[inferred_idx]
        mod_idx = _find_column(mod_header, col_name)
        if mod_idx is not None:
            return col_name, inferred_idx, mod_idx

    return None


def _find_column(header: List[str], name: str) -> Optional[int]:
    target = name.lower()
    for idx, col in enumerate(header):
        if col.lower() == target:
            return idx
    return None


def _infer_id_column(header: List[str]) -> Optional[int]:
    for idx, col in enumerate(header):
        if col.lower() == "id":
            return idx
    return None


def _build_row_map(
    rows: List[List[str]],
    key_idx: Optional[int],
    key_name: Optional[str],
) -> Dict[str, Dict[str, object]]:
    row_map: Dict[str, Dict[str, object]] = {}
    for idx, row in enumerate(rows):
        row_index = idx  # data row index, excluding header
        if key_idx is not None:
            if key_idx >= len(row):
                raise ValueError(
                    f"Row {row_index + 1} missing key column "
                    f"'{key_name or key_idx}'."
                )
            key = str(row[key_idx])
        else:
            key = str(row_index + 1)  # 1-based for readability

        if key in row_map:
            raise ValueError(f"Duplicate row key '{key}' encountered.")

        row_map[key] = {"row_index": row_index, "values": row}
    return row_map


def _merge_headers(orig_header: List[str], mod_header: List[str]) -> List[str]:
    merged = list(orig_header)
    for col in mod_header:
        if col not in merged:
            merged.append(col)
    return merged


def _get_cell(
    row_values: List[str],
    col_index_map: Dict[str, int],
    col_name: str,
) -> Optional[str]:
    if col_name not in col_index_map:
        return None
    idx = col_index_map[col_name]
    if idx >= len(row_values):
        return ""
    return row_values[idx]
