"""
Snapshot management for Google Sheets - the "Time Machine" feature.

Provides save/restore functionality for spreadsheet state with safety checks.
"""

from __future__ import annotations

import getpass
import gzip
import json
import sys
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from g_gremlin import google_client
from g_gremlin.paths import gremlin_path
from g_gremlin.sheets_io import get_all_sheet_names


# Default snapshot directory
SNAPSHOTS_DIR = Path(".g_gremlin/snapshots")


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


def _redact_argv(argv: List[str]) -> List[str]:
    """Redact sensitive values from command line arguments."""
    redacted = []
    skip_next = False
    # Flags that contain secrets - expanded for enterprise security
    sensitive_flags = {
        "--token", "--secret", "--password", "--api-key", "--access-token",
        "--credentials", "--refresh-token", "--client-secret", "--api-token",
        "--bearer", "--authorization", "--webhook", "--apikey", "--auth-token",
    }

    for i, arg in enumerate(argv):
        if skip_next:
            redacted.append("[REDACTED]")
            skip_next = False
            continue

        # Check if this is a sensitive flag
        if arg.lower() in sensitive_flags:
            redacted.append(arg)
            skip_next = True
        elif "=" in arg:
            key, _ = arg.split("=", 1)
            if key.lower() in sensitive_flags or any(s in key.lower() for s in ("token", "secret", "password", "key")):
                redacted.append(f"{key}=[REDACTED]")
            else:
                redacted.append(arg)
        else:
            redacted.append(arg)

    return redacted


def _get_caller_email() -> Optional[str]:
    """Try to get caller email from cached OAuth token."""
    try:
        token_path = gremlin_path("token.json")
        if token_path.exists():
            token_data = json.loads(token_path.read_text(encoding="utf-8"))
            # Email might be stored in various places depending on OAuth flow
            return token_data.get("email") or token_data.get("id_token_claims", {}).get("email")
    except Exception:
        pass
    return None


def collect_command_metadata() -> Dict[str, Any]:
    """
    Collect command metadata with token redaction.

    Returns dict with:
    - command_argv: Redacted sys.argv
    - command_string: Redacted command string
    - caller_user: Current username
    - caller_email: Email from OAuth token if available
    - correlation_id: Unique ID for this operation
    """
    argv = _redact_argv(sys.argv)
    return {
        "command_argv": argv,
        "command_string": " ".join(argv),
        "caller_user": getpass.getuser(),
        "caller_email": _get_caller_email(),
        "correlation_id": str(uuid.uuid4()),
    }


@dataclass
class TabSnapshot:
    """Snapshot of a single tab's data."""
    name: str
    values_grid: List[List[Any]]
    formulas_grid: List[List[Any]]
    row_count: int = 0
    col_count: int = 0


@dataclass
class SnapshotMetadata:
    """Metadata about a snapshot."""
    name: str
    created_at: str
    spreadsheet_id: str
    concurrency_token: str
    tab_count: int
    total_cells: int
    file_path: Optional[str] = None
    # Phase 1 additions for backup/rollback
    shape: Optional[Dict[str, Dict[str, int]]] = None  # {"Tab1": {"rows": 100, "cols": 15}}
    command_string: Optional[str] = None  # Full command (tokens redacted)
    command_argv: Optional[List[str]] = None  # sys.argv (tokens redacted)
    correlation_id: Optional[str] = None  # Links to AgenticResult
    caller_user: Optional[str] = None  # getpass.getuser()
    caller_email: Optional[str] = None  # From OAuth token if available
    tabs: Optional[List[str]] = None  # List of tab names in snapshot


@dataclass
class Snapshot:
    """Complete spreadsheet snapshot."""
    version: str
    name: str
    created_at: str
    spreadsheet_id: str
    concurrency_token: str
    tabs: List[TabSnapshot]
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class SnapshotResult:
    """Result of a snapshot operation."""
    success: bool
    message: str = ""
    snapshot_path: Optional[Path] = None
    errors: List[str] = field(default_factory=list)


@dataclass
class RestoreResult:
    """Result of a restore operation."""
    success: bool
    message: str = ""
    tabs_restored: int = 0
    cells_restored: int = 0
    backup_created: Optional[str] = None
    errors: List[str] = field(default_factory=list)


