"""Constants and presets for opendeviationbar-py.

This module centralizes all constants to eliminate duplication across the codebase.
Import from here instead of defining locally.

SSoT (Single Source of Truth) for:
- MICROSTRUCTURE_COLUMNS: Optional microstructure feature columns
- TIER1_SYMBOLS: High-liquidity crypto symbols
- THRESHOLD_PRESETS: Named threshold values in decimal basis points
- THRESHOLD_DECIMAL_MIN/MAX: Valid threshold range
- _CRYPTO_BASES: Known crypto base symbols for asset class detection
- _FOREX_CURRENCIES: Known forex currencies for asset class detection
- MEM_GUARDS: Memory guard registry (Issue #49)

Per-symbol thresholds are in symbols.toml (symbol_registry.get_thresholds_for_symbol()).

Environment Variable SSoT (from .mise.toml):
- OPENDEVIATIONBAR_CRYPTO_MIN_THRESHOLD: Minimum threshold for crypto (250 dbps)
- OPENDEVIATIONBAR_FOREX_MIN_THRESHOLD: Minimum threshold for forex (50 dbps)
- OPENDEVIATIONBAR_INTER_BAR_LOOKBACK_COUNT: Lookback for inter-bar features (200)
- OPENDEVIATIONBAR_INCLUDE_INTRA_BAR_FEATURES: Auto-enable intra-bar features (true)
"""

from __future__ import annotations

import os as _os

# =============================================================================
# Schema Version Constants (Cache Evolution)
# =============================================================================
# Used for cache validation and schema evolution tracking.
# Increment when schema changes require cache invalidation.
#
# Version history:
# - 6.0.0: OHLCV only (legacy, pre-microstructure)
# - 7.0.0: Added 15 microstructure columns (Issue #25)
# - 10.0.0: Added ouroboros_mode column
# - 11.0.0: Current version with modular architecture

SCHEMA_VERSION_OHLCV_ONLY: str = "6.0.0"  # Pre-microstructure (legacy)
SCHEMA_VERSION_MICROSTRUCTURE: str = "7.0.0"  # Added 15 microstructure columns
SCHEMA_VERSION_OUROBOROS: str = "10.0.0"  # Added ouroboros_mode column

# Minimum versions required for features
MIN_VERSION_FOR_MICROSTRUCTURE: str = SCHEMA_VERSION_MICROSTRUCTURE
MIN_VERSION_FOR_OUROBOROS: str = SCHEMA_VERSION_OUROBOROS

# =============================================================================
# Microstructure Columns (Issue #25, v7.0+)
# =============================================================================
# These columns are optional and only present when include_microstructure=True
# or when bars are generated with microstructure features enabled.
#
# IMPORTANT: Keep this list in sync with:
# - crates/opendeviationbar-core/src/bar.rs (Rust struct fields)
# - python/opendeviationbar/clickhouse/schema.sql (ClickHouse columns)

MICROSTRUCTURE_COLUMNS: tuple[str, ...] = (
    # Basic extended columns
    "vwap",
    "buy_volume",
    "sell_volume",
    "individual_trade_count",
    "agg_record_count",
    # Microstructure features (Issue #25)
    "duration_us",
    "ofi",
    "vwap_close_deviation",
    "price_impact",
    "kyle_lambda_proxy",
    "trade_intensity",
    "volume_per_trade",
    "aggression_ratio",
    "aggregation_density",
    "turnover_imbalance",
)

# =============================================================================
# Inter-Bar Feature Columns (Issue #59, v12.0+)
# =============================================================================
# Computed from a lookback window of trades BEFORE each bar opens.
# All features are Optional - None when no lookback data available.
#
# IMPORTANT: Keep this list in sync with:
# - crates/opendeviationbar-core/src/interbar.rs (Rust feature computation)
# - crates/opendeviationbar-core/src/types.rs (OpenDeviationBar struct fields)
# - python/opendeviationbar/clickhouse/schema.sql (ClickHouse columns)

# Issue #128: Split into tier sub-tuples for per-feature toggle support.
# Backward compat: INTER_BAR_FEATURE_COLUMNS remains the combined tuple.
INTER_BAR_TIER1_COLUMNS: tuple[str, ...] = (
    "lookback_trade_count",
    "lookback_ofi",
    "lookback_duration_us",
    "lookback_intensity",
    "lookback_vwap_raw",
    "lookback_vwap_position",
    "lookback_count_imbalance",
)

INTER_BAR_TIER2_COLUMNS: tuple[str, ...] = (
    # Issue #128: Garman-Klass promoted from Tier 3 → Tier 2
    "lookback_kyle_lambda",
    "lookback_burstiness",
    "lookback_volume_skew",
    "lookback_volume_kurt",
    "lookback_price_range",
    "lookback_garman_klass_vol",
)

