"""Signal engine configuration loaded from risk_configs + environment.

Defines ``SignalEngineConfig`` (the top-level dataclass) and four derived
sub-configs — ``HardFilterConfig``, ``HeuristicConfig``,
``ProbabilisticConfig``, ``ExitConfig`` — that expose relevant subsets for
cleaner function signatures.

``load_config()`` reads from the ``risk_configs`` table's JSONB ``config``
column and falls back to safe defaults on any error.  Environment variables
with the ``SIGNAL_ENGINE_`` prefix override database values.

Requirements: 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7
"""

from __future__ import annotations

import json
import logging
import os
from dataclasses import dataclass, field
from typing import Any

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Sub-configs — thin wrappers over relevant subsets of SignalEngineConfig
# ---------------------------------------------------------------------------


@dataclass
class HardFilterConfig:
    """Thresholds for the pre-pipeline hard filter engine."""

    valuation_min: float = 0.3
    earnings_days: int = 5
    macro_bias_skip: float = -1.0


@dataclass
class HeuristicConfig:
    """Thresholds for the heuristic (deterministic) pipeline verdict."""

    buy_confidence: float = 0.70
    buy_s_total: float = 1.2
    buy_valuation_min: float = 0.5
    watch_confidence: float = 0.55
    macro_bias_threshold: float = 0.0  # macro_bias must be > this for BUY
    earnings_days_threshold: int = 5  # earnings_proximity must be > this for BUY


@dataclass
class ProbabilisticConfig:
    """Thresholds for the probabilistic (Bayesian) pipeline verdict."""

    buy_p_up: float = 0.60
    buy_entropy_max: float = 0.90
    buy_ev_r_min: float = 1.5
    buy_valuation_min: float = 0.5
    watch_p_up: float = 0.55
    watch_entropy_max: float = 0.95
    entropy_skip: float = 0.95

    # Regime priors
    regime_prior_bull: float = 0.58
    regime_prior_range: float = 0.50
    regime_prior_bear: float = 0.42

    # Fundamental gates (same semantics as heuristic)
    macro_bias_threshold: float = 0.0
    earnings_days_threshold: int = 5


@dataclass
class ExitConfig:
    """Configuration for the exit engine."""

    trailing_stop_atr_multiplier: float = 2.0


# ---------------------------------------------------------------------------
# Top-level config
# ---------------------------------------------------------------------------