@dataclass
class SnapshotDiff:
    """Diff between current state and a snapshot."""
    tabs_added: List[str]
    tabs_removed: List[str]
    tabs_modified: List[Dict[str, Any]]
    total_cell_changes: int = 0


@dataclass
class ShapeDrift:
    """Represents a shape change between snapshot and current state."""
    tab: str
    expected_rows: int
    expected_cols: int
    actual_rows: int
    actual_cols: int

    @property
    def has_drift(self) -> bool:
        """Any row or column change counts as drift."""
        return self.expected_rows != self.actual_rows or self.expected_cols != self.actual_cols

    @property
    def row_delta(self) -> int:
        """Positive = rows added, negative = rows removed."""
        return self.actual_rows - self.expected_rows

    @property
    def col_delta(self) -> int:
        """Positive = cols added, negative = cols removed."""
        return self.actual_cols - self.expected_cols

    def __str__(self) -> str:
        parts = []
        if self.row_delta != 0:
            parts.append(f"rows: {self.expected_rows}→{self.actual_rows} ({self.row_delta:+d})")
        if self.col_delta != 0:
            parts.append(f"cols: {self.expected_cols}→{self.actual_cols} ({self.col_delta:+d})")
        return f"{self.tab}: {', '.join(parts)}" if parts else f"{self.tab}: no drift"


def check_shape_drift(
    spreadsheet_id: str,
    snapshot: Snapshot,
) -> List[ShapeDrift]:
    """
    Check for shape drift between current spreadsheet and a snapshot.

    Any row or column change counts as drift (threshold: >0 change).

    Args:
        spreadsheet_id: The spreadsheet ID to check
        snapshot: The snapshot to compare against

    Returns:
        List of ShapeDrift objects for tabs with changes. Empty list = no drift.
    """
    service = google_client.get_sheets_service()
    drifts: List[ShapeDrift] = []

    for tab in snapshot.tabs:
        # Get current shape for this tab
        range_spec = f"'{tab.name}'"
        try:
            response = (
                service.spreadsheets()
                .values()
                .get(
                    spreadsheetId=spreadsheet_id,
                    range=range_spec,
                    valueRenderOption="UNFORMATTED_VALUE",
                )
                .execute()
            )
            current_values = response.get("values", [])
            actual_rows = len(current_values)
            actual_cols = max((len(r) for r in current_values), default=0)
        except Exception:
            # Tab might have been deleted - treat as full drift
            actual_rows = 0
            actual_cols = 0

        drift = ShapeDrift(
            tab=tab.name,
            expected_rows=tab.row_count,
            expected_cols=tab.col_count,
            actual_rows=actual_rows,
            actual_cols=actual_cols,
        )

        if drift.has_drift:
            drifts.append(drift)

    return drifts


def get_sheet_shape(spreadsheet_id: str, tab_name: str) -> Tuple[int, int]:
    """
    Get the current row and column count for a tab.

    Args:
        spreadsheet_id: The spreadsheet ID
        tab_name: The tab name

    Returns:
        Tuple of (row_count, col_count)
    """
    service = google_client.get_sheets_service()
    range_spec = f"'{tab_name}'"

    response = (
        service.spreadsheets()
        .values()
        .get(
            spreadsheetId=spreadsheet_id,
            range=range_spec,
            valueRenderOption="UNFORMATTED_VALUE",
        )
        .execute()
    )
    values = response.get("values", [])
    row_count = len(values)
    col_count = max((len(r) for r in values), default=0)
    return row_count, col_count