INTER_BAR_TIER3_COLUMNS: tuple[str, ...] = (
    # Issue #128: Hurst + PE have ZERO presence in Gen600 top 100 configs
    "lookback_kaufman_er",
    "lookback_hurst",
    "lookback_permutation_entropy",
)

INTER_BAR_FEATURE_COLUMNS: tuple[str, ...] = (
    *INTER_BAR_TIER1_COLUMNS,
    *INTER_BAR_TIER2_COLUMNS,
    *INTER_BAR_TIER3_COLUMNS,
)

# =============================================================================
# Intra-Bar Feature Columns (Issue #59, v12.0+)
# =============================================================================
# Computed from trades WITHIN each bar (from open_time to close_time).
# All features are Optional - None when intra-bar features disabled.
#
# Features include:
# - 8 ITH (Investment Time Horizon) features
# - 12 statistical features
# - 2 complexity features (require 60+ trades)
#
# IMPORTANT: Keep this list in sync with:
# - crates/opendeviationbar-core/src/intrabar/features.rs (Rust feature computation)
# - crates/opendeviationbar-core/src/types.rs (OpenDeviationBar struct fields)

INTRA_BAR_FEATURE_COLUMNS: tuple[str, ...] = (
    # ITH features (8 features) - Investment Time Horizon from trading-fitness
    "intra_bull_epoch_density",
    "intra_bear_epoch_density",
    "intra_bull_excess_gain",
    "intra_bear_excess_gain",
    "intra_bull_cv",
    "intra_bear_cv",
    "intra_max_drawdown",
    "intra_max_runup",
    # Statistical features (12 features)
    "intra_trade_count",
    "intra_ofi",
    "intra_duration_us",
    "intra_intensity",
    "intra_vwap_position",
    "intra_count_imbalance",
    "intra_kyle_lambda",
    "intra_burstiness",
    "intra_volume_skew",
    "intra_volume_kurt",
    "intra_kaufman_er",
    "intra_garman_klass_vol",
    # Complexity features (2 features, require 60+ trades)
    # Issue #128: These can be toggled off via OPENDEVIATIONBAR_COMPUTE_HURST/PE
    "intra_hurst",
    "intra_permutation_entropy",
)

# Issue #128: Intra-bar complexity columns (gated by per-feature toggles)
INTRA_BAR_COMPLEXITY_COLUMNS: tuple[str, ...] = (
    "intra_hurst",
    "intra_permutation_entropy",
)

# =============================================================================
# Trade ID Range Columns (Issue #72, v12.4+)
# =============================================================================
# Aggregate trade ID range for data integrity verification.
# Enables gap detection: bars[i].first_agg_trade_id == bars[i-1].last_agg_trade_id + 1
#
# IMPORTANT: Keep this list in sync with:
# - crates/opendeviationbar-core/src/types.rs (OpenDeviationBar struct fields)
# - python/opendeviationbar/clickhouse/schema.sql (ClickHouse columns)

TRADE_ID_RANGE_COLUMNS: tuple[str, ...] = (
    "first_agg_trade_id",
    "last_agg_trade_id",
)

# =============================================================================
# Timestamp Columns (close_time_ms + open_time_ms for ClickHouse)
# =============================================================================
# ClickHouse stores close_time_ms (ORDER BY key) and open_time_ms (bar open time).
# close_time_ms replaces the legacy "timestamp_ms" column.
#
# IMPORTANT: Keep this list in sync with:
# - src/helpers.rs (opendeviationbar_to_dict output)
# - python/opendeviationbar/clickhouse/schema.sql (ClickHouse columns)

TIMESTAMP_COLUMNS: tuple[str, ...] = (
    "close_time_ms",
    "open_time_ms",
)

# =============================================================================
# Bar Flag Columns (Issue #101: Liquidation cascade detection)
# =============================================================================
# Boolean flags derived from bar properties. Always included in query output
# regardless of include_microstructure. No Rust changes needed — computed
# from existing ClickHouse columns via DEFAULT expression.
#
# is_liquidation_cascade: True when a single Binance matching-engine atomic
#   batch collapses many fills into one bar. Rule: range >= 10x threshold
#   AND duration_us <= 100ms. Covers 95.6% of all extreme bars (10x+).
#   Two sub-populations: micro-ghost (1-10 trades, matching artifact) and
#   massive-cascade (1000+ trades, genuine liquidation event).
#   Forensic evidence: 2025-10-10 24,433 trades all at microsecond
#   1760131449755735 — no Binance data product can split further.
#
# IMPORTANT: Keep in sync with schema.sql DEFAULT expression.

BAR_FLAG_COLUMNS: tuple[str, ...] = ("is_liquidation_cascade",)