@dataclass
class SignalEngineConfig:
    """Configuration loaded from risk_configs + environment.

    All fields carry safe defaults so that a fresh deployment works without
    any database rows or environment variables.
    """

    dual_pipeline_enabled: bool = False
    heuristic_pipeline_enabled: bool = True
    probabilistic_pipeline_enabled: bool = True
    shadow_mode: bool = False

    # Timeframe weights
    timeframe_weights: dict[str, float] = field(
        default_factory=lambda: {
            "M30": 0.03,
            "H1": 0.07,
            "H4": 0.15,
            "D": 0.30,
            "W": 0.30,
            "M": 0.15,
        }
    )

    # Hard filter thresholds
    hard_filter_valuation_min: float = 0.3
    hard_filter_earnings_days: int = 5
    hard_filter_macro_bias_skip: float = -1.0

    # Heuristic verdict thresholds
    heuristic_buy_confidence: float = 0.70
    heuristic_buy_s_total: float = 1.2
    heuristic_buy_valuation_min: float = 0.5
    heuristic_watch_confidence: float = 0.55

    # Probabilistic verdict thresholds
    prob_buy_p_up: float = 0.60
    prob_buy_entropy_max: float = 0.90
    prob_buy_ev_r_min: float = 1.5
    prob_buy_valuation_min: float = 0.5
    prob_watch_p_up: float = 0.55
    prob_watch_entropy_max: float = 0.95
    prob_entropy_skip: float = 0.95

    # Regime priors
    regime_prior_bull: float = 0.58
    regime_prior_range: float = 0.50
    regime_prior_bear: float = 0.42

    # Exit engine
    trailing_stop_atr_multiplier: float = 2.0

    # Polling
    polling_interval_seconds: int = 30

    # -- Derived sub-configs ------------------------------------------------

    @property
    def hard_filter_config(self) -> HardFilterConfig:
        return HardFilterConfig(
            valuation_min=self.hard_filter_valuation_min,
            earnings_days=self.hard_filter_earnings_days,
            macro_bias_skip=self.hard_filter_macro_bias_skip,
        )

    @property
    def heuristic_config(self) -> HeuristicConfig:
        return HeuristicConfig(
            buy_confidence=self.heuristic_buy_confidence,
            buy_s_total=self.heuristic_buy_s_total,
            buy_valuation_min=self.heuristic_buy_valuation_min,
            watch_confidence=self.heuristic_watch_confidence,
            macro_bias_threshold=0.0,
            earnings_days_threshold=self.hard_filter_earnings_days,
        )

    @property
    def probabilistic_config(self) -> ProbabilisticConfig:
        return ProbabilisticConfig(
            buy_p_up=self.prob_buy_p_up,
            buy_entropy_max=self.prob_buy_entropy_max,
            buy_ev_r_min=self.prob_buy_ev_r_min,
            buy_valuation_min=self.prob_buy_valuation_min,
            watch_p_up=self.prob_watch_p_up,
            watch_entropy_max=self.prob_watch_entropy_max,
            entropy_skip=self.prob_entropy_skip,
            regime_prior_bull=self.regime_prior_bull,
            regime_prior_range=self.regime_prior_range,
            regime_prior_bear=self.regime_prior_bear,
            macro_bias_threshold=0.0,
            earnings_days_threshold=self.hard_filter_earnings_days,
        )

    @property
    def exit_config(self) -> ExitConfig:
        return ExitConfig(
            trailing_stop_atr_multiplier=self.trailing_stop_atr_multiplier,
        )


# ---------------------------------------------------------------------------
# Config loading helpers
# ---------------------------------------------------------------------------

# SQL to fetch all signal_engine_* keys from the active risk_configs row's
# JSONB config column.  The query extracts each top-level key/value pair and
# filters to those prefixed with ``signal_engine_``.
_CONFIG_QUERY = """
SELECT key, value
FROM (
    SELECT key, value
    FROM risk_configs,
         jsonb_each_text(config)
    WHERE active = TRUE
    ORDER BY updated_at DESC
    LIMIT 1
) sub
WHERE key LIKE 'signal_engine_%'
"""

# Mapping from risk_configs JSON key → SignalEngineConfig field name.
# Keys in the DB are prefixed ``signal_engine_`` which is stripped to match
# the dataclass field names.
_FIELD_TYPES: dict[str, type] = {
    "dual_pipeline_enabled": bool,
    "heuristic_pipeline_enabled": bool,
    "probabilistic_pipeline_enabled": bool,
    "shadow_mode": bool,
    "timeframe_weights": dict,
    "hard_filter_valuation_min": float,
    "hard_filter_earnings_days": int,
    "hard_filter_macro_bias_skip": float,
    "heuristic_buy_confidence": float,
    "heuristic_buy_s_total": float,
    "heuristic_buy_valuation_min": float,
    "heuristic_watch_confidence": float,
    "prob_buy_p_up": float,
    "prob_buy_entropy_max": float,
    "prob_buy_ev_r_min": float,
    "prob_buy_valuation_min": float,
    "prob_watch_p_up": float,
    "prob_watch_entropy_max": float,
    "prob_entropy_skip": float,
    "regime_prior_bull": float,
    "regime_prior_range": float,
    "regime_prior_bear": float,
    "trailing_stop_atr_multiplier": float,
    "polling_interval_seconds": int,
}


