"""Orchestrator: kintsugi_pass() — the core discover-repair-verify-log cycle."""

from __future__ import annotations

import logging
import time
from datetime import UTC, datetime, timedelta

from opendeviationbar.sidecar.repair_signal import (
    clear_yesterday_signal,
    read_yesterday_signal,
)
from opendeviationbar.symbol_registry import (
    get_registered_symbols,
    get_thresholds_for_symbol,
)

from ._helpers import _kintsugi_cfg
from .discovery import (
    KintsugiResult,
    Shard,
    _select_repair_candidates,
    _shard_has_vision_available,
    _shard_is_parquet_cached,
    _shard_sort_key,
    discover_shards,
)
from .repair import repair_shard
from .telemetry import (
    _emit_telemetry,
    _maybe_log_repair,
    _notify_kintsugi_failures,
    _sd_notify,
)
from .verify import verify_repair

logger = logging.getLogger(__name__)

# Maximum age for a yesterday-repair signal file (hours).
_SIGNAL_MAX_AGE_HOURS = 24.0


def _load_yesterday_priority_shards() -> list[Shard]:
    """Load yesterday-repair signal file and create priority Shard objects.

    Returns an empty list if no signal file exists or it is stale (>24h).
    Each flagged symbol is expanded to all its registered thresholds, creating
    one Shard per (symbol, threshold) pair covering yesterday's full UTC day.

    STARTUP-03: Kintsugi yesterday priority queue.
    """
    signal = read_yesterday_signal()
    if signal is None:
        return []

    # Check staleness
    try:
        ts = datetime.fromisoformat(signal["timestamp"])
        age_hours = (datetime.now(UTC) - ts).total_seconds() / 3600.0
        if age_hours > _SIGNAL_MAX_AGE_HOURS:
            logger.warning(
                "stale yesterday-repair signal (age=%.1fh), discarding",
                age_hours,
            )
            clear_yesterday_signal()
            return []
    except (KeyError, ValueError, TypeError):
        logger.warning("yesterday-repair signal: invalid timestamp, discarding")
        clear_yesterday_signal()
        return []

    flagged_symbols: dict[str, str] = signal.get("symbols", {})
    if not flagged_symbols:
        return []

    # Only process symbols that are enabled in the registry
    enabled = set(get_registered_symbols(enabled_only=True))

    # Compute yesterday's date range
    yesterday = datetime.now(UTC).date() - timedelta(days=1)
    yesterday_start_us = int(
        datetime.combine(yesterday, datetime.min.time(), UTC).timestamp() * 1_000_000
    )
    yesterday_end_us = yesterday_start_us + 86_400_000_000 - 1

    shards: list[Shard] = []
    reasons: list[str] = []

    for symbol, reason in flagged_symbols.items():
        if symbol not in enabled:
            logger.debug(
                "yesterday-repair: skipping %s (not in enabled registry)", symbol
            )
            continue

        thresholds = get_thresholds_for_symbol(symbol)
        for thr in thresholds:
            shards.append(
                Shard(
                    symbol=symbol,
                    threshold_decimal_bps=thr,
                    gap_start_ms=yesterday_start_us,
                    gap_end_ms=yesterday_end_us,
                    gap_hours=24.0,
                    anchor_last_tid=None,
                    resume_first_tid=None,
                    missing_trade_count=0,
                    is_overlap=False,
                    is_genesis=True,  # full-day recompute, not interior gap
                )
            )
        reasons.append(f"{symbol}={reason}")

    if shards:
        logger.info(
            "yesterday-repair signal: loaded %d shards for %d symbols (reasons: %s)",
            len(shards),
            len({s.symbol for s in shards}),
            ", ".join(reasons),
        )

    return shards


