"""
Named range management for Google Sheets.

Provides CRUD operations for named ranges via the Sheets API.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple

from g_gremlin import google_client
from g_gremlin.a1 import parse_a1_range


@dataclass
class NamedRange:
    """Represents a named range in a spreadsheet."""
    name: str
    range_a1: str  # e.g., "'Sheet Name'!A1:B10"
    named_range_id: Optional[str] = None  # Google's internal ID


@dataclass
class NamedRangeResult:
    """Result of a named range operation."""
    success: bool
    created: int = 0
    updated: int = 0
    deleted: int = 0
    errors: List[str] = None

    def __post_init__(self):
        if self.errors is None:
            self.errors = []


def list_named_ranges(spreadsheet_id: str) -> List[NamedRange]:
    """
    List all named ranges in a spreadsheet.

    Args:
        spreadsheet_id: The spreadsheet ID

    Returns:
        List of NamedRange objects
    """
    service = google_client.get_sheets_service()

    # Fetch spreadsheet metadata including named ranges
    response = (
        service.spreadsheets()
        .get(
            spreadsheetId=spreadsheet_id,
            fields="namedRanges,sheets.properties.sheetId,sheets.properties.title",
        )
        .execute()
    )

    # Build sheet ID to name mapping
    sheet_map: Dict[int, str] = {}
    for sheet in response.get("sheets", []):
        props = sheet.get("properties", {})
        sheet_map[props.get("sheetId")] = props.get("title", "")

    # Parse named ranges
    ranges: List[NamedRange] = []
    for nr in response.get("namedRanges", []):
        name = nr.get("name", "")
        nr_id = nr.get("namedRangeId", "")
        grid_range = nr.get("range", {})

        # Convert GridRange to A1 notation
        range_a1 = _grid_range_to_a1(grid_range, sheet_map)

        ranges.append(NamedRange(
            name=name,
            range_a1=range_a1,
            named_range_id=nr_id,
        ))

    return sorted(ranges, key=lambda r: r.name)


def create_named_range(
    spreadsheet_id: str,
    name: str,
    range_a1: str,
) -> NamedRangeResult:
    """
    Create a new named range.

    Args:
        spreadsheet_id: The spreadsheet ID
        name: Name for the range (must be valid identifier)
        range_a1: A1 notation for the range (e.g., "'Sheet Name'!A1:B10")

    Returns:
        NamedRangeResult indicating success/failure
    """
    # Validate name
    if not _is_valid_range_name(name):
        return NamedRangeResult(
            success=False,
            errors=[f"Invalid range name '{name}'. Must start with letter/underscore and contain only letters, numbers, underscores."],
        )

    service = google_client.get_sheets_service()

    # Get sheet ID from range
    sheet_id, grid_range = _a1_to_grid_range(spreadsheet_id, range_a1, service)
    if sheet_id is None:
        return NamedRangeResult(
            success=False,
            errors=[f"Could not resolve sheet for range '{range_a1}'"],
        )

    # Build request
    request = {
        "addNamedRange": {
            "namedRange": {
                "name": name,
                "range": grid_range,
            }
        }
    }

    try:
        service.spreadsheets().batchUpdate(
            spreadsheetId=spreadsheet_id,
            body={"requests": [request]},
        ).execute()
        return NamedRangeResult(success=True, created=1)
    except Exception as e:
        return NamedRangeResult(success=False, errors=[str(e)])


def create_named_ranges_batch(
    spreadsheet_id: str,
    ranges: List[Tuple[str, str]],
) -> NamedRangeResult:
    """
    Create multiple named ranges in a single batch operation.

    Args:
        spreadsheet_id: The spreadsheet ID
        ranges: List of (name, range_a1) tuples

    Returns:
        NamedRangeResult with total created count and any errors
    """
    service = google_client.get_sheets_service()
    requests = []
    errors = []
    created = 0

    for name, range_a1 in ranges:
        # Validate name
        if not _is_valid_range_name(name):
            errors.append(f"Invalid range name '{name}'. Skipping.")
            continue

        # Get sheet ID from range
        try:
            sheet_id, grid_range = _a1_to_grid_range(spreadsheet_id, range_a1, service)
            if sheet_id is None:
                errors.append(f"Could not resolve sheet for range '{range_a1}' (name: {name}). Skipping.")
                continue

            # Build request
            requests.append({
                "addNamedRange": {
                    "namedRange": {
                        "name": name,
                        "range": grid_range,
                    }
                }
            })
        except Exception as e:
            errors.append(f"Error processing range '{name}': {e}")
            continue

    # Execute batch update if we have requests
    if requests:
        try:
            service.spreadsheets().batchUpdate(
                spreadsheetId=spreadsheet_id,
                body={"requests": requests},
            ).execute()
            created = len(requests)
        except Exception as e:
            errors.append(f"Batch update failed: {e}")
            return NamedRangeResult(success=False, errors=errors)

    success = created > 0 and len(errors) == 0
    return NamedRangeResult(success=success, created=created, errors=errors)


def delete_named_range(
    spreadsheet_id: str,
    name: str,
) -> NamedRangeResult:
    """
    Delete a named range by name.

    Args:
        spreadsheet_id: The spreadsheet ID
        name: Name of the range to delete

    Returns:
        NamedRangeResult indicating success/failure
    """
    # Find the named range ID
    existing = list_named_ranges(spreadsheet_id)
    target = next((r for r in existing if r.name == name), None)

    if not target:
        return NamedRangeResult(
            success=False,
            errors=[f"Named range '{name}' not found"],
        )

    service = google_client.get_sheets_service()

    request = {
        "deleteNamedRange": {
            "namedRangeId": target.named_range_id,
        }
    }

    try:
        service.spreadsheets().batchUpdate(
            spreadsheetId=spreadsheet_id,
            body={"requests": [request]},
        ).execute()
        return NamedRangeResult(success=True, deleted=1)
    except Exception as e:
        return NamedRangeResult(success=False, errors=[str(e)])


def rename_named_range(
    spreadsheet_id: str,
    old_name: str,
    new_name: str,
) -> NamedRangeResult:
    """
    Rename an existing named range.

    Args:
        spreadsheet_id: The spreadsheet ID
        old_name: Current name of the range
        new_name: New name for the range

    Returns:
        NamedRangeResult indicating success/failure
    """
    # Validate new name
    if not _is_valid_range_name(new_name):
        return NamedRangeResult(
            success=False,
            errors=[f"Invalid range name '{new_name}'. Must start with letter/underscore and contain only letters, numbers, underscores."],
        )

    # Find the named range
    existing = list_named_ranges(spreadsheet_id)
    target = next((r for r in existing if r.name == old_name), None)

    if not target:
        available = [r.name for r in existing[:10]]
        return NamedRangeResult(
            success=False,
            errors=[f"Named range '{old_name}' not found. Available: {', '.join(available)}..."],
        )

    # Check if new name already exists
    if any(r.name == new_name for r in existing):
        return NamedRangeResult(
            success=False,
            errors=[f"Named range '{new_name}' already exists."],
        )

    service = google_client.get_sheets_service()

    request = {
        "updateNamedRange": {
            "namedRange": {
                "namedRangeId": target.named_range_id,
                "name": new_name,
            },
            "fields": "name",
        }
    }

    try:
        service.spreadsheets().batchUpdate(
            spreadsheetId=spreadsheet_id,
            body={"requests": [request]},
        ).execute()
        return NamedRangeResult(success=True, updated=1)
    except Exception as e:
        return NamedRangeResult(success=False, errors=[str(e)])


def upsert_named_ranges(
    spreadsheet_id: str,
    ranges: Dict[str, str],
) -> NamedRangeResult:
    """
    Create or update multiple named ranges (batch operation).

    Uses upsert pattern: updates if exists, creates if not.

    Args:
        spreadsheet_id: The spreadsheet ID
        ranges: Dict of name -> A1 notation

    Returns:
        NamedRangeResult with counts of created/updated
    """
    if not ranges:
        return NamedRangeResult(success=True)

    service = google_client.get_sheets_service()

    # Get existing named ranges
    existing = list_named_ranges(spreadsheet_id)
    existing_map = {r.name: r for r in existing}

    # Build requests
    requests: List[Dict[str, Any]] = []
    created = 0
    updated = 0
    errors: List[str] = []

    for name, range_a1 in ranges.items():
        # Validate name
        if not _is_valid_range_name(name):
            errors.append(f"Invalid range name '{name}'")
            continue

        # Get sheet ID and grid range
        sheet_id, grid_range = _a1_to_grid_range(spreadsheet_id, range_a1, service)
        if sheet_id is None:
            errors.append(f"Could not resolve sheet for range '{range_a1}'")
            continue

        if name in existing_map:
            # Update existing
            requests.append({
                "updateNamedRange": {
                    "namedRange": {
                        "namedRangeId": existing_map[name].named_range_id,
                        "name": name,
                        "range": grid_range,
                    },
                    "fields": "range",
                }
            })
            updated += 1
        else:
            # Create new
            requests.append({
                "addNamedRange": {
                    "namedRange": {
                        "name": name,
                        "range": grid_range,
                    }
                }
            })
            created += 1

    if not requests:
        return NamedRangeResult(
            success=len(errors) == 0,
            errors=errors,
        )

    try:
        service.spreadsheets().batchUpdate(
            spreadsheetId=spreadsheet_id,
            body={"requests": requests},
        ).execute()
        return NamedRangeResult(
            success=True,
            created=created,
            updated=updated,
            errors=errors if errors else None,
        )
    except Exception as e:
        errors.append(str(e))
        return NamedRangeResult(success=False, errors=errors)


def _is_valid_range_name(name: str) -> bool:
    """Check if name is a valid named range identifier."""
    if not name:
        return False
    # Must start with letter or underscore, contain only alphanumeric and underscore
    return bool(re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", name))


def _grid_range_to_a1(grid_range: Dict[str, Any], sheet_map: Dict[int, str]) -> str:
    """Convert a GridRange to A1 notation."""
    sheet_id = grid_range.get("sheetId", 0)
    sheet_name = sheet_map.get(sheet_id, f"Sheet{sheet_id}")

    start_row = grid_range.get("startRowIndex", 0) + 1  # 0-indexed to 1-indexed
    end_row = grid_range.get("endRowIndex", start_row)  # exclusive
    start_col = grid_range.get("startColumnIndex", 0)
    end_col = grid_range.get("endColumnIndex", start_col + 1)  # exclusive

    start_col_letter = _col_index_to_letter(start_col)
    end_col_letter = _col_index_to_letter(end_col - 1)

    # Quote sheet name if needed
    if " " in sheet_name or "-" in sheet_name or "_" in sheet_name:
        sheet_name = f"'{sheet_name}'"

    # Single cell vs range
    if start_row == end_row - 1 and start_col == end_col - 1:
        return f"{sheet_name}!{start_col_letter}{start_row}"
    else:
        return f"{sheet_name}!{start_col_letter}{start_row}:{end_col_letter}{end_row - 1}"


def _a1_to_grid_range(
    spreadsheet_id: str,
    range_a1: str,
    service,
) -> Tuple[Optional[int], Optional[Dict[str, Any]]]:
    """
    Convert A1 notation to GridRange dict.

    Returns:
        Tuple of (sheet_id, grid_range_dict) or (None, None) if invalid
    """
    # Parse the A1 range
    try:
        parsed = parse_a1_range(range_a1)
    except Exception:
        # Invalid A1 notation
        return None, None

    # Get sheet ID
    sheet_name = parsed.sheet_name or "Sheet1"
    # Remove quotes if present
    sheet_name = sheet_name.strip("'")

    # Fetch sheet ID
    response = (
        service.spreadsheets()
        .get(
            spreadsheetId=spreadsheet_id,
            fields="sheets.properties.sheetId,sheets.properties.title",
        )
        .execute()
    )

    sheet_id = None
    for sheet in response.get("sheets", []):
        props = sheet.get("properties", {})
        if props.get("title") == sheet_name:
            sheet_id = props.get("sheetId")
            break

    if sheet_id is None:
        return None, None

    # Build GridRange
    grid_range: Dict[str, Any] = {"sheetId": sheet_id}

    if parsed.start_row:
        grid_range["startRowIndex"] = parsed.start_row - 1  # 0-indexed
    if parsed.end_row:
        grid_range["endRowIndex"] = parsed.end_row  # exclusive
    elif parsed.start_row:
        grid_range["endRowIndex"] = parsed.start_row  # single row

    if parsed.start_col:
        grid_range["startColumnIndex"] = parsed.start_col - 1  # 0-indexed
    if parsed.end_col:
        grid_range["endColumnIndex"] = parsed.end_col  # exclusive
    elif parsed.start_col:
        grid_range["endColumnIndex"] = parsed.start_col  # single col

    return sheet_id, grid_range


def _col_index_to_letter(index: int) -> str:
    """Convert 0-indexed column number to letter(s)."""
    result = ""
    index += 1  # Convert to 1-indexed
    while index > 0:
        index, remainder = divmod(index - 1, 26)
        result = chr(65 + remainder) + result
    return result


@dataclass
class ApplyNamedRangesResult:
    """Result of applying named ranges to formulas."""
    success: bool
    total_formulas_scanned: int = 0
    replacements_found: int = 0
    replacements_applied: int = 0
    replacements: List[Dict[str, Any]] = None
    errors: List[str] = None

    def __post_init__(self):
        if self.replacements is None:
            self.replacements = []
        if self.errors is None:
            self.errors = []


def apply_named_ranges_to_formulas(
    spreadsheet_id: str,
    dry_run: bool = True,
    sheet_name: Optional[str] = None,
) -> ApplyNamedRangesResult:
    """
    Scan formulas for raw cell references and replace with named ranges.

    This scans all formulas for raw cell references (e.g., B55) and replaces them
    with their defined Named Range (e.g., DRV_COACHING_RATE).

    Handles scope resolution: if B55 exists as a local named range on "Tab A"
    and a different one on "Tab B", we check the cell's sheet context.

    Args:
        spreadsheet_id: The spreadsheet ID
        dry_run: If True, only report what would be changed (no writes)
        sheet_name: Optional - only process this sheet

    Returns:
        ApplyNamedRangesResult with details of replacements
    """
    from g_gremlin.graph import parse_formula_references

    service = google_client.get_sheets_service()

    # Get all named ranges
    named_ranges = list_named_ranges(spreadsheet_id)

    if not named_ranges:
        return ApplyNamedRangesResult(
            success=True,
            errors=["No named ranges defined. Create some first with 'g-gremlin names create'."],
        )

    # Build lookup: cell A1 -> named range name
    # Handle both global and sheet-local scopes
    global_ranges: Dict[str, str] = {}  # cell -> name
    local_ranges: Dict[str, Dict[str, str]] = {}  # sheet -> {cell -> name}

    for nr in named_ranges:
        # Parse the range to get the cell(s)
        range_a1 = nr.range_a1
        if "!" in range_a1:
            sheet, cell_part = range_a1.split("!", 1)
            sheet = sheet.strip("'")

            # For single cells, add to lookup
            if ":" not in cell_part:
                # Local scope
                if sheet not in local_ranges:
                    local_ranges[sheet] = {}
                local_ranges[sheet][cell_part.upper()] = nr.name
            # Also add as global (for cross-sheet refs)
            global_ranges[f"{sheet}!{cell_part.upper()}"] = nr.name

    # Get all sheets
    response = (
        service.spreadsheets()
        .get(
            spreadsheetId=spreadsheet_id,
            fields="sheets.properties.sheetId,sheets.properties.title",
        )
        .execute()
    )

    sheets_to_process = []
    for sheet in response.get("sheets", []):
        title = sheet.get("properties", {}).get("title", "")
        if sheet_name is None or title == sheet_name:
            sheets_to_process.append(title)

    if not sheets_to_process:
        return ApplyNamedRangesResult(
            success=False,
            errors=[f"Sheet '{sheet_name}' not found."],
        )

    total_formulas = 0
    replacements: List[Dict[str, Any]] = []
    updates_to_apply: List[Dict[str, Any]] = []

    for current_sheet in sheets_to_process:
        range_spec = f"'{current_sheet}'!A1:ZZ10000"
        try:
            formulas_response = (
                service.spreadsheets()
                .values()
                .get(spreadsheetId=spreadsheet_id, range=range_spec, valueRenderOption="FORMULA")
                .execute()
            )
        except Exception:
            continue  # Skip sheets that fail

        formulas_grid = formulas_response.get("values", [])

        for r_idx, row in enumerate(formulas_grid):
            for c_idx, formula in enumerate(row):
                if not isinstance(formula, str) or not formula.startswith("="):
                    continue

                total_formulas += 1
                new_formula = formula
                made_replacement = False

                # Extract all cell references from the formula
                refs = parse_formula_references(formula)

                for ref in refs:
                    ref_upper = ref.upper()
                    named_range_name = None

                    # Check local scope first (same sheet)
                    if "!" not in ref_upper:
                        # Reference without sheet prefix - check current sheet's local ranges
                        local_map = local_ranges.get(current_sheet, {})
                        if ref_upper in local_map:
                            named_range_name = local_map[ref_upper]
                    else:
                        # Reference with sheet prefix - check global ranges
                        if ref_upper in global_ranges:
                            named_range_name = global_ranges[ref_upper]

                    if named_range_name:
                        # Replace the reference in the formula
                        # Need to be careful with regex to match whole cell refs
                        pattern = re.compile(
                            rf"(?<![A-Za-z0-9_])({re.escape(ref)})(?![A-Za-z0-9_])",
                            re.IGNORECASE,
                        )
                        new_formula_candidate = pattern.sub(named_range_name, new_formula)
                        if new_formula_candidate != new_formula:
                            new_formula = new_formula_candidate
                            made_replacement = True

                            col_letter = _col_index_to_letter(c_idx)
                            cell_a1 = f"{current_sheet}!{col_letter}{r_idx + 1}"
                            replacements.append({
                                "cell": cell_a1,
                                "old_ref": ref,
                                "new_ref": named_range_name,
                                "old_formula": formula,
                                "new_formula": new_formula,
                            })

                if made_replacement:
                    col_letter = _col_index_to_letter(c_idx)
                    cell_a1 = f"'{current_sheet}'!{col_letter}{r_idx + 1}"
                    updates_to_apply.append({
                        "range": cell_a1,
                        "values": [[new_formula]],
                    })

    if not dry_run and updates_to_apply:
        # Apply updates in batch
        try:
            batch_data = [
                {"range": u["range"], "values": u["values"]}
                for u in updates_to_apply
            ]
            service.spreadsheets().values().batchUpdate(
                spreadsheetId=spreadsheet_id,
                body={
                    "valueInputOption": "USER_ENTERED",
                    "data": batch_data,
                },
            ).execute()
        except Exception as e:
            return ApplyNamedRangesResult(
                success=False,
                total_formulas_scanned=total_formulas,
                replacements_found=len(replacements),
                replacements=replacements,
                errors=[f"Failed to apply updates: {e}"],
            )

    return ApplyNamedRangesResult(
        success=True,
        total_formulas_scanned=total_formulas,
        replacements_found=len(replacements),
        replacements_applied=len(updates_to_apply) if not dry_run else 0,
        replacements=replacements,
    )