def _parse_value(raw: str, target_type: type) -> Any:
    """Coerce a raw string value from the DB/env into *target_type*.

    Booleans accept ``true``/``false`` (case-insensitive).
    Dicts are parsed as JSON.
    """
    if target_type is bool:
        return raw.lower() in ("true", "1", "yes")
    if target_type is dict:
        return json.loads(raw)
    if target_type is int:
        return int(raw)
    if target_type is float:
        return float(raw)
    return raw


def _apply_db_rows(
    config: SignalEngineConfig,
    rows: list[tuple[str, str]],
) -> None:
    """Mutate *config* in-place from ``(key, value)`` DB rows.

    Keys are expected to be prefixed ``signal_engine_`` — the prefix is
    stripped before matching against dataclass fields.
    """
    for key, value in rows:
        field_name = key.removeprefix("signal_engine_")
        target_type = _FIELD_TYPES.get(field_name)
        if target_type is None:
            logger.debug("Ignoring unknown signal_engine config key: %s", key)
            continue
        try:
            parsed = _parse_value(value, target_type)
            setattr(config, field_name, parsed)
        except (ValueError, TypeError, json.JSONDecodeError):
            logger.warning(
                "Invalid value for signal_engine config key %s: %r — keeping default",
                key,
                value,
            )


def _apply_env_overrides(config: SignalEngineConfig) -> None:
    """Override config fields from environment variables.

    Environment variables use the ``SIGNAL_ENGINE_`` prefix (upper-case).
    For example ``SIGNAL_ENGINE_DUAL_PIPELINE_ENABLED=true`` overrides
    ``dual_pipeline_enabled``.
    """
    prefix = "SIGNAL_ENGINE_"
    for env_key, env_value in os.environ.items():
        if not env_key.startswith(prefix):
            continue
        field_name = env_key[len(prefix):].lower()
        target_type = _FIELD_TYPES.get(field_name)
        if target_type is None:
            continue
        try:
            parsed = _parse_value(env_value, target_type)
            setattr(config, field_name, parsed)
        except (ValueError, TypeError, json.JSONDecodeError):
            logger.warning(
                "Invalid env override %s=%r — keeping previous value",
                env_key,
                env_value,
            )


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


async def load_config(pool: Any) -> SignalEngineConfig:
    """Load signal engine configuration from the database and environment.

    1. Start with safe defaults (``SignalEngineConfig()``).
    2. Query ``risk_configs`` for keys prefixed ``signal_engine_``.
    3. Apply matching values over the defaults.
    4. Apply environment variable overrides (``SIGNAL_ENGINE_*``).
    5. On any DB error, fall back to defaults with ``dual_pipeline_enabled=False``.

    The *pool* argument is an ``asyncpg.Pool`` (typed as ``Any`` to avoid a
    hard import dependency at module level).

    Requirements: 13.1, 13.6, 13.7
    """
    config = SignalEngineConfig()

    # Step 1 — read from risk_configs
    try:
        rows = await pool.fetch(_CONFIG_QUERY)
        if rows:
            _apply_db_rows(config, [(r["key"], r["value"]) for r in rows])
    except Exception:
        logger.warning(
            "Failed to load signal engine config from risk_configs — "
            "defaulting to disabled (fail-safe)",
            exc_info=True,
        )
        # Ensure fail-safe: dual pipeline stays off
        config.dual_pipeline_enabled = False

    # Step 2 — environment overrides (always applied, even after DB failure)
    _apply_env_overrides(config)

    logger.info(
        "Signal engine config loaded: dual_pipeline_enabled=%s, "
        "heuristic=%s, probabilistic=%s, shadow_mode=%s, "
        "polling_interval=%ds",
        config.dual_pipeline_enabled,
        config.heuristic_pipeline_enabled,
        config.probabilistic_pipeline_enabled,
        config.shadow_mode,
        config.polling_interval_seconds,
    )

    return config
