"""
Utilities for writing CSV data into Google Sheets tabs.
"""

from __future__ import annotations

import csv
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Tuple

from g_gremlin import google_client
from g_gremlin.a1 import col_index_to_letter, parse_a1_range
from g_gremlin.streaming import ChunkedWriter

WriteMode = Literal["overwrite", "append"]


@dataclass
class WriteResult:
    """
    Outcome of a write operation to a Sheet tab.
    """

    mode: WriteMode
    created_sheet: bool
    existing_rows_before: int
    rows_written: int
    total_rows_after: int
    backup_tab: Optional[str] = None


def write_csv_to_sheet(
    spreadsheet_id: str,
    tab_name: str,
    csv_path: Path,
    mode: WriteMode = "overwrite",
    backup: bool = False,
    formulas: bool = False,
    user_entered: bool = False,
    anchor: str = "A1",
    resume: bool = False,
    checkpoint_interval: int = 500,
    checkpoint_path: Optional[Path] = None,
    streaming: bool = False,
    clear_all: bool = False,
) -> WriteResult:
    """
    Write a CSV file into a Google Sheet tab, either overwriting or appending rows.

    In overwrite mode, by default only cell values are cleared (formatting and
    notes are preserved). Use clear_all=True to also clear formatting and notes.

    Raises:
        ValueError: if the CSV is empty or mode is invalid.
        HttpError: if Google Sheets operations fail.
    """
    normalized_mode: WriteMode = _normalize_mode(mode)
    start_row, start_col = _parse_anchor(anchor)
    rows = _read_csv(csv_path)
    if not rows:
        raise ValueError("CSV is empty; nothing to write.")
    if formulas:
        rows = _sanitize_formulas(rows)

    service = google_client.get_sheets_service()
    created, sheet_id = _ensure_sheet_exists(service, spreadsheet_id, tab_name)
    backup_tab: Optional[str] = None
    if backup and not created and sheet_id is not None:
        backup_tab = _create_backup_tab(service, spreadsheet_id, sheet_id, tab_name)
    existing_rows = _count_existing_rows(service, spreadsheet_id, tab_name)
    value_input_option = "USER_ENTERED" if (formulas or user_entered) else "RAW"

    streaming_enabled = streaming or resume
    if streaming_enabled:
        checkpoint_file = checkpoint_path or _default_checkpoint_path(spreadsheet_id, tab_name)
        writer = ChunkedWriter(checkpoint_file, chunk_size=max(1, checkpoint_interval))
        if not resume:
            writer.clear_checkpoint()

        rows_written, total_after = _streaming_write(
            service=service,
            spreadsheet_id=spreadsheet_id,
            tab_name=tab_name,
            rows=rows,
            mode=normalized_mode,
            value_input_option=value_input_option,
            start_row=start_row,
            start_col=start_col,
            existing_rows=existing_rows,
            writer=writer,
            resume=resume,
        )

        return WriteResult(
            mode=normalized_mode,
            created_sheet=created,
            existing_rows_before=existing_rows,
            rows_written=rows_written,
            total_rows_after=total_after,
            backup_tab=backup_tab,
        )

    if normalized_mode == "overwrite":
        rows_written, total_after = _overwrite_tab(
            service,
            spreadsheet_id,
            tab_name,
            rows,
            value_input_option,
            start_row=start_row,
            start_col=start_col,
            clear_all=clear_all,
        )
    else:
        rows_written, total_after = _append_tab(
            service,
            spreadsheet_id,
            tab_name,
            rows,
            existing_rows,
            value_input_option,
            start_row=start_row,
            start_col=start_col,
        )

    return WriteResult(
        mode=normalized_mode,
        created_sheet=created,
        existing_rows_before=existing_rows,
        rows_written=rows_written,
        total_rows_after=total_after,
        backup_tab=backup_tab,
    )


def _normalize_mode(mode: str) -> WriteMode:
    value = mode.lower().strip()
    if value not in {"overwrite", "append"}:
        raise ValueError(f"Unsupported mode '{mode}'. Use overwrite or append.")
    return value  # type: ignore[return-value]


def _parse_anchor(anchor: str) -> Tuple[int, int]:
    """
    Convert an anchor like 'A1' into 1-based row/col coordinates.
    """
    candidate = anchor.strip()
    if not candidate:
        raise ValueError("Anchor cannot be empty.")

    try:
        parsed = parse_a1_range(candidate if "!" in candidate else f"Sheet!{candidate}")
    except Exception as exc:
        raise ValueError(f"Invalid anchor '{anchor}': {exc}") from exc

    return parsed.start_row, parsed.start_col


def _read_csv(csv_path: Path) -> List[List[str]]:
    with csv_path.open("r", newline="", encoding="utf-8-sig") as handle:
        reader = csv.reader(handle)
        return [list(row) for row in reader]


