"""
Sync orchestrator for g-gremlin.

Provides config-driven multi-tab write with dry-run support.
"""

from __future__ import annotations

import csv
import re
from dataclasses import asdict, dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple

from g_gremlin.config_schema import SyncConfig, validate_config


@dataclass
class TabPlan:
    """Plan for a single tab write."""
    tab_name: str
    csv_path: str
    rows: int
    cols: int
    formula_count: int
    anchor: str
    formulas_enabled: bool
    sheet_refs: List[str]  # Sheet names referenced in formulas


@dataclass
class SyncPlan:
    """Complete sync plan for dry-run output."""
    sheet_id: str
    tabs: List[TabPlan]
    named_ranges: Dict[str, str]
    styles_count: int
    variables: Dict[str, str]
    total_cells: int
    total_formulas: int
    estimated_api_calls: int
    warnings: List[str]
    sheet_refs_in_config: Set[str]  # Tabs declared in config
    sheet_refs_in_formulas: Set[str]  # Sheets referenced by formulas
    missing_refs: Set[str]  # Refs in formulas but not in config

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "sheet_id": self.sheet_id,
            "tabs": [asdict(t) for t in self.tabs],
            "named_ranges": self.named_ranges,
            "styles_count": self.styles_count,
            "variables": self.variables,
            "total_cells": self.total_cells,
            "total_formulas": self.total_formulas,
            "estimated_api_calls": self.estimated_api_calls,
            "warnings": self.warnings,
            "sheet_refs_in_config": list(self.sheet_refs_in_config),
            "sheet_refs_in_formulas": list(self.sheet_refs_in_formulas),
            "missing_refs": list(self.missing_refs),
        }


@dataclass
class TabWriteResult:
    """Result of writing a single tab."""
    tab_name: str
    rows_written: int
    existing_rows_before: int
    total_rows_after: int


@dataclass
class SyncResult:
    """Result of sync execution."""
    success: bool
    tabs_written: int
    total_rows: int
    named_ranges_created: int
    styles_applied: int
    errors: List[str]
    tab_results: List[TabWriteResult] = field(default_factory=list)


# Built-in template variables
BUILTIN_VARIABLES = {
    "TODAY": lambda: datetime.now().strftime("%Y-%m-%d"),
    "NOW": lambda: datetime.now().isoformat(timespec="seconds"),
}


def _get_sheet_headers(spreadsheet_id: str, tab_name: str) -> List[str]:
    """
    Get the header row (first row) from a sheet tab.

    Args:
        spreadsheet_id: The spreadsheet ID
        tab_name: Name of the tab

    Returns:
        List of header values (may be empty if tab doesn't exist or is empty)
    """
    from g_gremlin.sheets_io import get_all_sheet_names
    from g_gremlin import google_client

    try:
        # Check if tab exists
        existing_sheets = get_all_sheet_names(spreadsheet_id)
        if tab_name not in existing_sheets:
            return []

        # Get first row
        service = google_client.get_sheets_service()
        range_ref = f"'{tab_name}'!1:1"
        response = (
            service.spreadsheets()
            .values()
            .get(spreadsheetId=spreadsheet_id, range=range_ref)
            .execute()
        )
        values = response.get("values", [])
        if values and len(values) > 0:
            return [str(v) for v in values[0]]
        return []
    except Exception:
        # If any error, just return empty (tab probably doesn't exist)
        return []


def _compare_schemas(csv_headers: List[str], sheet_headers: List[str]) -> Dict[str, Any]:
    """
    Compare CSV headers with current sheet headers.

    Args:
        csv_headers: Headers from the incoming CSV
        sheet_headers: Headers from the current sheet

    Returns:
        Dict with added, removed, reordered columns and a summary message
    """
    csv_set = set(csv_headers)
    sheet_set = set(sheet_headers)

    added = csv_set - sheet_set
    removed = sheet_set - csv_set
    reordered = csv_headers != sheet_headers and csv_set == sheet_set

    message = None
    if added or removed or reordered:
        parts = []
        if added:
            parts.append(f"{len(added)} new columns: {', '.join(sorted(added))}")
        if removed:
            parts.append(f"{len(removed)} removed columns: {', '.join(sorted(removed))}")
        if reordered:
            parts.append("column order changed")
        message = "; ".join(parts)

    return {
        "added": sorted(added),
        "removed": sorted(removed),
        "reordered": reordered,
        "message": message,
    }


