"""
Build a target-driven B2B SaaS waterfall model in Google Sheets.
"""

from __future__ import annotations

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


DEFAULT_PACING = [0.15, 0.20, 0.30, 0.35]


def _spread_quarterly_to_monthly(pacing: List[float]) -> List[float]:
    """
    Evenly split quarterly pacing across three months per quarter.
    """
    monthly: List[float] = []
    for val in pacing:
        monthly.extend([round(float(val) / 3, 6)] * 3)
    return monthly


DEFAULT_MONTHLY_PACING = _spread_quarterly_to_monthly(DEFAULT_PACING)

QUARTERLY_COLUMN_LETTERS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q"]
QUARTERLY_TIME_COLS = list(range(4, len(QUARTERLY_COLUMN_LETTERS)))  # E through Q
QUARTERLY_FY26_COLS = [9, 10, 11, 12]  # J-M
QUARTERLY_FY27_COLS = [13, 14, 15, 16]  # N-Q
QUARTERLY_PACING_COLS = ["E", "F", "G", "H"]


def _col_letter(idx: int) -> str:
    """
    Convert zero-based index to Excel-style column letter.
    """
    result = ""
    idx_val = idx
    while True:
        idx_val, rem = divmod(idx_val, 26)
        result = chr(ord("A") + rem) + result
        if idx_val == 0:
            break
        idx_val -= 1
    return result


MONTHLY_COLUMN_LETTERS = [_col_letter(i) for i in range(28)]  # A..AB
MONTHLY_TIME_COLS = list(range(4, 28))
MONTHLY_FY26_COLS = MONTHLY_TIME_COLS[:12]  # months 1-12
MONTHLY_FY27_COLS = MONTHLY_TIME_COLS[12:]  # months 13-24
MONTHLY_PACING_COLS = [_col_letter(i) for i in range(4, 16)]  # E..P


@dataclass(frozen=True)
class WaterfallLayout:
    name: str  # quarterly | monthly
    column_letters: List[str]
    time_cols: List[int]
    fy26_cols: List[int]
    fy27_cols: List[int]
    pacing_cols: List[str]
    lag_header: str
    sdr_productivity_cell: str
    ae_productivity_cell: str
    sdr_productivity_metric: str
    sdr_capacity_metric: str
    ae_capacity_metric: str

    @property
    def end_column(self) -> str:
        return self.column_letters[-1]


QUARTERLY_LAYOUT = WaterfallLayout(
    name="quarterly",
    column_letters=QUARTERLY_COLUMN_LETTERS,
    time_cols=QUARTERLY_TIME_COLS,
    fy26_cols=QUARTERLY_FY26_COLS,
    fy27_cols=QUARTERLY_FY27_COLS,
    pacing_cols=QUARTERLY_PACING_COLS,
    lag_header="Lag (qtrs)",
    sdr_productivity_cell="$B$22",
    ae_productivity_cell="$B$23",
    sdr_productivity_metric="SDR Productivity (disco calls/qtr)",
    sdr_capacity_metric="SDR Capacity (disco calls/qtr)",
    ae_capacity_metric="AE Capacity ($/qtr)",
)

MONTHLY_LAYOUT = WaterfallLayout(
    name="monthly",
    column_letters=MONTHLY_COLUMN_LETTERS,
    time_cols=MONTHLY_TIME_COLS,
    fy26_cols=MONTHLY_FY26_COLS,
    fy27_cols=MONTHLY_FY27_COLS,
    pacing_cols=MONTHLY_PACING_COLS,
    lag_header="Lag (mo)",
    sdr_productivity_cell="$B$21",
    ae_productivity_cell="$B$24",
    sdr_productivity_metric="SDR Productivity (disco calls/month)",
    sdr_capacity_metric="SDR Capacity (disco calls/month)",
    ae_capacity_metric="AE Capacity ($/month)",
)


@dataclass
class WaterfallInputs:
    annual_target: float
    granularity: str = "quarterly"  # quarterly | monthly
    nl_attribution: float = 0.925
    upsell_attribution: float = 0.075
    multiplier: float = 1.2
    disco_to_pipe_rate: float = 0.4
    nl_win_rate: float = 0.25
    nl_asp: float = 175000.0
    nl_lag: int = 2
    upsell_win_rate: float = 0.50
    upsell_asp: float = 43750.0
    upsell_lag: int = 1
    sdr_productivity: int = 10
    ae_productivity: float = 250000.0
    ae_to_se_ratio: float = 2.5
    poc_rate: float = 0.8
    quarterly_pacing: Optional[List[float]] = None
    monthly_pacing: Optional[List[float]] = None
    nl_lag_months: Optional[int] = None
    upsell_lag_months: Optional[int] = None
    fy27_growth: float = 1.15
    fy_start_month: int = 1
    first_fy: int = 26  # First fiscal year (e.g., 26 for FY26)
    tab_suffix: str = ""  # e.g., "_v2" to append to tab names
    idempotency_action: str = ""  # "replace" or "suffix"

    def __post_init__(self) -> None:
        gran = (self.granularity or "quarterly").lower()
        if gran not in ("quarterly", "monthly"):
            raise ValueError("granularity must be 'quarterly' or 'monthly'.")
        self.granularity = gran

        self.quarterly_pacing = _validate_pacing("quarterly_pacing", self.quarterly_pacing or DEFAULT_PACING, 4)
        if self.granularity == "monthly":
            derived_monthly = self.monthly_pacing or _spread_quarterly_to_monthly(self.quarterly_pacing)
            self.monthly_pacing = _validate_pacing("monthly_pacing", derived_monthly, 12)
        else:
            self.monthly_pacing = self.monthly_pacing or _spread_quarterly_to_monthly(self.quarterly_pacing)

        if self.fy_start_month < 1 or self.fy_start_month > 12:
            raise ValueError("fy_start_month must be between 1 and 12.")

        # Derive month-based lags if not explicitly provided
        if self.nl_lag_months is None:
            self.nl_lag_months = int(self.nl_lag * 3)
        if self.upsell_lag_months is None:
            self.upsell_lag_months = int(self.upsell_lag * 3)