def _ensure_sheet_exists(service, spreadsheet_id: str, tab_name: str) -> Tuple[bool, Optional[int]]:
    """
    Ensure the target tab exists; create if missing.

    Returns:
        Tuple[bool, Optional[int]]: (created, sheetId)
    """
    sheets = (
        service.spreadsheets()
        .get(spreadsheetId=spreadsheet_id, fields="sheets.properties.sheetId,sheets.properties.title")
        .execute()
        .get("sheets", [])
    )
    for sheet in sheets:
        props = sheet.get("properties", {}) or {}
        if props.get("title") == tab_name:
            return False, props.get("sheetId")

    add_request = {"addSheet": {"properties": {"title": tab_name}}}
    body = {"requests": [add_request]}
    resp = service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body=body).execute()
    replies = resp.get("replies", []) if isinstance(resp, dict) else []
    sheet_id = None
    if replies:
        props = replies[0].get("addSheet", {}).get("properties", {})
        sheet_id = props.get("sheetId")
    return True, sheet_id


def _count_existing_rows(service, spreadsheet_id: str, tab_name: str) -> int:
    range_ref = _tab_reference(tab_name)
    resp = (
        service.spreadsheets()
        .values()
        .get(spreadsheetId=spreadsheet_id, range=range_ref, majorDimension="ROWS", valueRenderOption="UNFORMATTED_VALUE")
        .execute()
    )
    values = resp.get("values", []) or []
    return len(values)


def _overwrite_tab(
    service,
    spreadsheet_id: str,
    tab_name: str,
    rows: List[List[str]],
    value_input_option: str,
    start_row: int,
    start_col: int,
    clear_all: bool = False,
) -> Tuple[int, int]:
    """
    Clear cell values in the target range and write all CSV rows.

    By default, only cell values are cleared (formatting and notes are preserved).
    When clear_all=True, also clears formatting and notes.
    """
    range_ref = _tab_reference(tab_name)
    col_count = max((len(r) for r in rows), default=0) or 1
    end_row = start_row + len(rows) - 1
    end_col = start_col + col_count - 1
    start_col_letter = col_index_to_letter(start_col)
    end_col_letter = col_index_to_letter(end_col)

    # Clear the range before writing
    clear_range = f"{range_ref}!{start_col_letter}{start_row}:{end_col_letter}{end_row}"

    if clear_all:
        # Clear values, formatting, and notes using batchUpdate
        # First, get the sheet ID
        metadata = service.spreadsheets().get(spreadsheetId=spreadsheet_id).execute()
        sheet_id = None
        for sheet in metadata.get("sheets", []):
            if sheet.get("properties", {}).get("title") == tab_name:
                sheet_id = sheet["properties"]["sheetId"]
                break

        if sheet_id is not None:
            # Use repeatCell with empty userEnteredValue to clear all content
            requests = [
                {
                    "updateCells": {
                        "range": {
                            "sheetId": sheet_id,
                            "startRowIndex": start_row - 1,  # 0-indexed
                            "endRowIndex": end_row,
                            "startColumnIndex": start_col - 1,
                            "endColumnIndex": end_col,
                        },
                        "fields": "userEnteredValue,userEnteredFormat,note",
                    }
                }
            ]
            service.spreadsheets().batchUpdate(
                spreadsheetId=spreadsheet_id, body={"requests": requests}
            ).execute()
        else:
            # Fallback to values-only clear if sheet ID not found
            service.spreadsheets().values().clear(
                spreadsheetId=spreadsheet_id, range=clear_range
            ).execute()
    else:
        # Default: clear values only (preserves formatting and notes)
        service.spreadsheets().values().clear(
            spreadsheetId=spreadsheet_id, range=clear_range
        ).execute()

    start_cell = f"{start_col_letter}{start_row}"
    service.spreadsheets().values().update(
        spreadsheetId=spreadsheet_id,
        range=f"{range_ref}!{start_cell}",
        valueInputOption=value_input_option,
        body={"values": rows},
    ).execute()
    return len(rows), end_row


def _append_tab(
    service,
    spreadsheet_id: str,
    tab_name: str,
    rows: List[List[str]],
    existing_rows: int,
    value_input_option: str,
    start_row: int,
    start_col: int,
) -> Tuple[int, int]:
    """
    Append CSV rows after the last non-empty row. If the tab is empty, writes all rows including header.
    """
    range_ref = _tab_reference(tab_name)
    if existing_rows == 0:
        start_col_letter = col_index_to_letter(start_col)
        start_cell = f"{start_col_letter}{start_row}"
        service.spreadsheets().values().update(
            spreadsheetId=spreadsheet_id,
            range=f"{range_ref}!{start_cell}",
            valueInputOption=value_input_option,
            body={"values": rows},
        ).execute()
        return len(rows), start_row + len(rows) - 1

    if len(rows) <= 1:
        # Only header present; nothing to append.
        return 0, existing_rows

    data_rows = rows[1:]
    service.spreadsheets().values().append(
        spreadsheetId=spreadsheet_id,
        range=range_ref,
        valueInputOption=value_input_option,
        insertDataOption="INSERT_ROWS",
        body={"values": data_rows},
    ).execute()
    return len(data_rows), existing_rows + len(data_rows)