# =============================================================================
# Tier-1 Symbols (high-liquidity, available on all Binance markets)
# SSoT: symbols.toml (tier=1 entries). Verified by test_symbol_registry.py. (Issue #79)
# Quick win: use symbol_registry.get_tier1_symbols() (will read from [meta] in future)
# Note: Format is WITHOUT USDT suffix (e.g., "BTC" not "BTCUSDT") for compatibility
# =============================================================================

TIER1_SYMBOLS: tuple[str, ...] = (
    "AAVE",
    "ADA",
    "AVAX",
    "BCH",
    "BNB",
    "BTC",
    "DOGE",
    "ETH",
    "FIL",
    "LINK",
    "LTC",
    "NEAR",
    "SOL",
    "SUI",
    "UNI",
    "WIF",
    "WLD",
    "XRP",
)

# =============================================================================
# Threshold Range (from opendeviationbar-core)
# =============================================================================

THRESHOLD_DECIMAL_MIN: int = 1  # 1 dbps = 0.001%
THRESHOLD_DECIMAL_MAX: int = 100_000  # 100,000 dbps = 100%

# Default threshold for single-threshold operations (e.g. populate, backtest)
# 250 dbps = 0.25%
# Quick win: use symbol_registry helper (will read from [meta] in future)
DEFAULT_THRESHOLD: int = 250  # Can be: from opendeviationbar.symbol_registry import get_default_threshold; get_default_threshold()


# =============================================================================
# Threshold Presets (in decimal basis points)
# =============================================================================
# 1 dbps = 0.001% = 0.00001 (one-tenth of a basis point)
# Example: 250 dbps = 0.25%
# Quick win: use symbol_registry.get_default_thresholds() for the numeric values

THRESHOLD_PRESETS: dict[str, int] = {
    "micro": 10,  # 10 dbps = 0.01% (scalping)
    "tight": 50,  # 50 dbps = 0.05% (day trading)
    "standard": 100,  # 100 dbps = 0.1% (swing trading)
    "medium": 250,  # 250 dbps = 0.25% (default)
    "wide": 500,  # 500 dbps = 0.5% (position trading)
    "macro": 750,  # 75bps = 0.75% (long-term)
}

# =============================================================================
# Asset Class Detection Helpers
# =============================================================================

# Common crypto base symbols for detection
_CRYPTO_BASES: frozenset[str] = frozenset(
    {
        "AAVE",
        "ADA",
        "ALGO",
        "APT",
        "ATOM",
        "AVAX",
        "BCH",
        "BNB",
        "BTC",
        "DOGE",
        "DOT",
        "ETC",
        "ETH",
        "FIL",
        "LINK",
        "LTC",
        "NEAR",
        "POL",
        "SOL",
        "SUI",
        "UNI",
        "WIF",
        "WLD",
        "XLM",
        "XRP",
        # Wrapped tokens (Issue #62: crypto threshold enforcement)
        "CBETH",  # Coinbase staked ETH
        "WBTC",  # Wrapped Bitcoin
        "WETH",  # Wrapped Ethereum
        "WPOL",  # Wrapped Polygon (was WMATIC, MATIC→POL rebrand)
        "WSTETH",  # Lido staked ETH
    }
)

# Common forex base/quote currencies
_FOREX_CURRENCIES: frozenset[str] = frozenset(
    {
        "EUR",
        "USD",
        "GBP",
        "JPY",
        "CHF",
        "AUD",
        "NZD",
        "CAD",
        "SEK",
        "NOK",
    }
)

# =============================================================================
# Continuity Validation Constants
# =============================================================================

# Default tolerance for continuity validation (0.1% = 0.001).
# Matches precompute_open_deviation_bars() default. Override via env var:
#   export OPENDEVIATIONBAR_CONTINUITY_TOLERANCE=0.005
CONTINUITY_TOLERANCE_PCT: float = float(
    _os.environ.get("OPENDEVIATIONBAR_CONTINUITY_TOLERANCE", "0.001")
)

# =============================================================================
# Exchange Session Column Names (Ouroboros feature)
# =============================================================================

EXCHANGE_SESSION_COLUMNS: tuple[str, ...] = (
    "exchange_session_sydney",
    "exchange_session_tokyo",
    "exchange_session_london",
    "exchange_session_newyork",
)

# =============================================================================
# All Optional Columns (for cache operations)
# =============================================================================
# Union of microstructure + exchange session + inter-bar + intra-bar + trade ID columns

ALL_OPTIONAL_COLUMNS: tuple[str, ...] = (
    *MICROSTRUCTURE_COLUMNS,
    *EXCHANGE_SESSION_COLUMNS,
    *INTER_BAR_FEATURE_COLUMNS,
    *INTRA_BAR_FEATURE_COLUMNS,
    *TRADE_ID_RANGE_COLUMNS,  # Issue #72
)