def build_waterfall(
    inputs: WaterfallInputs,
    sheets_service,
    spreadsheet_id: Optional[str] = None,
    title: str = "Sales & Marketing Waterfall",
) -> Dict[str, Any]:
    """
    Build the full waterfall workbook (two assumption tabs + main waterfall).
    """
    created = False
    if spreadsheet_id is None:
        spreadsheet = sheets_service.spreadsheets().create(body={"properties": {"title": title}}).execute()
        spreadsheet_id = spreadsheet["spreadsheetId"]
        created = True

    # Use dynamic FY years from inputs
    first_fy = inputs.first_fy or 26
    second_fy = first_fy + 1
    suffix = inputs.tab_suffix or ""
    fy26_tab = f"FY{first_fy}_Assumptions{suffix}"
    fy27_tab = f"FY{second_fy}_Assumptions{suffix}"
    waterfall_tab = f"Waterfall{suffix}"
    replace_existing = inputs.idempotency_action == "replace"

    if inputs.granularity == "monthly":
        layout = MONTHLY_LAYOUT
        _write_assumptions_tab_monthly(sheets_service, spreadsheet_id, fy26_tab, inputs, growth=1.0, replace=replace_existing)
        _write_assumptions_tab_monthly(sheets_service, spreadsheet_id, fy27_tab, inputs, growth=inputs.fy27_growth, replace=replace_existing)
        rows = _waterfall_rows_monthly(layout, fy26_tab, fy27_tab, inputs.fy_start_month, inputs)
    else:
        layout = QUARTERLY_LAYOUT
        _write_assumptions_tab(sheets_service, spreadsheet_id, fy26_tab, inputs, growth=1.0, replace=replace_existing)
        _write_assumptions_tab(sheets_service, spreadsheet_id, fy27_tab, inputs, growth=inputs.fy27_growth, replace=replace_existing)
        rows = _waterfall_rows_quarterly(layout, fy26_tab, fy27_tab, inputs.fy_start_month, inputs)

    _write_rows(sheets_service, spreadsheet_id, waterfall_tab, rows, layout.column_letters, replace=replace_existing)
    _apply_waterfall_style(sheets_service, spreadsheet_id, waterfall_tab, layout.column_letters)
    validation = _validate_waterfall(
        sheets_service,
        spreadsheet_id,
        waterfall_tab,
        layout,
        target_metric="Total Target",
        realized_metric="Total Bookings Realized",
        sdr_capacity_metric=layout.sdr_capacity_metric,
        sdr_demand_metric="Total Disco Calls",
        ae_capacity_metric=layout.ae_capacity_metric,
    )

    return {
        "spreadsheet_id": spreadsheet_id,
        "url": f"https://docs.google.com/spreadsheets/d/{spreadsheet_id}",
        "tabs_created": [fy26_tab, fy27_tab, waterfall_tab],
        "created": created,
        "validation": validation,
    }


# ---------------------------------------------------------------------------
# Tab builders
# ---------------------------------------------------------------------------


def _write_assumptions_tab(
    service,
    spreadsheet_id: str,
    tab_name: str,
    inputs: WaterfallInputs,
    growth: float = 1.0,
    replace: bool = False,
) -> None:
    rows = _assumptions_rows(inputs, growth)
    _write_rows(service, spreadsheet_id, tab_name, rows, QUARTERLY_COLUMN_LETTERS, replace=replace)


def _write_assumptions_tab_monthly(
    service,
    spreadsheet_id: str,
    tab_name: str,
    inputs: WaterfallInputs,
    growth: float = 1.0,
    replace: bool = False,
) -> None:
    rows = _assumptions_rows_monthly(inputs, growth)
    _write_rows(service, spreadsheet_id, tab_name, rows, MONTHLY_COLUMN_LETTERS, replace=replace)


