"""Kintsugi daemon: long-running adaptive polling loop."""

from __future__ import annotations

import logging
import os
import time

from ._helpers import _kintsugi_cfg
from .discovery import KintsugiResult
from .orchestrator import kintsugi_pass
from .telemetry import _sd_notify

logger = logging.getLogger(__name__)


def _trailing_edge_fill(
    *,
    symbol: str | None = None,
    threshold: int | None = None,
    include_microstructure: bool = True,
) -> int:
    """Fill trailing edge: latest cached bar → now for all pairs.

    Absorbed from the former gap-backfill daemon (#238 Phase 2).
    Uses Vision-first + REST fallback via backfill_all_gaps().

    Writer Ownership (#280, Spike A): Streaming symbols (BTCUSDT, ETHUSDT)
    are SKIPPED — the sidecar owns their trailing edge. Without this guard,
    kintsugi's fresh processor produces bars with different first_agg_trade_id
    than the sidecar, causing persistent overlaps that grow every pass.
    Empirically validated 2026-03-19: 39 overlaps → 0.
    """
    try:
        from opendeviationbar.backfill import backfill_all_gaps, backfill_gap

        # (#280 Spike A) Skip streaming symbols — sidecar owns their trailing edge.
        # SSoT: symbols.toml (enabled=true) via SidecarConfig default.
        # Env var OPENDEVIATIONBAR_STREAMING_SYMBOLS overrides if set.
        env_syms = os.environ.get("OPENDEVIATIONBAR_STREAMING_SYMBOLS", "")
        if env_syms.strip():
            streaming_symbols = {s.strip() for s in env_syms.split(",") if s.strip()}
        else:
            # No env var override — read from registry (same SSoT as sidecar)
            from opendeviationbar.symbol_registry import get_registered_symbols
            streaming_symbols = set(get_registered_symbols(enabled_only=True))

        if symbol and threshold:
            if symbol in streaming_symbols:
                logger.debug(
                    "trailing-edge fill: SKIP %s@%d (sidecar-owned streaming symbol)",
                    symbol,
                    threshold,
                )
                return 0
            result = backfill_gap(
                symbol,
                threshold,
                include_microstructure=include_microstructure,
            )
            bars = result.bars_written
        else:
            results = backfill_all_gaps(
                include_microstructure=include_microstructure,
                exclude_symbols=streaming_symbols,
            )
            bars = sum(r.bars_written for r in results)
            if streaming_symbols:
                logger.info(
                    "trailing-edge fill: SKIP %d pairs for streaming symbols %s (sidecar-owned)",
                    len(streaming_symbols) * 4,  # approximate: symbols x thresholds
                    sorted(streaming_symbols),
                )

        if bars > 0:
            logger.info("trailing-edge fill: %d bars written", bars)
    except (RuntimeError, OSError, ConnectionError, ValueError) as e:
        logger.warning("trailing-edge fill failed: %s", e)
        return 0
    else:
        return bars


def _periodic_rectification(
    iteration: int,
    *,
    symbol: str | None = None,
    threshold: int | None = None,
) -> None:
    """Run bar rectification every N iterations (~hourly)."""
    if _kintsugi_cfg().rectification_cycle_interval <= 0:
        return
    if iteration % _kintsugi_cfg().rectification_cycle_interval != 0:
        return

    try:
        from opendeviationbar.backfill import (
            _get_cached_symbol_threshold_pairs,
            rectify_recent_bars,
        )

        pairs = _get_cached_symbol_threshold_pairs()
        if symbol:
            pairs = [(s, t) for s, t in pairs if s == symbol]
        if threshold:
            pairs = [(s, t) for s, t in pairs if t == threshold]

        for sym, thr in pairs:
            try:
                rectified = rectify_recent_bars(sym, thr)
                if rectified > 0:
                    logger.info("rectified %d bars for %s@%d", rectified, sym, thr)
            except (RuntimeError, OSError, ConnectionError, ValueError) as e:
                logger.warning("rectification failed for %s@%d: %s", sym, thr, e)
    except (ImportError, RuntimeError) as e:
        logger.warning("rectification skipped: %s", e)


def _pressure_aware_sleep(
    results: list[KintsugiResult],
    trailing_bars: int,
    interval_active_s: int,
    interval_clean_s: int,
    remaining_shards: int = 0,
) -> int:
    """Compute sleep duration adaptively.

    - **Remaining shards > 0**: sleep 0 (continue immediately)
    - **Activity but queue drained**: ``interval_active_s`` (re-discover soon)
    - **Clean (no activity)**: ``interval_clean_s`` (poll infrequently)
    - **Memory pressure**: back off regardless of queue depth
    """
    import gc

    has_activity = any(r.healed or r.error for r in results) or trailing_bars > 0

    if remaining_shards > 0:
        # Work remains — no reason to idle
        sleep_s = 0
    elif has_activity:
        # Queue drained this pass but there was activity — re-discover soon
        sleep_s = interval_active_s
    else:
        # Fully clean — poll infrequently
        sleep_s = interval_clean_s

    # Memory pressure check (ported from gap-backfill)
    try:
        from opendeviationbar.backfill import _check_memory_watermark, _env_int

        memory_max = _env_int("OPENDEVIATIONBAR_MEMORY_MAX_BYTES", 4 * 1024**3)
        watermark = _check_memory_watermark(memory_max)
        if watermark == "gc":
            gc.collect()
            sleep_s = max(sleep_s, 60)  # At least 60s to let GC take effect
            logger.info("memory pressure 'gc' — sleep extended to %ds", sleep_s)
        elif watermark == "abort":
            sleep_s = 900
            logger.warning("memory pressure 'abort' — sleeping 900s for recovery")
    except (ImportError, RuntimeError):
        pass  # Memory check not available, use default sleep

    return sleep_s


