from __future__ import annotations

import logging
import threading
from typing import Any

import polars as pl

from .config_helpers import _resilience_cfg
from .dead_letter import (
    _niffler_dead_letter,
    _niffler_immediate_replay,
    _niffler_notify_dead_letter,
)

logger = logging.getLogger(__name__)


def _write_bars_batch_to_clickhouse(
    symbol: str,
    threshold: int,
    bars: pl.DataFrame | list[dict],
    *,
    cache: object | None = None,
    ouroboros_mode: str = "aion",
) -> None:
    """Write a batch of completed bars to ClickHouse.

    Issue #96 Task #144 Phase 3: Streaming latency optimization.
    Issue #117: Accepts optional cached OpenDeviationBarCache instance.
    ALLOC-02: Accepts pl.DataFrame directly (skips dict->DataFrame conversion).
    ALLOC-03: ouroboros_mode parameter replaces per-flush get_symbol_entries() lookup.
    """
    from opendeviationbar.clickhouse.cache import OpenDeviationBarCache

    # ALLOC-02: Skip conversion when already a DataFrame
    if isinstance(bars, list):
        if not bars:
            return
        bars_df = pl.DataFrame(bars)
    else:
        bars_df = bars
        if bars_df.is_empty():
            return

    # Metadata keys (_symbol, _threshold) already .pop()'d at call site

    # Issue #158: Stathera source tagging
    from opendeviationbar import __version__

    source_version = f"{__version__}+sidecar"

    if cache is not None:
        cache.store_bars_batch(
            symbol=symbol,
            threshold_decimal_bps=threshold,
            bars=bars_df,
            version=source_version,
            overlap_strategy="off",
            ouroboros_mode=ouroboros_mode,
        )
    else:
        with OpenDeviationBarCache() as ch_cache:
            ch_cache.store_bars_batch(
                symbol=symbol,
                threshold_decimal_bps=threshold,
                bars=bars_df,
                version=source_version,
                overlap_strategy="off",
                ouroboros_mode=ouroboros_mode,
            )


def _write_batch_with_retry(
    symbol: str,
    threshold: int,
    bars: pl.DataFrame | list[dict[str, Any]],
    *,
    cache: object | None = None,
    ouroboros_mode: str = "aion",
) -> bool:
    """Write a batch to ClickHouse with stamina retry + dead-letter fallback.

    Returns True if write succeeded, False if all retries exhausted (dead-lettered).
    Issue #117: Accepts optional cached OpenDeviationBarCache.
    ALLOC-02: Accepts pl.DataFrame directly (no conversion needed).
    ALLOC-03: ouroboros_mode parameter threaded from caller.
    Charon: Uses stamina for production-grade retry (replaces hand-rolled loop).
    """
    import stamina

    try:

        @stamina.retry(
            on=(OSError, RuntimeError, ConnectionError, ValueError),
            attempts=_resilience_cfg().flush_max_retries,
            timeout=30.0,
        )
        def _stamina_write() -> None:
            _write_bars_batch_to_clickhouse(
                symbol, threshold, bars, cache=cache, ouroboros_mode=ouroboros_mode
            )

        _stamina_write()
    except (OSError, RuntimeError, ConnectionError, ValueError):
        # All retries exhausted -- dead-letter
        # Dead-letter needs list[dict] for Parquet serialization
        bar_dicts = bars.to_dicts() if isinstance(bars, pl.DataFrame) else bars
        logger.exception(
            "flush FAILED after %d retries for %s@%d (%d bars) -- writing dead-letter",
            _resilience_cfg().flush_max_retries,
            symbol,
            threshold,
            len(bar_dicts),
        )
        dl_path = _niffler_dead_letter(symbol, threshold, bar_dicts)
        if dl_path is not None:
            _niffler_notify_dead_letter(symbol, threshold, len(bar_dicts), dl_path)
            # Niffler: spawn immediate replay in daemon thread (non-blocking)
            threading.Thread(
                target=_niffler_immediate_replay,
                name="niffler-replay",
                daemon=True,
            ).start()
        return False
    else:
        return True