def _waterfall_rows_quarterly(layout: WaterfallLayout, fy26_tab: str, fy27_tab: str, fy_start_month: int, inputs: WaterfallInputs) -> List[List[Any]]:
    rows: List[List[Any]] = []

    # Use dynamic FY years from inputs
    first_fy = inputs.first_fy or 26
    second_fy = first_fy + 1
    prior_fy = first_fy - 1
    prior2_fy = first_fy - 2

    # Header with fiscal-aware quarter labels
    q_labels = _quarter_labels(fy_start_month)
    header_map = {
        0: "Type",
        1: "Category",
        2: "Metric",
        3: layout.lag_header,
        4: f"Q4 FY{prior2_fy}",
        5: f"Q1 FY{prior_fy} {q_labels[0]}",
        6: f"Q2 FY{prior_fy} {q_labels[1]}",
        7: f"Q3 FY{prior_fy} {q_labels[2]}",
        8: f"Q4 FY{prior_fy} {q_labels[3]}",
        9: f"Q1 FY{first_fy} {q_labels[0]}",
        10: f"Q2 FY{first_fy} {q_labels[1]}",
        11: f"Q3 FY{first_fy} {q_labels[2]}",
        12: f"Q4 FY{first_fy} {q_labels[3]}",
        13: f"Q1 FY{second_fy} {q_labels[0]}",
        14: f"Q2 FY{second_fy} {q_labels[1]}",
        15: f"Q3 FY{second_fy} {q_labels[2]}",
        16: f"Q4 FY{second_fy} {q_labels[3]}",
    }
    rows.append(_row_with_values(layout, header_map))
    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "ACTIVITY TARGETS (DRIVERS)"}))

    nl_target_row = len(rows) + 1
    rows.append(_activity_target_row(layout, "Direct NL", "ACV", "NL Target (DRIVER)", fy26_tab, fy27_tab, "$B$5"))
    upsell_target_row = len(rows) + 1
    rows.append(_activity_target_row(layout, "Direct Upsell", "ACV", "Upsell Target (DRIVER)", fy26_tab, fy27_tab, "$B$6"))
    total_target_row = len(rows) + 1
    rows.append(_total_target_row(layout, nl_target_row, upsell_target_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "NL ACTIVITY WATERFALL"}))

    nl_disco_row = len(rows) + 1
    rows.append(_nl_activity_row(layout, "Disco Calls Needed", nl_target_row, fy26_tab, fy27_tab, include_disco=True))
    nl_pipeline_row = len(rows) + 1
    rows.append(_nl_activity_row(layout, "Pipeline Opps Needed", nl_target_row, fy26_tab, fy27_tab, include_disco=False, include_pipe=True))
    nl_deals_row = len(rows) + 1
    rows.append(_nl_activity_row(layout, "C/W Deals Needed", nl_target_row, fy26_tab, fy27_tab, include_disco=False, include_pipe=False))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "UPSELL ACTIVITY WATERFALL"}))

    upsell_pipeline_row = len(rows) + 1
    rows.append(_upsell_activity_row(layout, "Pipeline Opps Needed", upsell_target_row, fy26_tab, fy27_tab, include_pipe=True))
    upsell_deals_row = len(rows) + 1
    rows.append(_upsell_activity_row(layout, "C/W Deals Needed", upsell_target_row, fy26_tab, fy27_tab, include_pipe=False))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "ACTIVITY TOTALS"}))

    total_disco_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total Disco Calls", nl_disco_row, None))
    total_pipeline_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total Pipeline Opps", nl_pipeline_row, upsell_pipeline_row))
    total_deals_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total C/W Deals", nl_deals_row, upsell_deals_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "COHORT BOOKINGS (Lagged Recognition)"}))

    nl_lagged_row = len(rows) + 1
    rows.append(_lagged_row(layout, "Direct NL", "Lagged C/W Deals", nl_deals_row, fy26_tab, fy27_tab, lag_cell="$B$12", lag_value=str(inputs.nl_lag)))
    upsell_lagged_row = len(rows) + 1
    rows.append(_lagged_row(layout, "Direct Upsell", "Lagged C/W Deals", upsell_deals_row, fy26_tab, fy27_tab, lag_cell="$B$16", lag_value=str(inputs.upsell_lag)))
    total_lagged_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total Lagged Deals", nl_lagged_row, upsell_lagged_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "REALIZED BOOKINGS"}))

    nl_realized_row = len(rows) + 1
    rows.append(_realized_row(layout, "Direct NL", "NL Bookings Realized", nl_lagged_row, fy26_tab, fy27_tab, asp_cell="$B$11"))
    upsell_realized_row = len(rows) + 1
    rows.append(_realized_row(layout, "Direct Upsell", "Upsell Bookings Realized", upsell_lagged_row, fy26_tab, fy27_tab, asp_cell="$B$15"))
    total_realized_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total Bookings Realized", nl_realized_row, upsell_realized_row, category="ACV"))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "SDR CAPACITY (Productivity-Driven)"}))

    sdr_productivity_row = len(rows) + 1
    rows.append(_capacity_input_row(layout, "SDR", layout.sdr_productivity_metric, fy26_tab, fy27_tab, QUARTERLY_LAYOUT.sdr_productivity_cell))
    sdr_required_row = len(rows) + 1
    rows.append(_ceiling_row(layout, "SDR", "SDRs Required", total_disco_row, sdr_productivity_row))
    sdr_capacity_row = len(rows) + 1
    rows.append(_product_row(layout, "SDR", layout.sdr_capacity_metric, sdr_required_row, sdr_productivity_row))
    rows.append(_difference_row(layout, "SDR", "Capacity vs Demand (should be +)", sdr_capacity_row, total_disco_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "AE CAPACITY (Productivity-Driven)"}))

    ae_productivity_row = len(rows) + 1
    rows.append(_capacity_input_row(layout, "AE", "AE Productivity ($/qtr/ramped)", fy26_tab, fy27_tab, QUARTERLY_LAYOUT.ae_productivity_cell, category="ACV"))
    ae_required_row = len(rows) + 1
    rows.append(_ceiling_row(layout, "AE", "Ramped AEs Required", total_target_row, ae_productivity_row, category="Count"))
    ae_capacity_row = len(rows) + 1
    rows.append(_product_row(layout, "AE", layout.ae_capacity_metric, ae_required_row, ae_productivity_row, category="ACV"))
    rows.append(_difference_row(layout, "AE", "Capacity vs Target (should be +)", ae_capacity_row, total_target_row, category="ACV"))
    deals_per_ae_row = len(rows) + 1
    rows.append(_ratio_row(layout, "AE", "C/W Deals per Ramped AE", total_deals_row, ae_required_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "SE CAPACITY (Derived from AE Requirements)"}))

    se_ratio_row = len(rows) + 1
    rows.append(_capacity_input_row(layout, "SE", "AE to SE Ratio (DRIVER)", fy26_tab, fy27_tab, "$B$19", category="Ratio"))
    se_required_row = len(rows) + 1
    rows.append(_ceiling_row(layout, "SE", "SEs Required", ae_required_row, se_ratio_row, category="Count"))
    poc_required_row = len(rows) + 1
    rows.append(_poc_required_row(layout, total_pipeline_row, fy26_tab, fy27_tab))
    rows.append(_ratio_row(layout, "SE", "PoCs per SE", poc_required_row, se_required_row))

    return rows