def build_sync_plan(
    config: SyncConfig,
    only_tab: Optional[str] = None,
    check_schemas: bool = False,
) -> SyncPlan:
    """
    Build a sync plan from config without making any API calls.

    This is used for dry-run preview and validation.

    Args:
        config: Validated SyncConfig
        only_tab: If set, only include this tab in the plan
        check_schemas: If True, compare CSV headers with current sheet headers

    Returns:
        SyncPlan with all details for preview
    """
    warnings: List[str] = []
    tab_plans: List[TabPlan] = []
    all_sheet_refs: Set[str] = set()
    total_cells = 0
    total_formulas = 0

    # Filter tabs if --only specified
    tabs_to_process = config.tabs
    if only_tab:
        if only_tab not in config.tabs:
            warnings.append(f"Tab '{only_tab}' not found in config")
            tabs_to_process = {}
        else:
            tabs_to_process = {only_tab: config.tabs[only_tab]}

    # Build variables dict with builtins
    variables = _build_variables(config)

    # Analyze each tab
    for tab_name, tab_config in tabs_to_process.items():
        csv_path = tab_config.resolve_csv_path(config.config_dir)

        if not csv_path.exists():
            warnings.append(f"CSV file not found: {csv_path}")
            continue

        # Read and analyze CSV
        rows, cols, formula_count, sheet_refs = _analyze_csv(
            csv_path,
            tab_config.formulas,
            variables,
            tab_name,
        )

        # Schema diff check (if enabled and sheet_id available)
        if check_schemas and config.sheet_id:
            # Get CSV headers
            try:
                with csv_path.open("r", encoding="utf-8") as f:
                    reader = csv.reader(f)
                    csv_headers = next(reader, [])

                # Get current sheet headers
                sheet_headers = _get_sheet_headers(config.sheet_id, tab_name)

                # Compare if sheet exists (has headers)
                if sheet_headers:
                    schema_diff = _compare_schemas(csv_headers, sheet_headers)
                    if schema_diff["message"]:
                        warnings.append(
                            f"⚠ {tab_name}: Schema changed - {schema_diff['message']}. "
                            "This may affect existing formulas."
                        )
            except Exception as e:
                warnings.append(f"Could not check schema for {tab_name}: {e}")

        tab_plans.append(TabPlan(
            tab_name=tab_name,
            csv_path=str(csv_path),
            rows=rows,
            cols=cols,
            formula_count=formula_count,
            anchor=tab_config.anchor,
            formulas_enabled=tab_config.formulas,
            sheet_refs=list(sheet_refs),
        ))

        all_sheet_refs.update(sheet_refs)
        total_cells += rows * cols
        total_formulas += formula_count

    # Check config validation warnings
    config_warnings = validate_config(config)
    warnings.extend(config_warnings)

    # Calculate sheet refs in config vs formulas
    sheet_refs_in_config = set(config.tabs.keys())
    missing_refs = all_sheet_refs - sheet_refs_in_config

    if missing_refs:
        warnings.append(
            f"Formulas reference sheets not in config: {', '.join(sorted(missing_refs))}. "
            "Use --validate-live to check if they exist in the spreadsheet."
        )

    # Check max cells
    if total_cells > config.settings.max_cells:
        warnings.append(
            f"Total cells ({total_cells:,}) exceeds max_cells ({config.settings.max_cells:,})"
        )

    # Estimate API calls: 1 per tab + 1 for named ranges + 1 for styles
    estimated_calls = len(tab_plans)
    if config.named_ranges:
        estimated_calls += 1
    if config.styles:
        estimated_calls += 1

    return SyncPlan(
        sheet_id=config.sheet_id,
        tabs=tab_plans,
        named_ranges=config.named_ranges,
        styles_count=len(config.styles),
        variables=variables,
        total_cells=total_cells,
        total_formulas=total_formulas,
        estimated_api_calls=estimated_calls,
        warnings=warnings,
        sheet_refs_in_config=sheet_refs_in_config,
        sheet_refs_in_formulas=all_sheet_refs,
        missing_refs=missing_refs,
    )


def validate_live_refs(
    config: SyncConfig,
    plan: SyncPlan,
) -> Tuple[List[str], List[str]]:
    """
    Validate sheet references against actual spreadsheet (makes ONE API call).

    Args:
        config: SyncConfig with sheet_id
        plan: SyncPlan with formula refs

    Returns:
        Tuple of (existing_sheets, missing_sheets)
    """
    from g_gremlin.sheets_io import get_all_sheet_names

    existing = set(get_all_sheet_names(config.sheet_id))
    existing_lower = {s.lower() for s in existing}

    # Check which formula refs exist
    found: List[str] = []
    missing: List[str] = []

    for ref in plan.sheet_refs_in_formulas:
        if ref.lower() in existing_lower:
            found.append(ref)
        else:
            missing.append(ref)

    return found, missing