def create_snapshot(
    spreadsheet_id: str,
    name: str,
    tabs: Optional[List[str]] = None,
    output_dir: Optional[Path] = None,
    metadata: Optional[Dict[str, Any]] = None,
) -> SnapshotResult:
    """
    Create a snapshot of a spreadsheet.

    Args:
        spreadsheet_id: The spreadsheet ID
        name: Name for the snapshot (used in filename)
        tabs: Optional list of tab names to snapshot (None = all tabs)
        output_dir: Optional output directory (default: .g_gremlin/snapshots/)
        metadata: Optional command metadata from collect_command_metadata()

    Returns:
        SnapshotResult with path to created snapshot
    """
    service = google_client.get_sheets_service()

    # Get sheet metadata including revision ID
    try:
        metadata = google_client.get_sheet_metadata(spreadsheet_id)
        concurrency_token = metadata.get("headRevisionId") or metadata.get("modifiedTime", "")
    except Exception as e:
        return SnapshotResult(
            success=False,
            errors=[f"Failed to get sheet metadata: {e}"],
        )

    # Get all sheet names
    try:
        all_sheets = get_all_sheet_names(spreadsheet_id)
    except Exception as e:
        return SnapshotResult(
            success=False,
            errors=[f"Failed to get sheet names: {e}"],
        )

    # Determine which tabs to snapshot
    tabs_to_snapshot = tabs if tabs else all_sheets

    # Validate requested tabs exist
    missing_tabs = set(tabs_to_snapshot) - set(all_sheets)
    if missing_tabs:
        return SnapshotResult(
            success=False,
            errors=[f"Tabs not found: {', '.join(missing_tabs)}"],
        )

    # Snapshot each tab
    tab_snapshots: List[TabSnapshot] = []
    total_cells = 0

    for tab_name in tabs_to_snapshot:
        range_spec = f"'{tab_name}'!A1:ZZ10000"

        try:
            # Fetch values
            values_response = (
                service.spreadsheets()
                .values()
                .get(
                    spreadsheetId=spreadsheet_id,
                    range=range_spec,
                    valueRenderOption="UNFORMATTED_VALUE",
                )
                .execute()
            )
            values_grid = values_response.get("values", [])

            # Fetch formulas
            formulas_response = (
                service.spreadsheets()
                .values()
                .get(
                    spreadsheetId=spreadsheet_id,
                    range=range_spec,
                    valueRenderOption="FORMULA",
                )
                .execute()
            )
            formulas_grid = formulas_response.get("values", [])

        except Exception as e:
            return SnapshotResult(
                success=False,
                errors=[f"Failed to fetch tab '{tab_name}': {e}"],
            )

        row_count = max(len(values_grid), len(formulas_grid))
        col_count = max(
            (max(len(r) for r in values_grid) if values_grid else 0),
            (max(len(r) for r in formulas_grid) if formulas_grid else 0),
        )

        tab_snapshots.append(TabSnapshot(
            name=tab_name,
            values_grid=values_grid,
            formulas_grid=formulas_grid,
            row_count=row_count,
            col_count=col_count,
        ))

        total_cells += row_count * col_count

    # Create snapshot object
    created_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
    snapshot = Snapshot(
        version="1.0",
        name=name,
        created_at=created_at,
        spreadsheet_id=spreadsheet_id,
        concurrency_token=concurrency_token,
        tabs=tab_snapshots,
        metadata={
            "spreadsheet_title": metadata.get("name", ""),
            "tab_count": len(tab_snapshots),
            "total_cells": total_cells,
        },
    )

    # Serialize to JSON
    snapshot_dict = {
        "version": snapshot.version,
        "name": snapshot.name,
        "created_at": snapshot.created_at,
        "spreadsheet_id": snapshot.spreadsheet_id,
        "concurrency_token": snapshot.concurrency_token,
        "tabs": [
            {
                "name": t.name,
                "values_grid": t.values_grid,
                "formulas_grid": t.formulas_grid,
                "row_count": t.row_count,
                "col_count": t.col_count,
            }
            for t in snapshot.tabs
        ],
        "metadata": snapshot.metadata,
    }

    # Determine output path
    output_dir = output_dir or SNAPSHOTS_DIR
    output_dir.mkdir(parents=True, exist_ok=True)

    # Create filename with timestamp
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    safe_name = name.replace(" ", "_").replace("/", "_")
    filename = f"{safe_name}_{timestamp}.json.gz"
    output_path = output_dir / filename

    # Write compressed snapshot
    try:
        with gzip.open(output_path, "wt", encoding="utf-8") as f:
            json.dump(snapshot_dict, f, indent=2)
    except Exception as e:
        return SnapshotResult(
            success=False,
            errors=[f"Failed to write snapshot: {e}"],
        )

    # Build shape dictionary for index
    shape_dict = {
        t.name: {"rows": t.row_count, "cols": t.col_count}
        for t in tab_snapshots
    }
    tab_names = [t.name for t in tab_snapshots]

    # Update index with enhanced metadata
    _update_index(
        snapshots_dir=output_dir,
        name=name,
        path=output_path,
        created_at=created_at,
        spreadsheet_id=spreadsheet_id,
        concurrency_token=concurrency_token,
        shape=shape_dict,
        tabs=tab_names,
        metadata=metadata,
    )

    return SnapshotResult(
        success=True,
        message=f"Snapshot created: {len(tab_snapshots)} tabs, {total_cells} cells",
        snapshot_path=output_path,
    )