def _waterfall_rows_monthly(layout: WaterfallLayout, fy26_tab: str, fy27_tab: str, fy_start_month: int, inputs: WaterfallInputs) -> List[List[Any]]:
    rows: List[List[Any]] = []

    # Use dynamic FY years from inputs
    first_fy = inputs.first_fy or 26
    second_fy = first_fy + 1

    month_labels = _month_labels(fy_start_month)
    header_map: Dict[int, str] = {0: "Type", 1: "Category", 2: "Metric", 3: layout.lag_header}
    for idx, col_idx in enumerate(layout.fy26_cols):
        header_map[col_idx] = f"M{idx + 1} FY{first_fy} ({month_labels[idx]})"
    for idx, col_idx in enumerate(layout.fy27_cols):
        header_map[col_idx] = f"M{idx + 1} FY{second_fy} ({month_labels[idx]})"
    rows.append(_row_with_values(layout, header_map))
    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "ACTIVITY TARGETS (DRIVERS)"}))

    nl_target_row = len(rows) + 1
    rows.append(_activity_target_row(layout, "Direct NL", "ACV", "NL Target (DRIVER)", fy26_tab, fy27_tab, "$B$5"))
    upsell_target_row = len(rows) + 1
    rows.append(_activity_target_row(layout, "Direct Upsell", "ACV", "Upsell Target (DRIVER)", fy26_tab, fy27_tab, "$B$6"))
    total_target_row = len(rows) + 1
    rows.append(_total_target_row(layout, nl_target_row, upsell_target_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "NL ACTIVITY WATERFALL"}))

    nl_disco_row = len(rows) + 1
    rows.append(_nl_activity_row(layout, "Disco Calls Needed", nl_target_row, fy26_tab, fy27_tab, include_disco=True))
    nl_pipeline_row = len(rows) + 1
    rows.append(_nl_activity_row(layout, "Pipeline Opps Needed", nl_target_row, fy26_tab, fy27_tab, include_pipe=True))
    nl_deals_row = len(rows) + 1
    rows.append(_nl_activity_row(layout, "C/W Deals Needed", nl_target_row, fy26_tab, fy27_tab))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "UPSELL ACTIVITY WATERFALL"}))

    upsell_pipeline_row = len(rows) + 1
    rows.append(_upsell_activity_row(layout, "Pipeline Opps Needed", upsell_target_row, fy26_tab, fy27_tab, include_pipe=True))
    upsell_deals_row = len(rows) + 1
    rows.append(_upsell_activity_row(layout, "C/W Deals Needed", upsell_target_row, fy26_tab, fy27_tab))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "ACTIVITY TOTALS"}))

    total_disco_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total Disco Calls", nl_disco_row, None))
    total_pipeline_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total Pipeline Opps", nl_pipeline_row, upsell_pipeline_row))
    total_deals_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total C/W Deals", nl_deals_row, upsell_deals_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "COHORT BOOKINGS (Lagged Recognition)"}))

    nl_lagged_row = len(rows) + 1
    rows.append(_lagged_row(layout, "Direct NL", "Lagged C/W Deals", nl_deals_row, fy26_tab, fy27_tab, lag_cell="$B$12", lag_value=str(inputs.nl_lag_months)))
    upsell_lagged_row = len(rows) + 1
    rows.append(_lagged_row(layout, "Direct Upsell", "Lagged C/W Deals", upsell_deals_row, fy26_tab, fy27_tab, lag_cell="$B$16", lag_value=str(inputs.upsell_lag_months)))
    total_lagged_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total Lagged Deals", nl_lagged_row, upsell_lagged_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "REALIZED BOOKINGS"}))

    nl_realized_row = len(rows) + 1
    rows.append(_realized_row(layout, "Direct NL", "NL Bookings Realized", nl_lagged_row, fy26_tab, fy27_tab, asp_cell="$B$11"))
    upsell_realized_row = len(rows) + 1
    rows.append(_realized_row(layout, "Direct Upsell", "Upsell Bookings Realized", upsell_lagged_row, fy26_tab, fy27_tab, asp_cell="$B$15"))
    total_realized_row = len(rows) + 1
    rows.append(_total_activity_row(layout, "Total Bookings Realized", nl_realized_row, upsell_realized_row, category="ACV"))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "SDR CAPACITY (Productivity-Driven)"}))

    sdr_productivity_row = len(rows) + 1
    rows.append(_capacity_input_row(layout, "SDR", layout.sdr_productivity_metric, fy26_tab, fy27_tab, MONTHLY_LAYOUT.sdr_productivity_cell))
    sdr_required_row = len(rows) + 1
    rows.append(_ceiling_row(layout, "SDR", "SDRs Required", total_disco_row, sdr_productivity_row))
    sdr_capacity_row = len(rows) + 1
    rows.append(_product_row(layout, "SDR", layout.sdr_capacity_metric, sdr_required_row, sdr_productivity_row))
    rows.append(_difference_row(layout, "SDR", "Capacity vs Demand (should be +)", sdr_capacity_row, total_disco_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "AE CAPACITY (Productivity-Driven)"}))

    ae_productivity_row = len(rows) + 1
    rows.append(_capacity_input_row(layout, "AE", "AE Productivity ($/month/ramped)", fy26_tab, fy27_tab, MONTHLY_LAYOUT.ae_productivity_cell, category="ACV"))
    ae_required_row = len(rows) + 1
    rows.append(_ceiling_row(layout, "AE", "Ramped AEs Required", total_target_row, ae_productivity_row, category="Count"))
    ae_capacity_row = len(rows) + 1
    rows.append(_product_row(layout, "AE", layout.ae_capacity_metric, ae_required_row, ae_productivity_row, category="ACV"))
    rows.append(_difference_row(layout, "AE", "Capacity vs Target (should be +)", ae_capacity_row, total_target_row, category="ACV"))
    deals_per_ae_row = len(rows) + 1
    rows.append(_ratio_row(layout, "AE", "C/W Deals per Ramped AE", total_deals_row, ae_required_row))

    rows.append(_blank_row(layout))
    rows.append(_row_with_values(layout, {0: "SE CAPACITY (Derived from AE Requirements)"}))

    se_ratio_row = len(rows) + 1
    rows.append(_capacity_input_row(layout, "SE", "AE to SE Ratio (DRIVER)", fy26_tab, fy27_tab, "$B$19", category="Ratio"))
    se_required_row = len(rows) + 1
    rows.append(_ceiling_row(layout, "SE", "SEs Required", ae_required_row, se_ratio_row, category="Count"))
    poc_required_row = len(rows) + 1
    rows.append(_poc_required_row(layout, total_pipeline_row, fy26_tab, fy27_tab))
    rows.append(_ratio_row(layout, "SE", "PoCs per SE", poc_required_row, se_required_row))

    return rows


# ---------------------------------------------------------------------------
# Assumptions rows
# ---------------------------------------------------------------------------