def execute_sync(
    config: SyncConfig,
    plan: SyncPlan,
    dry_run: bool = True,
    resume: bool = False,
) -> SyncResult:
    """
    Execute the sync plan (write tabs, named ranges, styles).

    Args:
        config: SyncConfig
        plan: SyncPlan from build_sync_plan
        dry_run: If True, don't actually write (default True for safety)
        resume: Enable checkpointed streaming writes (for large tabs)

    Returns:
        SyncResult with execution details
    """
    if not plan.tabs:
        msg = "No tabs to sync (missing CSV files or --only filtered everything)."
        return SyncResult(
            success=False,
            tabs_written=0,
            total_rows=0,
            named_ranges_created=0,
            styles_applied=0,
            errors=[msg],
        )

    if dry_run:
        return SyncResult(
            success=True,
            tabs_written=0,
            total_rows=0,
            named_ranges_created=0,
            styles_applied=0,
            errors=["Dry run - no changes made"],
        )

    # Safety check: abort if max cells exceeded
    if plan.total_cells > config.settings.max_cells:
        return SyncResult(
            success=False,
            tabs_written=0,
            total_rows=0,
            named_ranges_created=0,
            styles_applied=0,
            errors=[
                f"Aborted: Would write {plan.total_cells:,} cells, "
                f"exceeds max_cells limit ({config.settings.max_cells:,}). "
                "Increase settings.max_cells in config to proceed."
            ],
        )

    # Safety check: abort if no tabs to write (likely config/path error)
    if not plan.tabs:
        return SyncResult(
            success=False,
            tabs_written=0,
            total_rows=0,
            named_ranges_created=0,
            styles_applied=0,
            errors=[
                "Aborted: No tabs to write. Check that CSV files exist and "
                "--only tab name matches config. See warnings in dry-run output."
            ],
        )

    from g_gremlin.sheets_io import write_csv_to_sheet

    errors: List[str] = []
    tabs_written = 0
    total_rows = 0
    tab_results: List[TabWriteResult] = []

    # Build variables for template substitution
    variables = _build_variables(config)

    # Write each tab
    for tab_plan in plan.tabs:
        tab_config = config.tabs[tab_plan.tab_name]
        csv_path = Path(tab_plan.csv_path)

        try:
            # Read CSV and apply template substitution
            processed_csv = _process_csv_for_write(
                csv_path,
                variables,
                tab_plan.tab_name,
            )

            # Write to a temp file for the write function
            import tempfile
            with tempfile.NamedTemporaryFile(
                mode="w",
                suffix=".csv",
                delete=False,
                newline="",
                encoding="utf-8",
            ) as tmp:
                writer = csv.writer(tmp)
                writer.writerows(processed_csv)
                tmp_path = Path(tmp.name)

            try:
                result = write_csv_to_sheet(
                    spreadsheet_id=config.sheet_id,
                    tab_name=tab_plan.tab_name,
                    csv_path=tmp_path,
                    mode="overwrite",
                    backup=config.settings.require_backup,
                    formulas=tab_config.formulas,
                    anchor=tab_config.anchor,
                    resume=resume,
                    streaming=resume,
                )
                tabs_written += 1
                total_rows += result.rows_written
                tab_results.append(
                    TabWriteResult(
                        tab_name=tab_plan.tab_name,
                        rows_written=result.rows_written,
                        existing_rows_before=result.existing_rows_before,
                        total_rows_after=result.total_rows_after,
                    )
                )
            finally:
                tmp_path.unlink(missing_ok=True)

        except Exception as e:
            errors.append(f"Error writing tab '{tab_plan.tab_name}': {e}")

    # Create/update named ranges
    named_ranges_created = 0
    if config.named_ranges:
        from g_gremlin.named_ranges import upsert_named_ranges
        nr_result = upsert_named_ranges(config.sheet_id, config.named_ranges)
        named_ranges_created = nr_result.created + nr_result.updated
        if nr_result.errors:
            errors.extend(nr_result.errors)

    # Apply styles to each tab
    styles_applied = 0
    if config.styles:
        from g_gremlin.styling import apply_styles, StyleSpec as StylingStyleSpec

        for tab_plan in plan.tabs:
            # Convert config styles to styling module format
            styling_specs = [
                StylingStyleSpec(
                    range=s.range,
                    header=s.header,
                    bold=s.bold,
                    background=s.background,
                    font_color=s.font_color,
                    zebra=s.zebra,
                    font_size=s.font_size,
                    freeze_rows=s.freeze_rows,
                    freeze_cols=s.freeze_cols,
                )
                for s in config.styles
            ]

            style_result = apply_styles(
                config.sheet_id,
                tab_plan.tab_name,
                styling_specs,
                data_rows=tab_plan.rows,
            )

            if style_result.success:
                styles_applied += style_result.styles_applied
            if style_result.errors:
                errors.extend(style_result.errors)

    return SyncResult(
        success=len(errors) == 0,
        tabs_written=tabs_written,
        total_rows=total_rows,
        named_ranges_created=named_ranges_created,
        styles_applied=styles_applied,
        errors=errors,
        tab_results=tab_results,
    )