def _tab_reference(tab_name: str) -> str:
    """
    Quote a tab name so it is safe in A1 references.
    """
    escaped = tab_name.replace("'", "''")
    return f"'{escaped}'"


def _create_backup_tab(service, spreadsheet_id: str, source_sheet_id: int, source_title: str) -> str:
    """
    Copy the source tab and rename it using copyTo before writing.
    """
    backup_title = f"{source_title}_backup_{_timestamp_suffix()}"
    copy_resp = (
        service.spreadsheets()
        .sheets()
        .copyTo(
            spreadsheetId=spreadsheet_id,
            sheetId=source_sheet_id,
            body={"destinationSpreadsheetId": spreadsheet_id},
        )
        .execute()
    )
    new_sheet_id = copy_resp.get("sheetId")
    if new_sheet_id is not None:
        rename_req = {
            "updateSheetProperties": {
                "properties": {"sheetId": new_sheet_id, "title": backup_title},
                "fields": "title",
            }
        }
        service.spreadsheets().batchUpdate(
            spreadsheetId=spreadsheet_id, body={"requests": [rename_req]}
        ).execute()
    return backup_title


def get_sheet_id_by_name(service, spreadsheet_id: str, tab_name: str) -> Optional[int]:
    """
    Find a sheet ID by tab title. Returns None if not found.
    """
    metadata = (
        service.spreadsheets()
        .get(spreadsheetId=spreadsheet_id, fields="sheets.properties.sheetId,sheets.properties.title")
        .execute()
    )
    for sheet in metadata.get("sheets", []) or []:
        props = sheet.get("properties", {}) or {}
        if props.get("title") == tab_name:
            return props.get("sheetId")
    return None


def rename_tab(service, spreadsheet_id: str, sheet_id: int, new_name: str) -> None:
    """
    Rename a tab by sheet ID.
    """
    request = {
        "updateSheetProperties": {
            "properties": {"sheetId": sheet_id, "title": new_name},
            "fields": "title",
        }
    }
    service.spreadsheets().batchUpdate(
        spreadsheetId=spreadsheet_id, body={"requests": [request]}
    ).execute()


def copy_tab(service, spreadsheet_id: str, source_sheet_id: int, dest_name: str) -> int:
    """
    Copy a tab within the same spreadsheet and rename it to dest_name.
    Returns the new sheet ID.
    """
    copy_resp = (
        service.spreadsheets()
        .sheets()
        .copyTo(
            spreadsheetId=spreadsheet_id,
            sheetId=source_sheet_id,
            body={"destinationSpreadsheetId": spreadsheet_id},
        )
        .execute()
    )
    new_sheet_id = copy_resp.get("sheetId")
    if new_sheet_id is not None:
        rename_tab(service, spreadsheet_id, new_sheet_id, dest_name)
    return int(new_sheet_id) if new_sheet_id is not None else -1


def sanitize_formula(formula: str) -> str:
    """
    Auto-quote sheet names that contain underscores, hyphens, or spaces.

    Examples:
        =My_Sheet!A1 -> ='My_Sheet'!A1
        =My Sheet!A1 -> ='My Sheet'!A1
        ='Already Quoted'!A1 -> ='Already Quoted'!A1 (unchanged)
        =Sheet1!A1 -> =Sheet1!A1 (unchanged, no special chars)
    """
    # Match unquoted sheet names (not preceded by single quote)
    # Include letters, numbers, underscores, hyphens, and spaces
    pattern = r"(?<!')([A-Za-z0-9_ -]+)!"

    def quote_if_needed(match: re.Match[str]) -> str:
        sheet_name = match.group(1)
        # Quote if contains underscore, hyphen, or space
        if "_" in sheet_name or "-" in sheet_name or " " in sheet_name:
            return f"'{sheet_name}'!"
        return match.group(0)

    return re.sub(pattern, quote_if_needed, formula)


def _sanitize_formulas(rows: List[List[str]]) -> List[List[str]]:
    """
    Process cells for formula handling:
    - Cells starting with '=' are treated as formulas and sanitized
    - Cells starting with '\\=' have the backslash removed (literal '=' escape)
    """
    sanitized: List[List[str]] = []
    for row in rows:
        new_row: List[str] = []
        for cell in row:
            if isinstance(cell, str):
                if cell.startswith("\\="):
                    # Escape sequence: \= becomes literal =
                    new_row.append(cell[1:])  # Remove the backslash
                elif cell.startswith("="):
                    new_row.append(sanitize_formula(cell))
                else:
                    new_row.append(cell)
            else:
                new_row.append(cell)
        sanitized.append(new_row)
    return sanitized