def _assumptions_rows(inputs: WaterfallInputs, growth: float) -> List[List[Any]]:
    pacing = inputs.quarterly_pacing or DEFAULT_PACING
    target_value = (inputs.annual_target or 0) * growth

    rows: List[List[Any]] = []
    header = [""] * len(QUARTERLY_COLUMN_LETTERS)
    header[0:3] = ["Metric", "Value", "Notes"]
    header[4:8] = ["Q1 %", "Q2 %", "Q3 %", "Q4 %"]
    header[8] = "Pacing Sum"
    rows.append(header)

    pacing_row = [""] * len(QUARTERLY_COLUMN_LETTERS)
    pacing_row[0:3] = ["Annual NB Revenue Target", target_value, ""]
    pacing_row[4:8] = pacing
    pacing_row[8] = "=SUM(E2:H2)"
    rows.append(pacing_row)

    def r(metric: str, val: Any, notes: str = "") -> List[Any]:
        row = [""] * len(QUARTERLY_COLUMN_LETTERS)
        row[0:3] = [metric, val, notes]
        return row

    rows.append(r("% Attribution New Logo", inputs.nl_attribution))
    rows.append(r("% Attribution Upsell", inputs.upsell_attribution))
    rows.append(r("$ Attribution New Logo", "=B2*B3", "Formula"))
    rows.append(r("$ Attribution Upsell", "=B2*B4", "Formula"))
    rows.append(r("Multiplier on Bookings", inputs.multiplier, "Buffer/stretch"))
    rows.append([""] * len(QUARTERLY_COLUMN_LETTERS))
    rows.append(r("Disco to Pipe Rate", inputs.disco_to_pipe_rate, "Percent of discovery calls that convert to pipeline"))
    rows.append(r("New Logo Win Rate", inputs.nl_win_rate))
    rows.append(r("New Logo ASP", inputs.nl_asp))
    rows.append(r("New Logo Lag (quarters)", inputs.nl_lag, "Sales cycle"))
    rows.append([""] * len(QUARTERLY_COLUMN_LETTERS))
    rows.append(r("Upsell Win Rate", inputs.upsell_win_rate))
    rows.append(r("Upsell ASP", inputs.upsell_asp))
    rows.append(r("Upsell Lag (quarters)", inputs.upsell_lag))
    rows.append([""] * len(QUARTERLY_COLUMN_LETTERS))
    rows.append(r("PoC Rate", inputs.poc_rate, "Percent of opps needing PoC"))
    rows.append(r("AE to SE Ratio", inputs.ae_to_se_ratio))
    rows.append([""] * len(QUARTERLY_COLUMN_LETTERS))
    rows.append(r("SDR Productivity (disco calls/month)", inputs.sdr_productivity, "Driver"))
    rows.append(r("SDR Productivity (disco calls/qtr)", "=B21*3", "Calculated"))
    rows.append(r("AE Productivity ($/qtr/ramped)", inputs.ae_productivity, "Driver"))
    return rows


def _assumptions_rows_monthly(inputs: WaterfallInputs, growth: float) -> List[List[Any]]:
    pacing = inputs.monthly_pacing or DEFAULT_MONTHLY_PACING
    target_value = (inputs.annual_target or 0) * growth

    rows: List[List[Any]] = []
    header = [""] * len(MONTHLY_COLUMN_LETTERS)
    header[0:3] = ["Metric", "Value", "Notes"]
    for idx in range(12):
        header[4 + idx] = f"M{idx+1} %"
    header[16] = "Pacing Sum"
    rows.append(header)

    pacing_row = [""] * len(MONTHLY_COLUMN_LETTERS)
    pacing_row[0:3] = ["Annual NB Revenue Target", target_value, ""]
    for idx, val in enumerate(pacing):
        pacing_row[4 + idx] = val
    pacing_row[16] = "=SUM(E2:P2)"
    rows.append(pacing_row)

    def r(metric: str, val: Any, notes: str = "") -> List[Any]:
        row = [""] * len(MONTHLY_COLUMN_LETTERS)
        row[0:3] = [metric, val, notes]
        return row

    rows.append(r("% Attribution New Logo", inputs.nl_attribution))
    rows.append(r("% Attribution Upsell", inputs.upsell_attribution))
    rows.append(r("$ Attribution New Logo", "=B2*B3", "Formula"))
    rows.append(r("$ Attribution Upsell", "=B2*B4", "Formula"))
    rows.append(r("Multiplier on Bookings", inputs.multiplier, "Buffer/stretch"))
    rows.append([""] * len(MONTHLY_COLUMN_LETTERS))
    rows.append(r("Disco to Pipe Rate", inputs.disco_to_pipe_rate, "Percent of discovery calls that convert to pipeline"))
    rows.append(r("New Logo Win Rate", inputs.nl_win_rate))
    rows.append(r("New Logo ASP", inputs.nl_asp))
    rows.append(r("New Logo Lag (months)", inputs.nl_lag_months, "Sales cycle"))
    rows.append([""] * len(MONTHLY_COLUMN_LETTERS))
    rows.append(r("Upsell Win Rate", inputs.upsell_win_rate))
    rows.append(r("Upsell ASP", inputs.upsell_asp))
    rows.append(r("Upsell Lag (months)", inputs.upsell_lag_months))
    rows.append([""] * len(MONTHLY_COLUMN_LETTERS))
    rows.append(r("PoC Rate", inputs.poc_rate, "Percent of opps needing PoC"))
    rows.append(r("AE to SE Ratio", inputs.ae_to_se_ratio))
    rows.append([""] * len(MONTHLY_COLUMN_LETTERS))
    rows.append(r("SDR Productivity (disco calls/month)", inputs.sdr_productivity, "Driver"))
    rows.append(r("SDR Productivity (disco calls/qtr)", "=B21*3", "Calculated"))
    rows.append(r("AE Productivity ($/qtr/ramped)", inputs.ae_productivity, "Driver"))
    rows.append(r("AE Productivity ($/month/ramped)", "=B23/3", "Calculated"))
    return rows


# ---------------------------------------------------------------------------
# Row helpers
# ---------------------------------------------------------------------------