# =============================================================================
# Plugin Feature Columns (Issue #98: FeatureProvider plugin system)
# =============================================================================
# Dynamically registered by discovered FeatureProvider plugins at import time.
# These columns are Nullable(Float64) in ClickHouse, same as inter-bar features.
#
# IMPORTANT: Keep this list in sync with:
# - python/opendeviationbar/clickhouse/schema.sql
#   (via ALTER TABLE ADD COLUMN IF NOT EXISTS)
# - python/opendeviationbar/clickhouse/bulk_operations.py (INSERT column loop)
# - python/opendeviationbar/clickhouse/query_operations.py (SELECT column list)

_PLUGIN_FEATURE_COLUMNS: list[str] = []


def register_plugin_columns(columns: tuple[str, ...]) -> None:
    """Register additional columns from a FeatureProvider plugin.

    Called by ``plugins.loader.discover_providers()`` during discovery.
    Idempotent: duplicate column names are silently ignored.
    """
    for col in columns:
        if col not in _PLUGIN_FEATURE_COLUMNS:
            _PLUGIN_FEATURE_COLUMNS.append(col)


def get_all_columns() -> tuple[str, ...]:
    """ALL_OPTIONAL_COLUMNS + any dynamically registered plugin columns."""
    return (*ALL_OPTIONAL_COLUMNS, *_PLUGIN_FEATURE_COLUMNS)


# =============================================================================
# Memory Guard Registry (Issue #49)
# =============================================================================
# Each guard prevents a specific memory exhaustion pattern.
# Code references use "# MEM-XXX:" comments for traceability.
#
# Guards are organized by pipeline stage:
#   Loading  → MEM-001, MEM-004, MEM-007, MEM-010
#   Process  → MEM-002, MEM-003
#   Concat   → MEM-006, MEM-008
#   Test     → MEM-005
#   Process  → MEM-009
#
# When adding a new guard, assign the next number and add an entry here.

MEM_GUARDS: dict[str, dict[str, str]] = {
    "MEM-001": {
        "description": "Avoid map_elements() in Parquet parsing (native Polars ops)",
        "location": "storage/parquet.py:185",
        "stage": "loading",
    },
    "MEM-002": {
        "description": "Process trades in 100K chunks (~15 MB each)",
        "location": "orchestration/helpers.py:274, processors/api.py:371",
        "stage": "processing",
    },
    "MEM-003": {
        "description": "Select columns BEFORE .collect() on LazyFrame",
        "location": "orchestration/helpers.py:236, processors/api.py:341",
        "stage": "processing",
    },
    "MEM-004": {
        "description": "Guard read_ticks() with size estimation before .collect()",
        "location": "storage/parquet.py",
        "stage": "loading",
    },
    "MEM-005": {
        "description": "gc.collect() after each test to prevent accumulation",
        "location": "tests/conftest.py:26",
        "stage": "testing",
    },
    "MEM-006": {
        "description": "Use Polars concat instead of pandas for memory efficiency",
        "location": "conversion.py:107, orchestration/precompute.py:404",
        "stage": "concatenation",
    },
    "MEM-007": {
        "description": "Guard deprecated _fetch_binance() with date range limit",
        "location": "orchestration/helpers.py:136",
        "stage": "loading",
    },
    "MEM-008": {
        "description": "Streaming bar accumulation (avoid holding all in memory)",
        "location": "orchestration/open_deviation_bars.py",
        "stage": "concatenation",
    },
    "MEM-009": {
        "description": "Process-level RLIMIT_AS cap (MemoryError instead of OOM kill)",
        "location": "resource_guard.py",
        "stage": "process",
    },
    "MEM-010": {
        "description": "Pre-flight memory estimation before tick loading",
        "location": "resource_guard.py",
        "stage": "loading",
    },
    "MEM-011": {
        "description": "Adaptive chunk size: 100K base, 50K with microstructure",
        "location": "orchestration/helpers.py:304",
        "stage": "processing",
    },
    "MEM-012": {
        "description": "Streaming bar batches (10K) to prevent OOM",
        "location": "orchestration/helpers.py:236",
        "stage": "processing",
    },
    "MEM-013": {
        "description": "Force ClickHouse-first for long date ranges (>30 days)",
        "location": "orchestration/open_deviation_bars.py",
        "stage": "routing",
    },
    "MEM-014": {
        "description": "Chunked REST gap repair: 7-day sub-windows with streaming processor",
        "location": "backfill.py",
        "stage": "processing",
    },
}

# =============================================================================
# Long Range Threshold (MEM-013)
# =============================================================================
# Date ranges exceeding this limit require populate_cache_resumable() first.
# This prevents OOM on long backfills by forcing incremental caching.

LONG_RANGE_DAYS: int = 30