def list_snapshots(
    spreadsheet_id: Optional[str] = None,
    snapshots_dir: Optional[Path] = None,
) -> List[SnapshotMetadata]:
    """
    List available snapshots.

    Args:
        spreadsheet_id: Optional filter by spreadsheet ID
        snapshots_dir: Directory to search (default: .g_gremlin/snapshots/)

    Returns:
        List of SnapshotMetadata objects
    """
    snapshots_dir = snapshots_dir or SNAPSHOTS_DIR

    if not snapshots_dir.exists():
        return []

    snapshots: List[SnapshotMetadata] = []

    # First try to load from index for faster metadata access
    index_path = snapshots_dir / "index.json"
    index_data: Dict[str, Any] = {}
    if index_path.exists():
        try:
            with index_path.open("r", encoding="utf-8") as f:
                index_data = json.load(f).get("snapshots", {})
        except Exception:
            pass

    # Look for .json.gz files
    for path in sorted(snapshots_dir.glob("*.json.gz"), reverse=True):
        try:
            with gzip.open(path, "rt", encoding="utf-8") as f:
                data = json.load(f)

            # Filter by spreadsheet ID if specified
            if spreadsheet_id and data.get("spreadsheet_id") != spreadsheet_id:
                continue

            meta = data.get("metadata", {})
            snapshot_name = data.get("name", path.stem)

            # Try to get enhanced metadata from index
            index_entry = index_data.get(snapshot_name, {})
            index_meta = index_entry.get("metadata", {})

            # Build shape from tabs if not in index
            shape = index_entry.get("shape")
            if not shape and "tabs" in data:
                shape = {
                    t["name"]: {"rows": t.get("row_count", 0), "cols": t.get("col_count", 0)}
                    for t in data.get("tabs", [])
                }

            # Get tab names from index or data
            tab_names = index_entry.get("tabs")
            if not tab_names and "tabs" in data:
                tab_names = [t["name"] for t in data.get("tabs", [])]

            snapshots.append(SnapshotMetadata(
                name=snapshot_name,
                created_at=data.get("created_at", ""),
                spreadsheet_id=data.get("spreadsheet_id", ""),
                concurrency_token=data.get("concurrency_token", ""),
                tab_count=meta.get("tab_count", 0),
                total_cells=meta.get("total_cells", 0),
                file_path=str(path),
                # New fields
                shape=shape,
                command_string=index_meta.get("command_string"),
                command_argv=index_meta.get("command_argv"),
                correlation_id=index_meta.get("correlation_id"),
                caller_user=index_meta.get("caller_user"),
                caller_email=index_meta.get("caller_email"),
                tabs=tab_names,
            ))
        except Exception:
            continue  # Skip corrupted files

    return snapshots


def load_snapshot(snapshot_path: Path) -> Optional[Snapshot]:
    """
    Load a snapshot from disk.

    Args:
        snapshot_path: Path to the snapshot file

    Returns:
        Snapshot object or None if failed
    """
    try:
        with gzip.open(snapshot_path, "rt", encoding="utf-8") as f:
            data = json.load(f)

        tabs = [
            TabSnapshot(
                name=t["name"],
                values_grid=t.get("values_grid", []),
                formulas_grid=t.get("formulas_grid", []),
                row_count=t.get("row_count", 0),
                col_count=t.get("col_count", 0),
            )
            for t in data.get("tabs", [])
        ]

        return Snapshot(
            version=data.get("version", "1.0"),
            name=data.get("name", ""),
            created_at=data.get("created_at", ""),
            spreadsheet_id=data.get("spreadsheet_id", ""),
            concurrency_token=data.get("concurrency_token", ""),
            tabs=tabs,
            metadata=data.get("metadata", {}),
        )
    except Exception:
        return None