def _activity_target_row(
    layout: WaterfallLayout, type_label: str, category: str, metric: str, fy26_tab: str, fy27_tab: str, attribution_cell: str
) -> List[Any]:
    row = _blank_row(layout)
    row[0] = type_label
    row[1] = category
    row[2] = metric
    for idx, col_idx in enumerate(layout.fy26_cols):
        pacing_col = layout.pacing_cols[idx % len(layout.pacing_cols)]
        row[col_idx] = _target_formula(fy26_tab, attribution_cell, pacing_col)
    for idx, col_idx in enumerate(layout.fy27_cols):
        pacing_col = layout.pacing_cols[idx % len(layout.pacing_cols)]
        row[col_idx] = _target_formula(fy27_tab, attribution_cell, pacing_col)
    return row


def _nl_activity_row(
    layout: WaterfallLayout,
    metric: str,
    target_row: int,
    fy26_tab: str,
    fy27_tab: str,
    include_disco: bool = False,
    include_pipe: bool = False,
) -> List[Any]:
    row = _blank_row(layout)
    row[0] = "Direct NL"
    row[1] = "Quantity"
    row[2] = metric
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        tab = fy26_tab if col_idx in layout.fy26_cols else fy27_tab
        target_cell = _cell(layout, col_idx, target_row)
        if include_disco:
            row[col_idx] = f"={target_cell}/{_sheet_cell(tab, '$B$11')}/{_sheet_cell(tab, '$B$10')}/{_sheet_cell(tab, '$B$9')}"
        elif include_pipe:
            row[col_idx] = f"={target_cell}/{_sheet_cell(tab, '$B$11')}/{_sheet_cell(tab, '$B$10')}"
        else:
            row[col_idx] = f"={target_cell}/{_sheet_cell(tab, '$B$11')}"
    return row


def _upsell_activity_row(
    layout: WaterfallLayout, metric: str, target_row: int, fy26_tab: str, fy27_tab: str, include_pipe: bool = False
) -> List[Any]:
    row = _blank_row(layout)
    row[0] = "Direct Upsell"
    row[1] = "Quantity"
    row[2] = metric
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        tab = fy26_tab if col_idx in layout.fy26_cols else fy27_tab
        target_cell = _cell(layout, col_idx, target_row)
        if include_pipe:
            row[col_idx] = f"={target_cell}/{_sheet_cell(tab, '$B$15')}/{_sheet_cell(tab, '$B$14')}"
        else:
            row[col_idx] = f"={target_cell}/{_sheet_cell(tab, '$B$15')}"
    return row


def _total_activity_row(layout: WaterfallLayout, metric: str, primary_row: int, secondary_row: Optional[int], category: str = "Quantity") -> List[Any]:
    row = _blank_row(layout)
    row[0] = "Total"
    row[1] = category
    row[2] = metric
    for col_idx in layout.time_cols:
        base = _cell(layout, col_idx, primary_row)
        if secondary_row:
            row[col_idx] = f"={base}+{_cell(layout, col_idx, secondary_row)}"
        else:
            row[col_idx] = f"={base}"
    return row


def _lagged_row(
    layout: WaterfallLayout,
    type_label: str,
    metric: str,
    source_row: int,
    fy26_tab: str,
    fy27_tab: str,
    lag_cell: str,
    lag_value: str,
) -> List[Any]:
    row = _blank_row(layout)
    row[0] = type_label
    row[1] = "Quantity"
    row[2] = metric
    row[3] = lag_value
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        tab = fy26_tab if col_idx in layout.fy26_cols else fy27_tab
        row[col_idx] = f"=IFERROR(OFFSET({_cell(layout, col_idx, source_row)},0,-{_sheet_cell(tab, lag_cell)}),0)"
    return row


def _realized_row(
    layout: WaterfallLayout, type_label: str, metric: str, source_row: int, fy26_tab: str, fy27_tab: str, asp_cell: str
) -> List[Any]:
    row = _blank_row(layout)
    row[0] = type_label
    row[1] = "ACV"
    row[2] = metric
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        tab = fy26_tab if col_idx in layout.fy26_cols else fy27_tab
        row[col_idx] = f"={_cell(layout, col_idx, source_row)}*{_sheet_cell(tab, asp_cell)}"
    return row


def _capacity_input_row(
    layout: WaterfallLayout, type_label: str, metric: str, fy26_tab: str, fy27_tab: str, assumption_cell: str, category: str = "Quantity"
) -> List[Any]:
    row = _blank_row(layout)
    row[0] = type_label
    row[1] = category
    row[2] = metric
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        tab = fy26_tab if col_idx in layout.fy26_cols else fy27_tab
        row[col_idx] = f"={_sheet_cell(tab, assumption_cell)}"
    return row


def _ceiling_row(layout: WaterfallLayout, type_label: str, metric: str, numerator_row: int, denominator_row: int, category: str = "Count") -> List[Any]:
    row = _blank_row(layout)
    row[0] = type_label
    row[1] = category
    row[2] = metric
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        row[col_idx] = f"=CEILING({_cell(layout, col_idx, numerator_row)}/{_cell(layout, col_idx, denominator_row)})"
    return row


def _product_row(
    layout: WaterfallLayout, type_label: str, metric: str, factor_row: int, multiplier_row: int, category: str = "Quantity"
) -> List[Any]:
    row = _blank_row(layout)
    row[0] = type_label
    row[1] = category
    row[2] = metric
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        row[col_idx] = f"={_cell(layout, col_idx, factor_row)}*{_cell(layout, col_idx, multiplier_row)}"
    return row


def _difference_row(
    layout: WaterfallLayout, type_label: str, metric: str, capacity_row: int, demand_row: int, category: str = "Quantity"
) -> List[Any]:
    row = _blank_row(layout)
    row[0] = type_label
    row[1] = category
    row[2] = metric
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        row[col_idx] = f"={_cell(layout, col_idx, capacity_row)}-{_cell(layout, col_idx, demand_row)}"
    return row