def format_plan_output(plan: SyncPlan, verbose: bool = False) -> str:
    """Format sync plan for CLI output."""
    lines: List[str] = []

    lines.append("DRY RUN - No changes will be made")
    lines.append("")

    # Tabs
    lines.append("Tabs to write:")
    for tab in plan.tabs:
        formula_str = f", {tab.formula_count} formulas" if tab.formula_count else ""
        lines.append(
            f"  {tab.tab_name:<20} {tab.csv_path:<40} "
            f"({tab.rows} rows, {tab.cols} cols{formula_str})"
        )

    # Named ranges
    if plan.named_ranges:
        lines.append("")
        lines.append("Named ranges to create/update:")
        for name, ref in plan.named_ranges.items():
            lines.append(f"  {name:<20} {ref}")

    # Styles
    if plan.styles_count:
        lines.append("")
        lines.append(f"Styles to apply: {plan.styles_count}")

    # Summary
    lines.append("")
    lines.append(f"Estimated API calls: {plan.estimated_api_calls}")
    lines.append(f"Estimated cells: {plan.total_cells:,}")

    # Warnings
    if plan.warnings:
        lines.append("")
        lines.append("Warnings:")
        for warning in plan.warnings:
            lines.append(f"  ! {warning}")

    lines.append("")
    lines.append("Run with --apply to execute.")

    return "\n".join(lines)


def _build_variables(config: SyncConfig) -> Dict[str, str]:
    """Build variables dict with builtins and user-defined."""
    variables: Dict[str, str] = {}

    # Add builtins
    for name, func in BUILTIN_VARIABLES.items():
        variables[name] = func()

    # Add sheet_id
    variables["SHEET_ID"] = config.sheet_id

    # Add user-defined (can override builtins)
    variables.update(config.variables)

    return variables


def _analyze_csv(
    csv_path: Path,
    formulas_enabled: bool,
    variables: Dict[str, str],
    tab_name: str,
) -> Tuple[int, int, int, Set[str]]:
    """
    Analyze a CSV file for sync planning.

    Returns:
        Tuple of (rows, cols, formula_count, sheet_refs)
    """
    with csv_path.open("r", newline="", encoding="utf-8-sig") as f:
        reader = csv.reader(f)
        rows = list(reader)

    if not rows:
        return 0, 0, 0, set()

    row_count = len(rows)
    col_count = max(len(row) for row in rows) if rows else 0
    formula_count = 0
    sheet_refs: Set[str] = set()

    if formulas_enabled:
        for row in rows:
            for cell in row:
                if isinstance(cell, str) and cell.startswith("="):
                    formula_count += 1
                    # Extract sheet references
                    refs = _extract_sheet_refs(cell)
                    sheet_refs.update(refs)

    return row_count, col_count, formula_count, sheet_refs


def _extract_sheet_refs(formula: str) -> Set[str]:
    """Extract sheet names referenced in a formula."""
    refs: Set[str] = set()

    # Quoted sheet names: 'Sheet Name'!A1
    quoted = re.findall(r"'([^']+)'!", formula)
    refs.update(quoted)

    # Unquoted sheet names: SheetName!A1
    unquoted = re.findall(r"([A-Za-z][A-Za-z0-9_]*)!", formula)
    refs.update(unquoted)

    return refs


def _process_csv_for_write(
    csv_path: Path,
    variables: Dict[str, str],
    tab_name: str,
) -> List[List[str]]:
    """
    Read CSV and apply template variable substitution.

    Variables are substituted in non-formula cells only.
    """
    with csv_path.open("r", newline="", encoding="utf-8-sig") as f:
        reader = csv.reader(f)
        rows = list(reader)

    # Add TAB_NAME to variables for this tab
    vars_with_tab = {**variables, "TAB_NAME": tab_name}

    processed: List[List[str]] = []
    for row in rows:
        new_row: List[str] = []
        for cell in row:
            if isinstance(cell, str):
                # Don't substitute in formulas
                if not cell.startswith("="):
                    cell = _substitute_variables(cell, vars_with_tab)
            new_row.append(cell)
        processed.append(new_row)

    return processed


def _substitute_variables(text: str, variables: Dict[str, str]) -> str:
    """Replace {{VAR}} placeholders with variable values."""
    def replacer(match: re.Match[str]) -> str:
        var_name = match.group(1)
        return variables.get(var_name, match.group(0))

    return re.sub(r"\{\{(\w+)\}\}", replacer, text)