def kintsugi_pass(  # noqa: PLR0915
    *,
    dry_run: bool = False,
    threshold: int | None = None,
    max_repairs: int | None = None,
    include_microstructure: bool = True,
    verbose: bool = False,
) -> list[KintsugiResult]:
    """Single pass: discover shards (gaps + overlaps), repair via day-recompute.

    Always processes shards in reverse chronological order (newest first).
    Both gaps (tid_delta > 0) and overlaps (tid_delta < 0) are repaired
    by deleting the affected Ouroboros day(s) and reprocessing from scratch.

    Shard priority is registry-driven: symbols with higher ``kintsugi_priority``
    in ``symbols.toml`` are repaired first (BTCUSDT=10, ETHUSDT=5, default=1).

    Parameters
    ----------
    dry_run
        If True, discover only — no repairs.
    threshold
        Filter to a specific threshold in dbps (e.g., 250).
    max_repairs
        Maximum repairs per pass (default from config).
    include_microstructure
        Include microstructure features in repaired bars.
    verbose
        Enable detailed logging.

    Returns
    -------
    list[KintsugiResult]
        Results for each shard repaired (or discovered in dry_run).
    """
    if max_repairs is None:
        max_repairs = _kintsugi_cfg().max_repairs_per_pass
    pass_t0 = time.monotonic()
    logger.info("kintsugi pass starting (dry_run=%s)", dry_run)
    _emit_telemetry("pass_start", dry_run=dry_run)

    # STARTUP-03: yesterday priority queue — signal file from sidecar
    yesterday_shards = _load_yesterday_priority_shards()

    shards = discover_shards(threshold=threshold)

    # Merge yesterday priority shards with normal discovery
    if yesterday_shards:
        # Deduplicate: remove any shards from normal discovery that match
        # a yesterday priority shard (same symbol + threshold + yesterday date)
        yesterday_keys = {
            (s.symbol, s.threshold_decimal_bps) for s in yesterday_shards
        }
        shards = [s for s in shards if (s.symbol, s.threshold_decimal_bps) not in yesterday_keys]
        # Prepend: yesterday shards go FIRST (before sort reorders normal shards)
        shards = yesterday_shards + shards
        logger.info(
            "yesterday-repair: injected %d priority shards for %s",
            len(yesterday_shards),
            sorted({s.symbol for s in yesterday_shards}),
        )
        _emit_telemetry(
            "yesterday_priority_injected",
            n_shards=len(yesterday_shards),
            symbols=sorted({s.symbol for s in yesterday_shards}),
        )

    if not shards:
        logger.info("kintsugi: no shards found — cache is clean")
        _emit_telemetry("discovery_complete", total_shards=0)
        # Still clear signal if yesterday shards were loaded but all clean
        if yesterday_shards:
            clear_yesterday_signal()
        return []

    # Sort: registry priority first, then recency (SSoT: symbols.toml kintsugi_priority)
    # Note: yesterday shards are prepended before sort, but sort preserves
    # their relative order since they share the same gap_end_ms.
    shards.sort(key=_shard_sort_key)

    _emit_telemetry("discovery_complete", total_shards=len(shards))
    logger.info("kintsugi: discovered %d shards", len(shards))

    if dry_run:
        for s in shards:
            if s.is_genesis:
                anomaly_type = "GENESIS"
                target_day = datetime.fromtimestamp(s.gap_start_ms / 1_000_000, tz=UTC).date()
                trade_info = f"seed {target_day}"
                tid_info = f"resume_first_tid={s.resume_first_tid}"
            else:
                anomaly_type = "OVERLAP" if s.is_overlap else "GAP"
                trade_info = (
                    f"{s.missing_trade_count:,} trades"
                    if s.missing_trade_count > 0
                    else f"gap={s.gap_hours:.1f}h"
                )
                tid_info = (
                    f"tid {s.anchor_last_tid}→{s.resume_first_tid}"
                    if s.anchor_last_tid is not None and s.resume_first_tid is not None
                    else f"ariadne={s.anchor_last_tid is not None}"
                )
            logger.info(
                "  [DRY-RUN] %s@%d %s %s (%s) ouro_splits=%d",
                s.symbol,
                s.threshold_decimal_bps,
                anomaly_type,
                trade_info,
                tid_info,
                len(s.ouroboros_boundaries),
            )
        return []

    results: list[KintsugiResult] = []

    candidates = _select_repair_candidates(shards, max_repairs)

    # 3-tier concurrency control: classify candidates by data source for
    # optimal parallelism. Parquet-cached: local I/O only, highest parallelism.
    # Vision-available: single HTTP GET per day, medium parallelism.
    # REST-only: hundreds of paginated REST calls, rate-limited, low parallelism.
    cfg = _kintsugi_cfg()
    parquet_candidates = [s for s in candidates if _shard_is_parquet_cached(s)]
    remaining = [s for s in candidates if s not in parquet_candidates]
    vision_candidates = [s for s in remaining if _shard_has_vision_available(s)]
    rest_candidates = [s for s in remaining if s not in vision_candidates]

    if rest_candidates:
        logger.info(
            "kintsugi concurrency: %d Parquet, %d Vision, %d REST-only candidates",
            len(parquet_candidates),
            len(vision_candidates),
            len(rest_candidates),
        )

    if not candidates:
        max_concurrent = cfg.max_concurrent_repairs
    elif parquet_candidates and not remaining:
        # All Parquet-cached — highest parallelism
        max_concurrent = cfg.max_concurrent_parquet
        logger.debug(
            "all %d candidates are Parquet-cached -> using max_concurrent_parquet=%d",
            len(candidates),
            max_concurrent,
        )
    elif candidates and all(s in rest_candidates for s in candidates):
        # All REST-only — cap to prevent budget dilution
        max_concurrent = min(cfg.max_concurrent_repairs, cfg.max_concurrent_rest)
        logger.info(
            "all %d candidates are REST-only -> capping to max_concurrent_rest=%d",
            len(candidates),
            max_concurrent,
        )
    elif not rest_candidates:
        # Parquet + Vision mix, no REST — use standard limit
        max_concurrent = cfg.max_concurrent_repairs
    else:
        # Mixed: has REST-only shards among fast shards.
        # Reorder so fast shards execute first, freeing slots before REST starts.
        max_concurrent = cfg.max_concurrent_repairs

    # Reorder: Parquet first, Vision second, REST last.
    # ThreadPoolExecutor processes submissions in order when pool is full,
    # so fast shards complete and free slots before REST shards start.
    candidates = parquet_candidates + vision_candidates + rest_candidates

    if max_concurrent <= 1 or len(candidates) <= 1:
        # Sequential path
        for shard in candidates:
            shard_label = f"{shard.symbol}@{shard.threshold_decimal_bps}"
            _emit_telemetry(
                "repair_start",
                symbol=shard.symbol,
                threshold=shard.threshold_decimal_bps,
                gap_hours=shard.gap_hours,
                gap_end_ms=shard.gap_end_ms,
            )
            result = repair_shard(
                shard,
                include_microstructure=include_microstructure,
                verbose=verbose,
            )

            result_state = (
                "healed"
                if result.healed
                else "clean"
                if not result.error
                else "failed"
            )
            _emit_telemetry(
                "repair_complete",
                symbol=shard.symbol,
                threshold=shard.threshold_decimal_bps,
                result=result_state,
                bars_written=result.bars_written,
                duration_s=round(result.duration_seconds, 1),
                error=result.error,
            )

            # Ping watchdog after each repair to prevent timeout during long repairs
            _sd_notify("WATCHDOG=1")
            _emit_telemetry("watchdog_ping", after_shard=shard_label)

            # Verify repair
            if result.healed:
                verified = verify_repair(shard)
                if not verified:
                    if result.bars_written > 0:
                        # Progress was made — remaining gap will be rediscovered
                        # on next kintsugi pass. Don't mark as failed to avoid
                        # counting toward the 3-attempt rate limit.
                        logger.info(
                            "repair for %s@%d wrote %d bars but gap not fully closed — "
                            "remaining gap will be picked up on next pass",
                            shard.symbol,
                            shard.threshold_decimal_bps,
                            result.bars_written,
                        )
                    else:
                        logger.warning(
                            "repair for %s@%d reported healed but gap persists "
                            "and 0 bars written",
                            shard.symbol,
                            shard.threshold_decimal_bps,
                        )
                        result.healed = False
                        result.error = (
                            "verification failed: gap persists after writing 0 bars"
                        )

            _maybe_log_repair(result)
            results.append(result)
    else:
        # Parallel path: different (symbol, threshold) pairs are fully independent
        # Each shard acquires its own per-pair Redis lock in _repair()
        from concurrent.futures import ThreadPoolExecutor, as_completed

        def _repair_one(shard: Shard) -> KintsugiResult:
            _emit_telemetry(
                "repair_start",
                symbol=shard.symbol,
                threshold=shard.threshold_decimal_bps,
                gap_hours=shard.gap_hours,
                gap_end_ms=shard.gap_end_ms,
            )
            return repair_shard(
                shard,
                include_microstructure=include_microstructure,
                verbose=verbose,
            )

        with ThreadPoolExecutor(
            max_workers=min(max_concurrent, len(candidates)),
            thread_name_prefix="kintsugi-shard",
        ) as executor:
            future_to_shard = {
                executor.submit(_repair_one, shard): shard
                for shard in candidates
            }
            for future in as_completed(future_to_shard):
                shard = future_to_shard[future]
                shard_label = f"{shard.symbol}@{shard.threshold_decimal_bps}"
                try:
                    result = future.result()
                except Exception as exc:
                    logger.exception("shard %s repair failed", shard_label)
                    result = KintsugiResult(
                        shard=shard,
                        healed=False,
                        bars_written=0,
                        duration_seconds=0.0,
                        had_anchor_tid=False,
                        ouroboros_splits=0,
                        error=str(exc),
                    )

                result_state = (
                    "healed"
                    if result.healed
                    else "clean"
                    if not result.error
                    else "failed"
                )
                _emit_telemetry(
                    "repair_complete",
                    symbol=shard.symbol,
                    threshold=shard.threshold_decimal_bps,
                    result=result_state,
                    bars_written=result.bars_written,
                    duration_s=round(result.duration_seconds, 1),
                    error=result.error,
                )

                _sd_notify("WATCHDOG=1")
                _emit_telemetry("watchdog_ping", after_shard=shard_label)

                # Verify repair
                if result.healed:
                    verified = verify_repair(shard)
                    if not verified:
                        if result.bars_written > 0:
                            logger.info(
                                "repair for %s@%d wrote %d bars but gap not fully closed — "
                                "remaining gap will be picked up on next pass",
                                shard.symbol,
                                shard.threshold_decimal_bps,
                                result.bars_written,
                            )
                        else:
                            logger.warning(
                                "repair for %s@%d reported healed but gap persists "
                                "and 0 bars written",
                                shard.symbol,
                                shard.threshold_decimal_bps,
                            )
                            result.healed = False
                            result.error = (
                                "verification failed: gap persists after writing 0 bars"
                            )

                _maybe_log_repair(result)
                results.append(result)

    # Summary (four-state: healed / clean / deferred / failed)
    pass_duration = time.monotonic() - pass_t0
    healed = sum(1 for r in results if r.healed)
    clean = sum(1 for r in results if not r.healed and not r.error)
    deferred = sum(1 for r in results if r.error == "deferred_today_guard")
    failed = sum(1 for r in results if r.error and r.error != "deferred_today_guard")
    total_bars = sum(r.bars_written for r in results)
    n_overlaps = sum(1 for r in results if r.shard.is_overlap)
    n_gaps = len(results) - n_overlaps
    logger.info(
        "kintsugi pass complete: %d healed, %d clean, %d deferred, %d failed, "
        "%d bars written (%d gaps, %d overlaps) (%.1fs)",
        healed,
        clean,
        deferred,
        failed,
        total_bars,
        n_gaps,
        n_overlaps,
        pass_duration,
    )
    _emit_telemetry(
        "pass_complete",
        healed=healed,
        clean=clean,
        deferred=deferred,
        failed=failed,
        total_bars=total_bars,
        n_gaps=n_gaps,
        n_overlaps=n_overlaps,
        duration_s=round(pass_duration, 1),
    )

    # STARTUP-03: clear signal file after processing yesterday priority shards
    if yesterday_shards:
        yesterday_keys_set = {
            (s.symbol, s.threshold_decimal_bps) for s in yesterday_shards
        }
        processed_keys = {
            (r.shard.symbol, r.shard.threshold_decimal_bps) for r in results
        }
        unprocessed = yesterday_keys_set - processed_keys
        if unprocessed:
            logger.warning(
                "yesterday-repair: %d shards not processed this pass (budget?), "
                "keeping signal file for next pass",
                len(unprocessed),
            )
        else:
            clear_yesterday_signal()
            logger.info("yesterday-repair: signal file cleared after processing all flagged shards")

    # Issue #117-119: Emit KINTSUGI_PASS_COMPLETE hook + quiet Telegram
    try:
        from opendeviationbar.hooks import HookEvent, emit_hook

        emit_hook(
            HookEvent.KINTSUGI_PASS_COMPLETE,
            symbol="*",
            n_discovered=len(shards),
            n_healed=healed,
            n_failed=failed,
            total_bars=total_bars,
            pass_duration=pass_duration,
        )
    except (ImportError, OSError, RuntimeError, ValueError):
        logger.debug("failed to emit KINTSUGI_PASS_COMPLETE hook", exc_info=True)
    try:
        from opendeviationbar.notify.telegram import _build_context_block, send_telegram

        total_missing = sum(
            r.shard.missing_trade_count
            for r in results
            if r.shard.missing_trade_count > 0
        )
        remaining = len(shards) - len(results)

        # Build human-readable outcome line
        outcomes = []
        if healed:
            outcomes.append(f"{healed} repaired")
        if deferred:
            outcomes.append(f"{deferred} deferred to tomorrow")
        if clean:
            outcomes.append(f"{clean} already clean")
        if failed:
            outcomes.append(f"{failed} FAILED")
        outcome_str = ", ".join(outcomes) if outcomes else "nothing to do"

        # Build human-readable healed details
        healed_details = ""
        healed_results = [r for r in results if r.healed]
        if healed_results:
            detail_lines = []
            for r in healed_results[:_kintsugi_cfg().max_healed_details]:
                kind = "overlap" if r.shard.is_overlap else "gap"
                src = f" via {r.source}" if r.source else ""
                detail_lines.append(
                    f"  {r.shard.symbol}@{r.shard.threshold_decimal_bps}: "
                    f"{kind}, {r.bars_written} bars{src}"
                )
            healed_details = "\n".join(detail_lines)
            if len(healed_results) > _kintsugi_cfg().max_healed_details:
                healed_details += (
                    f"\n  ... +{len(healed_results) - _kintsugi_cfg().max_healed_details} more"
                )
            healed_details = f"\n\n{healed_details}"

        pass_msg = (
            f"Kintsugi healed {healed} anomalies in {pass_duration:.0f}s"
            f"{healed_details}\n\n"
            f"<b>Result:</b> {outcome_str}\n"
            f"<b>Bars written:</b> {total_bars:,} from {total_missing:,} trades\n"
            f"<b>Remaining queue:</b> {remaining:,} shards"
            + _build_context_block("kintsugi")
        )
        send_telegram(pass_msg, disable_notification=True)
    except (ImportError, OSError, RuntimeError):
        logger.debug("failed to send kintsugi pass Telegram", exc_info=True)

    # Notify on real failures only (not deferred or clean)
    if failed > 0:
        _notify_kintsugi_failures(results, pass_duration=pass_duration)

    return results