def _ratio_row(
    layout: WaterfallLayout, type_label: str, metric: str, numerator_row: int, denominator_row: int, category: str = "Quantity"
) -> List[Any]:
    row = _blank_row(layout)
    row[0] = type_label
    row[1] = category
    row[2] = metric
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        row[col_idx] = f"=IFERROR({_cell(layout, col_idx, numerator_row)}/{_cell(layout, col_idx, denominator_row)},0)"
    return row


def _poc_required_row(layout: WaterfallLayout, total_pipeline_row: int, fy26_tab: str, fy27_tab: str) -> List[Any]:
    row = _blank_row(layout)
    row[0] = "SE"
    row[1] = "Quantity"
    row[2] = "PoCs Required"
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        tab = fy26_tab if col_idx in layout.fy26_cols else fy27_tab
        row[col_idx] = f"={_cell(layout, col_idx, total_pipeline_row)}*{_sheet_cell(tab, '$B$18')}"
    return row


def _total_target_row(layout: WaterfallLayout, nl_row: int, upsell_row: int) -> List[Any]:
    row = _blank_row(layout)
    row[0] = "Total"
    row[1] = "ACV"
    row[2] = "Total Target"
    for col_idx in layout.fy26_cols + layout.fy27_cols:
        row[col_idx] = f"={_cell(layout, col_idx, nl_row)}+{_cell(layout, col_idx, upsell_row)}"
    return row


def _target_formula(tab_name: str, attribution_cell: str, pacing_col: str) -> str:
    return f"={_sheet_cell(tab_name, attribution_cell)}*{_sheet_cell(tab_name, pacing_col + '$2')}*{_sheet_cell(tab_name, '$B$7')}"


def _row_with_values(layout: WaterfallLayout, cells: Dict[int, Any]) -> List[Any]:
    row = _blank_row(layout)
    for idx, val in cells.items():
        row[idx] = val
    return row


def _blank_row(layout: WaterfallLayout) -> List[Any]:
    return [""] * len(layout.column_letters)


# ---------------------------------------------------------------------------
# Sheets utilities
# ---------------------------------------------------------------------------


def _write_rows(
    service,
    spreadsheet_id: str,
    tab_name: str,
    rows: List[List[Any]],
    column_letters: List[str],
    replace: bool = False,
) -> None:
    sheet_id = _ensure_sheet(service, spreadsheet_id, tab_name, replace=replace)
    end_col = column_letters[-1]
    range_ref = f"{_quote_tab(tab_name)}!A1"
    service.spreadsheets().values().clear(spreadsheetId=spreadsheet_id, range=f"{_quote_tab(tab_name)}!A1:{end_col}100").execute()
    service.spreadsheets().values().update(
        spreadsheetId=spreadsheet_id,
        range=range_ref,
        valueInputOption="USER_ENTERED",
        body={"values": rows},
    ).execute()
    return sheet_id


def _ensure_sheet(service, spreadsheet_id: str, tab_name: str, replace: bool = False) -> int:
    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:
            if replace:
                sheet_id = int(props.get("sheetId"))
                service.spreadsheets().batchUpdate(
                    spreadsheetId=spreadsheet_id,
                    body={"requests": [{"deleteSheet": {"sheetId": sheet_id}}]},
                ).execute()
                break
            return int(props.get("sheetId"))

    add_request = {"addSheet": {"properties": {"title": tab_name}}}
    resp = service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body={"requests": [add_request]}).execute()
    replies = resp.get("replies", []) if isinstance(resp, dict) else []
    if replies:
        props = replies[0].get("addSheet", {}).get("properties", {})
        return int(props.get("sheetId"))
    raise RuntimeError(f"Failed to create tab '{tab_name}'.")


def _apply_waterfall_style(service, spreadsheet_id: str, tab_name: str, column_letters: List[str]) -> None:
    sheet_id = _get_sheet_id(service, spreadsheet_id, tab_name)
    header_range = {
        "sheetId": sheet_id,
        "startRowIndex": 0,
        "endRowIndex": 1,
        "startColumnIndex": 0,
        "endColumnIndex": len(column_letters),
    }

    # Data columns start at column D (index 3) for lag, E onwards for time periods
    data_start_col = 4  # Column E (first time period)
    data_end_col = len(column_letters)

    requests = [
        {
            "updateSheetProperties": {
                "properties": {"sheetId": sheet_id, "gridProperties": {"frozenRowCount": 1, "frozenColumnCount": 3}},
                "fields": "gridProperties.frozenRowCount,gridProperties.frozenColumnCount",
            }
        },
        {
            "repeatCell": {
                "range": header_range,
                "cell": {
                    "userEnteredFormat": {
                        "backgroundColor": _hex_to_color("#0f172a"),
                        "textFormat": {"bold": True, "foregroundColor": _hex_to_color("#ffffff")},
                    }
                },
                "fields": "userEnteredFormat(backgroundColor,textFormat)",
            }
        },
    ]
    service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body={"requests": requests}).execute()

    # Now apply number formatting based on row categories
    # Read the Category column (B) to determine formatting
    resp = service.spreadsheets().values().get(
        spreadsheetId=spreadsheet_id,
        range=f"'{tab_name}'!B1:B60",
        valueRenderOption="FORMATTED_VALUE"
    ).execute()
    categories = [row[0] if row else "" for row in resp.get("values", [])]

    # Build formatting requests based on categories
    format_requests = []
    for row_idx, category in enumerate(categories):
        if not category or row_idx == 0:  # Skip header and empty rows
            continue

        cat_lower = category.lower().strip()
        if cat_lower == "acv":
            # Currency format for ACV rows
            format_requests.append(_number_format_request(
                sheet_id, row_idx, row_idx + 1, data_start_col, data_end_col,
                {"type": "CURRENCY", "pattern": "$#,##0"}
            ))
        elif cat_lower in ("quantity", "count"):
            # Whole number format for quantity/count rows
            format_requests.append(_number_format_request(
                sheet_id, row_idx, row_idx + 1, data_start_col, data_end_col,
                {"type": "NUMBER", "pattern": "#,##0"}
            ))
        elif cat_lower == "ratio":
            # Decimal format for ratios
            format_requests.append(_number_format_request(
                sheet_id, row_idx, row_idx + 1, data_start_col, data_end_col,
                {"type": "NUMBER", "pattern": "#,##0.0"}
            ))

    if format_requests:
        service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body={"requests": format_requests}).execute()