def diff_with_snapshot(
    spreadsheet_id: str,
    snapshot_path: Path,
) -> Optional[SnapshotDiff]:
    """
    Compare current spreadsheet state against a snapshot.

    Args:
        spreadsheet_id: The spreadsheet ID
        snapshot_path: Path to the snapshot file

    Returns:
        SnapshotDiff showing differences, or None if failed
    """
    # Load snapshot
    snapshot = load_snapshot(snapshot_path)
    if not snapshot:
        return None

    service = google_client.get_sheets_service()

    # Get current sheets
    try:
        current_sheets = get_all_sheet_names(spreadsheet_id)
    except Exception:
        return None

    snapshot_sheet_names = {t.name for t in snapshot.tabs}

    # Find added/removed tabs
    tabs_added = [s for s in current_sheets if s not in snapshot_sheet_names]
    tabs_removed = [s for s in snapshot_sheet_names if s not in current_sheets]

    # Compare modified tabs
    tabs_modified: List[Dict[str, Any]] = []
    total_cell_changes = 0

    for tab in snapshot.tabs:
        if tab.name not in current_sheets:
            continue  # Tab was removed

        range_spec = f"'{tab.name}'"

        try:
            # Fetch current values AND formulas
            current_values_resp = (
                service.spreadsheets()
                .values()
                .get(
                    spreadsheetId=spreadsheet_id,
                    range=range_spec,
                    valueRenderOption="UNFORMATTED_VALUE",
                )
                .execute()
            )
            current_formulas_resp = (
                service.spreadsheets()
                .values()
                .get(
                    spreadsheetId=spreadsheet_id,
                    range=range_spec,
                    valueRenderOption="FORMULA",
                )
                .execute()
            )
            current_values = current_values_resp.get("values", [])
            current_formulas = current_formulas_resp.get("values", [])
        except Exception:
            continue

        # Compare cell by cell (both values AND formulas)
        changes = 0
        formula_changes = 0
        sample_changes: List[Dict[str, Any]] = []

        # Get max dimensions across all grids
        max_rows = max(
            len(tab.values_grid),
            len(tab.formulas_grid),
            len(current_values),
            len(current_formulas),
        )

        for r_idx in range(max_rows):
            snap_val_row = tab.values_grid[r_idx] if r_idx < len(tab.values_grid) else []
            snap_form_row = tab.formulas_grid[r_idx] if r_idx < len(tab.formulas_grid) else []
            curr_val_row = current_values[r_idx] if r_idx < len(current_values) else []
            curr_form_row = current_formulas[r_idx] if r_idx < len(current_formulas) else []

            max_cols = max(len(snap_val_row), len(snap_form_row), len(curr_val_row), len(curr_form_row))
            for c_idx in range(max_cols):
                snap_val = snap_val_row[c_idx] if c_idx < len(snap_val_row) else None
                snap_form = snap_form_row[c_idx] if c_idx < len(snap_form_row) else None
                curr_val = curr_val_row[c_idx] if c_idx < len(curr_val_row) else None
                curr_form = curr_form_row[c_idx] if c_idx < len(curr_form_row) else None

                # Check for value change or formula change
                val_changed = snap_val != curr_val
                form_changed = snap_form != curr_form

                if val_changed or form_changed:
                    changes += 1
                    if form_changed:
                        formula_changes += 1

                    if len(sample_changes) < 5:
                        col_letter = _col_index_to_letter(c_idx)
                        change_info = {
                            "cell": f"{col_letter}{r_idx + 1}",
                            "snapshot_value": snap_val,
                            "current_value": curr_val,
                        }
                        if form_changed:
                            change_info["snapshot_formula"] = snap_form
                            change_info["current_formula"] = curr_form
                        sample_changes.append(change_info)

        if changes > 0:
            tabs_modified.append({
                "name": tab.name,
                "changes": changes,
                "formula_changes": formula_changes,
                "sample_changes": sample_changes,
            })
            total_cell_changes += changes

    return SnapshotDiff(
        tabs_added=tabs_added,
        tabs_removed=tabs_removed,
        tabs_modified=tabs_modified,
        total_cell_changes=total_cell_changes,
    )