def kintsugi_daemon(
    *,
    interval_clean_s: int = 1800,  # SSoT-OK: daemon polling intervals
    interval_active_s: int = 900,  # SSoT-OK: daemon polling intervals
    max_repairs: int | None = None,
    threshold: int | None = None,
    include_microstructure: bool = True,
) -> None:
    """Long-running daemon with adaptive polling.

    Always processes shards in reverse chronological order (newest first).
    Both gaps and overlaps are repaired via day-recompute (DELETE + Ariadne).

    Runs ``kintsugi_pass()`` repeatedly:
    - Every ``interval_clean_s`` when cache is clean (default 30 min)
    - Every ``interval_active_s`` when shards are found (default 15 min)

    The CLI (``scripts/kintsugi.py``) overrides these defaults with aggressive
    intervals (60s active, 300s clean, 10 repairs) for all deployments.

    Stop with Ctrl+C (SIGINT) or SIGTERM.
    """
    if max_repairs is None:
        max_repairs = _kintsugi_cfg().max_repairs_per_pass
    logger.info(
        "kintsugi daemon starting (clean=%ds, active=%ds, max_repairs=%d)",
        interval_clean_s,
        interval_active_s,
        max_repairs,
    )

    # Issue #126: SIGHUP reloads config (enables runtime ouroboros_mode switching)
    import signal

    def _sighup_handler(*_args: object) -> None:
        from opendeviationbar.config import Settings

        Settings.reload_and_clear()
        new_mode = Settings.get().population.ouroboros_mode
        logger.info("SIGHUP: config reloaded (ouroboros_mode=%s)", new_mode)
        # Issue #134: Notify operators of runtime config change
        try:
            from opendeviationbar.notify.telegram import send_telegram

            send_telegram(
                f"<b>SIGHUP: kintsugi config reloaded</b>\n"
                f"<b>ouroboros_mode:</b> {new_mode}\n"
                f"<b>PID:</b> {os.getpid()}",
                disable_notification=True,
            )
        except (ImportError, OSError):
            logger.debug("SIGHUP: Telegram notification failed (non-fatal)")

    signal.signal(signal.SIGHUP, _sighup_handler)

    # Issue #121: Notify on daemon startup (confirms EnvironmentFile worked)
    _sd_notify("READY=1")
    try:
        from opendeviationbar.notify.telegram import _build_context_block, send_telegram

        send_telegram(
            "<b>KINTSUGI STARTED</b>\n"
            f"<b>Interval:</b> clean={interval_clean_s}s, active={interval_active_s}s\n"
            f"<b>Max repairs:</b> {max_repairs}\n"
            f"<b>PID:</b> {os.getpid()}" + _build_context_block("kintsugi"),
            disable_notification=True,
        )
    except (ImportError, OSError, RuntimeError):
        logger.debug("failed to send kintsugi startup Telegram", exc_info=True)

    iteration = 0
    try:
        while True:
            iteration += 1
            logger.info("=== kintsugi iteration %d ===", iteration)

            # Phase 1: Interior shard repairs (Stathera anomalies)
            results = kintsugi_pass(
                threshold=threshold,
                max_repairs=max_repairs,
                include_microstructure=include_microstructure,
            )

            # Issue #117: Ping systemd watchdog after each pass
            _sd_notify("WATCHDOG=1")

            # Phase 2 (#238): Trailing-edge fill (latest bar → now)
            trailing_bars = _trailing_edge_fill(
                symbol=None,
                threshold=threshold,
                include_microstructure=include_microstructure,
            )

            # Phase 2 (#238): Periodic bar rectification (every ~1 hour)
            _periodic_rectification(iteration, symbol=None, threshold=threshold)

            # Adaptive sleep: skip idle time when repair queue has remaining work.
            # If we repaired max_repairs shards, the queue likely has more.
            # Deferred shards (today-guard) don't consume budget — exclude them
            # to avoid a tight loop when all shards touch today.
            actionable = sum(
                1 for r in results if r.error != "deferred_today_guard"
            )
            budget_exhausted = actionable >= max_repairs
            remaining_estimate = max_repairs if budget_exhausted else 0
            sleep_s = _pressure_aware_sleep(
                results, trailing_bars, interval_active_s, interval_clean_s,
                remaining_shards=remaining_estimate,
            )

            if sleep_s > 0:
                logger.info(
                    "kintsugi sleeping %ds (%s)",
                    sleep_s,
                    "active" if (results or trailing_bars > 0) else "clean",
                )
                time.sleep(sleep_s)
            else:
                logger.info("kintsugi: shards remaining — continuing immediately")

    except KeyboardInterrupt:
        logger.info("kintsugi daemon stopped after %d iterations", iteration)