def _number_format_request(sheet_id: int, start_row: int, end_row: int, start_col: int, end_col: int, number_format: Dict[str, str]) -> Dict[str, Any]:
    """Create a repeatCell request for number formatting."""
    return {
        "repeatCell": {
            "range": {
                "sheetId": sheet_id,
                "startRowIndex": start_row,
                "endRowIndex": end_row,
                "startColumnIndex": start_col,
                "endColumnIndex": end_col,
            },
            "cell": {
                "userEnteredFormat": {
                    "numberFormat": number_format
                }
            },
            "fields": "userEnteredFormat.numberFormat",
        }
    }


def _validate_waterfall(
    service,
    spreadsheet_id: str,
    tab_name: str,
    layout: WaterfallLayout,
    target_metric: str,
    realized_metric: str,
    sdr_capacity_metric: str,
    sdr_demand_metric: str,
    ae_capacity_metric: str,
) -> Dict[str, Any]:
    resp = (
        service.spreadsheets()
        .values()
        .get(
            spreadsheetId=spreadsheet_id,
            range=f"{_quote_tab(tab_name)}!A1:{layout.end_column}60",
            valueRenderOption="UNFORMATTED_VALUE",
            majorDimension="ROWS",
        )
        .execute()
    )
    values: List[List[Any]] = resp.get("values", []) or []
    metrics: Dict[str, int] = {}
    for idx, row in enumerate(values):
        if len(row) > 2 and row[2]:
            metrics[str(row[2])] = idx

    errors: List[str] = []
    target_row_idx = metrics.get(target_metric)
    realized_row_idx = metrics.get(realized_metric)
    if target_row_idx is not None and realized_row_idx is not None:
        for col_idx in layout.fy26_cols + layout.fy27_cols:
            target_val = _numeric_cell(values, target_row_idx, col_idx)
            realized_val = _numeric_cell(values, realized_row_idx, col_idx)
            if target_val is None or realized_val is None:
                continue
            if abs(target_val - realized_val) > 1:
                errors.append(f"{layout.column_letters[col_idx]}: Target {target_val} != Realized {realized_val}")

    sdr_capacity_idx = metrics.get(sdr_capacity_metric)
    total_disco_idx = metrics.get(sdr_demand_metric)
    if sdr_capacity_idx is not None and total_disco_idx is not None:
        for col_idx in layout.fy26_cols + layout.fy27_cols:
            cap = _numeric_cell(values, sdr_capacity_idx, col_idx) or 0
            demand = _numeric_cell(values, total_disco_idx, col_idx) or 0
            if cap < demand:
                errors.append(f"{layout.column_letters[col_idx]}: SDR capacity {cap} < demand {demand}")

    ae_capacity_idx = metrics.get(ae_capacity_metric)
    if ae_capacity_idx is not None and target_row_idx is not None:
        for col_idx in layout.fy26_cols + layout.fy27_cols:
            cap = _numeric_cell(values, ae_capacity_idx, col_idx) or 0
            target = _numeric_cell(values, target_row_idx, col_idx) or 0
            if cap < target:
                errors.append(f"{layout.column_letters[col_idx]}: AE capacity {cap} < target {target}")

    return {"status": "ok" if not errors else "issues", "errors": errors}


def _numeric_cell(values: List[List[Any]], row_idx: int, col_idx: int) -> Optional[float]:
    try:
        val = values[row_idx][col_idx]
    except IndexError:
        return None
    if isinstance(val, (int, float)):
        return float(val)
    try:
        return float(val)
    except (TypeError, ValueError):
        return None


def _get_sheet_id(service, spreadsheet_id: str, tab_name: str) -> int:
    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 int(props.get("sheetId"))
    raise RuntimeError(f"Tab '{tab_name}' not found.")


def _hex_to_color(hex_value: str) -> Dict[str, float]:
    value = hex_value.lstrip("#")
    if len(value) == 3:
        value = "".join(ch * 2 for ch in value)
    if len(value) != 6:
        return {"red": 1, "green": 1, "blue": 1}
    r = int(value[0:2], 16) / 255.0
    g = int(value[2:4], 16) / 255.0
    b = int(value[4:6], 16) / 255.0
    return {"red": r, "green": g, "blue": b}


def _cell(layout: WaterfallLayout, col_idx: int, row_num: int) -> str:
    return f"{layout.column_letters[col_idx]}{row_num}"


def _sheet_cell(tab_name: str, cell_ref: str) -> str:
    return f"{_quote_tab(tab_name)}!{cell_ref}"


def _quote_tab(tab_name: str) -> str:
    escaped = tab_name.replace("'", "''")
    return f"'{escaped}'"


def _quarter_labels(fy_start_month: int) -> List[str]:
    """
    Return four quarter labels with month ranges based on fiscal start.
    """
    month_abbr = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
    start_idx = (fy_start_month - 1) % 12
    labels: List[str] = []
    for q in range(4):
        first = (start_idx + q * 3) % 12
        last = (first + 2) % 12
        labels.append(f"({month_abbr[first]}-{month_abbr[last]})")
    return labels


def _month_labels(fy_start_month: int) -> List[str]:
    """
    Return month labels ordered by fiscal start month.
    """
    month_abbr = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
    start_idx = (fy_start_month - 1) % 12
    return [month_abbr[(start_idx + i) % 12] for i in range(12)]


def _validate_pacing(label: str, values: List[float], expected_len: int) -> List[float]:
    if len(values) != expected_len:
        raise ValueError(f"{label} must contain {expected_len} values.")
    cast = [float(v) for v in values]
    total = sum(cast)
    if abs(total - 1.0) > 0.001:
        raise ValueError(f"{label} must sum to 1.0 (got {total:.4f}).")
    return cast


__all__ = ["WaterfallInputs", "build_waterfall"]