def restore_snapshot(
    snapshot_path: Path,
    spreadsheet_id: Optional[str] = None,
    tabs: Optional[List[str]] = None,
    create_backup: bool = True,
    force: bool = False,
) -> RestoreResult:
    """
    Restore a snapshot to a spreadsheet.

    Args:
        snapshot_path: Path to the snapshot file
        spreadsheet_id: Target spreadsheet ID (default: same as snapshot)
        tabs: Optional list of tabs to restore (None = all)
        create_backup: Create backup tabs before restoring
        force: Skip concurrency token check

    Returns:
        RestoreResult with details
    """
    # Load snapshot
    snapshot = load_snapshot(snapshot_path)
    if not snapshot:
        return RestoreResult(
            success=False,
            errors=["Failed to load snapshot file"],
        )

    target_id = spreadsheet_id or snapshot.spreadsheet_id
    service = google_client.get_sheets_service()

    # Check concurrency token AND shape drift if not forcing
    if not force:
        drift_errors: List[str] = []
        etag_mismatch = False
        shape_drifts: List[ShapeDrift] = []

        # Check ETag/concurrency token
        try:
            metadata = google_client.get_sheet_metadata(target_id)
            current_token = metadata.get("headRevisionId") or metadata.get("modifiedTime", "")

            if current_token != snapshot.concurrency_token:
                etag_mismatch = True
                drift_errors.append("Spreadsheet has been modified since snapshot was created (ETag mismatch).")
                drift_errors.append(f"  Snapshot token: {snapshot.concurrency_token[:20]}...")
                drift_errors.append(f"  Current token:  {current_token[:20]}...")
        except Exception as e:
            return RestoreResult(
                success=False,
                errors=[f"Failed to check concurrency: {e}"],
            )

        # Check shape drift (any row/col change)
        try:
            shape_drifts = check_shape_drift(target_id, snapshot)
            if shape_drifts:
                drift_errors.append("Shape drift detected (rows/columns changed):")
                for drift in shape_drifts:
                    drift_errors.append(f"  {drift}")
        except Exception as e:
            drift_errors.append(f"Failed to check shape drift: {e}")

        # Hard block on any drift
        if etag_mismatch or shape_drifts:
            drift_errors.append("")
            drift_errors.append("Use --force to override this check (data may be lost).")
            drift_errors.append(f"Suggested command: g-gremlin snapshot restore --name {snapshot.name} --force")
            return RestoreResult(
                success=False,
                errors=drift_errors,
            )

    # Determine tabs to restore
    tabs_to_restore = tabs if tabs else [t.name for t in snapshot.tabs]
    tabs_in_snapshot = {t.name: t for t in snapshot.tabs}

    # Validate requested tabs
    missing = [t for t in tabs_to_restore if t not in tabs_in_snapshot]
    if missing:
        return RestoreResult(
            success=False,
            errors=[f"Tabs not in snapshot: {', '.join(missing)}"],
        )

    # Create backup if requested
    backup_suffix = None
    if create_backup:
        backup_suffix = datetime.now().strftime("_backup_%Y%m%d_%H%M%S")

    tabs_restored = 0
    cells_restored = 0
    errors: List[str] = []

    for tab_name in tabs_to_restore:
        tab = tabs_in_snapshot[tab_name]

        # Create backup tab if requested
        if backup_suffix:
            try:
                _copy_tab(service, target_id, tab_name, f"{tab_name}{backup_suffix}")
            except Exception as e:
                errors.append(f"Failed to backup tab '{tab_name}': {e}")

        # Restore formulas (which includes values for non-formula cells)
        if tab.formulas_grid:
            range_spec = f"'{tab_name}'!A1"
            try:
                service.spreadsheets().values().update(
                    spreadsheetId=target_id,
                    range=range_spec,
                    valueInputOption="USER_ENTERED",
                    body={"values": tab.formulas_grid},
                ).execute()
                tabs_restored += 1
                cells_restored += tab.row_count * tab.col_count
            except Exception as e:
                errors.append(f"Failed to restore tab '{tab_name}': {e}")

    return RestoreResult(
        success=len(errors) == 0,
        message=f"Restored {tabs_restored} tabs, {cells_restored} cells",
        tabs_restored=tabs_restored,
        cells_restored=cells_restored,
        backup_created=backup_suffix if backup_suffix and tabs_restored > 0 else None,
        errors=errors,
    )