def get_all_sheet_names(spreadsheet_id: str) -> List[str]:
    """
    Return all tab names for a spreadsheet.
    """
    service = google_client.get_sheets_service()
    metadata = (
        service.spreadsheets()
        .get(spreadsheetId=spreadsheet_id, fields="sheets.properties.title")
        .execute()
    )
    return [s.get("properties", {}).get("title", "") for s in metadata.get("sheets", []) or []]


def apply_lint_fixes(spreadsheet_id: str, fixes: List[Dict[str, Any]], service=None) -> int:
    """
    Apply fixable lint changes (currently unquoted sheet refs).
    Returns number of fixes applied.
    """
    if not fixes:
        return 0
    service = service or google_client.get_sheets_service()
    data: List[Dict[str, Any]] = []
    for fix in fixes:
        fixed_formula = fix.get("fixed")
        cell = fix.get("cell")
        if not fixed_formula or not cell:
            continue
        data.append({"range": cell, "values": [[fixed_formula]]})

    if not data:
        return 0

    service.spreadsheets().values().batchUpdate(
        spreadsheetId=spreadsheet_id,
        body={"valueInputOption": "USER_ENTERED", "data": data},
    ).execute()
    return len(data)


def _timestamp_suffix() -> str:
    from datetime import datetime

    return datetime.utcnow().strftime("%Y%m%d_%H%M")


def _default_checkpoint_path(spreadsheet_id: str, tab_name: str) -> Path:
    safe_sheet = re.sub(r"[^A-Za-z0-9_.-]", "_", tab_name)
    safe_id = re.sub(r"[^A-Za-z0-9_.-]", "_", spreadsheet_id)
    return Path(".g_gremlin") / "checkpoints" / "sheets" / f"{safe_id}_{safe_sheet}.json"


def _streaming_write(
    service,
    spreadsheet_id: str,
    tab_name: str,
    rows: List[List[str]],
    mode: WriteMode,
    value_input_option: str,
    start_row: int,
    start_col: int,
    existing_rows: int,
    writer: ChunkedWriter,
    resume: bool,
) -> Tuple[int, int]:
    """
    Stream rows to a sheet with checkpoint support.
    Returns (rows_written_this_run, total_rows_after).
    """
    tab_ref = _tab_reference(tab_name)
    start_col_letter = col_index_to_letter(start_col)
    col_count = max((len(r) for r in rows), default=0) or 1

    checkpoint_state = writer.load_checkpoint() if resume else None
    skip_rows = checkpoint_state.completed_rows if checkpoint_state else 0

    if mode == "append" and existing_rows > 0:
        payload_rows = rows[1:] if len(rows) > 1 else []
    else:
        payload_rows = rows

    total_payload = len(payload_rows)
    already_written = skip_rows if resume else 0
    rows_written = max(total_payload - already_written, 0)

    # Clear target range on fresh overwrite runs
    if mode == "overwrite" or (mode == "append" and existing_rows == 0):
        if not resume or not checkpoint_state:
            end_row = start_row + len(rows) - 1
            end_col = start_col + col_count - 1
            start_letter = start_col_letter
            end_letter = col_index_to_letter(end_col)
            clear_range = f"{tab_ref}!{start_letter}{start_row}:{end_letter}{end_row}"
            service.spreadsheets().values().clear(spreadsheetId=spreadsheet_id, range=clear_range).execute()

        offset = start_row + (checkpoint_state.completed_rows if checkpoint_state else 0)

        def writer_func(batch: List[List[Any]]) -> None:
            nonlocal offset
            start_cell = f"{start_col_letter}{offset}"
            service.spreadsheets().values().update(
                spreadsheetId=spreadsheet_id,
                range=f"{tab_ref}!{start_cell}",
                valueInputOption=value_input_option,
                body={"values": batch},
            ).execute()
            offset += len(batch)

        if payload_rows:
            writer.write_chunks(iter(payload_rows), writer_func, resume=resume)

        total_after = start_row + len(rows) - 1
        return rows_written, total_after

    # Append mode with existing rows already present
    def writer_func_append(batch: List[List[Any]]) -> None:
        service.spreadsheets().values().append(
            spreadsheetId=spreadsheet_id,
            range=tab_ref,
            valueInputOption=value_input_option,
            insertDataOption="INSERT_ROWS",
            body={"values": batch},
        ).execute()

    if payload_rows:
        writer.write_chunks(iter(payload_rows), writer_func_append, resume=resume)

    total_after = existing_rows + rows_written
    return rows_written, total_after