def _copy_tab(service, spreadsheet_id: str, source_name: str, dest_name: str) -> None:
    """Copy a tab within a spreadsheet."""
    # Get source sheet ID
    response = (
        service.spreadsheets()
        .get(spreadsheetId=spreadsheet_id, fields="sheets.properties")
        .execute()
    )

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

    if source_id is None:
        raise ValueError(f"Source tab '{source_name}' not found")

    # Duplicate the sheet
    result = (
        service.spreadsheets()
        .sheets()
        .copyTo(
            spreadsheetId=spreadsheet_id,
            sheetId=source_id,
            body={"destinationSpreadsheetId": spreadsheet_id},
        )
        .execute()
    )

    new_sheet_id = result.get("sheetId")

    # Rename the new sheet
    service.spreadsheets().batchUpdate(
        spreadsheetId=spreadsheet_id,
        body={
            "requests": [
                {
                    "updateSheetProperties": {
                        "properties": {
                            "sheetId": new_sheet_id,
                            "title": dest_name,
                        },
                        "fields": "title",
                    }
                }
            ]
        },
    ).execute()


def _update_index(
    snapshots_dir: Path,
    name: str,
    path: Path,
    created_at: str,
    spreadsheet_id: str,
    concurrency_token: Optional[str] = None,
    shape: Optional[Dict[str, Dict[str, int]]] = None,
    tabs: Optional[List[str]] = None,
    metadata: Optional[Dict[str, Any]] = None,
) -> None:
    """
    Update the snapshots index file with enhanced metadata.

    Args:
        snapshots_dir: Directory containing snapshots
        name: Snapshot name
        path: Path to snapshot file
        created_at: ISO timestamp of creation
        spreadsheet_id: Source spreadsheet ID
        concurrency_token: ETag/revision ID at time of snapshot
        shape: Tab shapes as {"TabName": {"rows": N, "cols": M}}
        tabs: List of tab names in snapshot
        metadata: Additional metadata (command_string, correlation_id, etc.)
    """
    index_path = snapshots_dir / "index.json"

    index: Dict[str, Any] = {"schema_version": "2.0"}
    if index_path.exists():
        try:
            with index_path.open("r", encoding="utf-8") as f:
                index = json.load(f)
        except Exception:
            pass

    if "snapshots" not in index:
        index["snapshots"] = {}

    # Build entry with all new fields
    entry: Dict[str, Any] = {
        "path": str(path),
        "created_at": created_at,
        "spreadsheet_id": spreadsheet_id,
    }

    if concurrency_token:
        entry["concurrency_token"] = concurrency_token
    if shape:
        entry["shape"] = shape
    if tabs:
        entry["tabs"] = tabs
    if metadata:
        entry["metadata"] = metadata

    index["snapshots"][name] = entry
    index["schema_version"] = "2.0"

    with index_path.open("w", encoding="utf-8") as f:
        json.dump(index, f, indent=2)


def find_snapshot_by_name(
    name: str,
    snapshots_dir: Optional[Path] = None,
) -> Optional[Path]:
    """
    Find a snapshot by name.

    Args:
        name: Snapshot name
        snapshots_dir: Directory to search

    Returns:
        Path to snapshot file or None
    """
    snapshots_dir = snapshots_dir or SNAPSHOTS_DIR

    if not snapshots_dir.exists():
        return None

    # Check index first
    index_path = snapshots_dir / "index.json"
    if index_path.exists():
        try:
            with index_path.open("r", encoding="utf-8") as f:
                index = json.load(f)
            entry = index.get("snapshots", {}).get(name)
            if entry:
                path = Path(entry["path"])
                if path.exists():
                    return path
        except Exception:
            pass

    # Fall back to searching files
    safe_name = name.replace(" ", "_").replace("/", "_")
    for path in snapshots_dir.glob(f"{safe_name}_*.json.gz"):
        return path

    return None
