"""
matrix-bridge-dagi — configuration and validation (M2.1 + M2.2 + M3.0 + M3.1)
"""
import os
from dataclasses import dataclass, field
from typing import FrozenSet


@dataclass(frozen=True)
class BridgeConfig:
    # Matrix homeserver
    matrix_homeserver_url: str
    matrix_access_token: str
    matrix_user_id: str            # e.g. @dagi_bridge:daarion.space

    # Room → agent mapping (M1: single room)
    sofiia_room_id: str            # e.g. !abcdef:daarion.space

    # DAGI backend
    dagi_gateway_url: str          # e.g. http://dagi-gateway-node1:9300
    default_node_id: str           # e.g. NODA1

    # Sofiia Console (audit write)
    sofiia_console_url: str        # e.g. http://dagi-sofiia-console-node1:8002
    sofiia_internal_token: str     # X-Internal-Service-Token for audit ingest

    # Policy
    bridge_allowed_agents: FrozenSet[str]
    rate_limit_room_rpm: int       # max messages per room per minute
    rate_limit_sender_rpm: int     # max messages per sender per minute

    # H2: Backpressure queue
    queue_max_events: int          # max pending items (drops oldest on full)
    worker_concurrency: int        # parallel invoke workers
    queue_drain_timeout_s: float   # graceful shutdown drain timeout

    # M2.1: Mixed rooms
    # "!roomX:server=sofiia,helion;!roomY:server=druid"
    bridge_mixed_room_map: str
    # "!roomX:server=helion" — explicit default per mixed room (optional)
    bridge_mixed_defaults: str

    # M2.2: Mixed room guard rails
    max_agents_per_mixed_room: int   # fail-fast if room defines more agents than this
    max_slash_len: int               # reject slash token longer than this (anti-garbage)
    unknown_agent_behavior: str      # "ignore" | "reply_error"
    mixed_concurrency_cap: int       # max parallel invokes per (room, agent); 0 = unlimited

    # M3.0: Operator control channel
    # "@ivan:daarion.space,@sergiy:daarion.space"
    bridge_operator_allowlist: str
    # "!opsroom:server,!opsroom2:server2"
    bridge_control_rooms: str
    # "ignore" | "reply_error" (send ⛔ to room on unauthorized attempt)
    control_unauthorized_behavior: str

    # M3.1: Runbook runner — sofiia-console control token
    sofiia_control_token: str          # X-Control-Token for /api/runbooks/internal/runs

    # M3.4: Control channel safety — rate limiting + cooldown
    control_room_rpm: int          # Max commands per room per minute (0 = unlimited)
    control_operator_rpm: int      # Max commands per operator per minute
    control_run_next_rpm: int      # Max !runbook next calls per run_id per minute
    control_cooldown_s: float      # Anti-double-click debounce per (operator, verb, subcmd)

    # M2.3: Persistent event deduplication
    persistent_dedupe: bool        # Enable SQLite-backed dedupe across restarts
    bridge_data_dir: str           # Directory for SQLite DB and other bridge data
    processed_events_ttl_h: int    # TTL for processed events (hours)
    processed_events_prune_batch: int       # Max rows to prune per prune run
    processed_events_prune_interval_s: int  # Prune interval in seconds (0 = disable periodic)

    # M4.0: agent discovery
    discovery_rpm: int             # Max !agents replies per room per minute (0 = unlimited)

    # M5.0: node-aware routing
    bridge_allowed_nodes: str      # Comma-separated: "NODA1,NODA2"
    bridge_default_node: str       # Default node when none specified
    bridge_room_node_map: str      # Optional: "!roomA:server=NODA2;!roomB:server=NODA1"

    # M8.0: node health + soft-failover thresholds
    node_fail_consecutive: int     # Consecutive failures before node marked "down"
    node_lat_ewma_s: float         # EWMA latency threshold (seconds) → "degraded"
    node_ewma_alpha: float         # EWMA smoothing factor (0..1)

    # M8.1: sticky failover cache
    failover_sticky_ttl_s: float   # Seconds to hold sticky node preference after failover (0 = disabled)

    # M8.2: HA state persistence
    ha_health_snapshot_interval_s: int  # Seconds between node health writes to DB (0 = disabled)
    ha_health_max_age_s: int            # Max age of health snapshot to load on startup (seconds)

    # M9.0: Two-step confirmation TTL
    confirm_ttl_s: float  # Seconds a pending !confirm nonce is valid (0 = disabled)

    # M10.0: Policy export retention
    policy_export_retention_days: int  # Days to keep policy exports (0 = keep forever)

    # M10.2: Policy change history
    policy_history_limit: int  # Max rows in policy_changes table (0 = unlimited)

    # M11 soak: synthetic event injection (NEVER enable in production)
    debug_inject_enabled: bool  # POST /v1/debug/inject_event (default: False)

    # Service identity
    node_id: str
    build_sha: str
    build_time: str


def load_config() -> BridgeConfig:
    """Load and validate config from environment variables."""

    def _require(key: str) -> str:
        v = os.getenv(key, "").strip()
        if not v:
            raise RuntimeError(f"Required env var {key!r} is not set")
        return v

    def _optional(key: str, default: str = "") -> str:
        return os.getenv(key, default).strip()

    allowed_raw = _optional("BRIDGE_ALLOWED_AGENTS", "sofiia")
    allowed = frozenset(a.strip() for a in allowed_raw.split(",") if a.strip())

    return BridgeConfig(
        matrix_homeserver_url=_require("MATRIX_HOMESERVER_URL").rstrip("/"),
        matrix_access_token=_require("MATRIX_ACCESS_TOKEN"),
        matrix_user_id=_require("MATRIX_USER_ID"),
        sofiia_room_id=_require("SOFIIA_ROOM_ID"),
        dagi_gateway_url=_require("DAGI_GATEWAY_URL").rstrip("/"),
        default_node_id=_optional("DEFAULT_NODE_ID", "NODA1"),
        sofiia_console_url=_optional("SOFIIA_CONSOLE_URL", "").rstrip("/"),
        sofiia_internal_token=_optional("SOFIIA_INTERNAL_TOKEN", ""),
        bridge_allowed_agents=allowed,
        rate_limit_room_rpm=int(_optional("RATE_LIMIT_ROOM_RPM", "20")),
        rate_limit_sender_rpm=int(_optional("RATE_LIMIT_SENDER_RPM", "10")),
        queue_max_events=max(1, int(_optional("QUEUE_MAX_EVENTS", "100"))),
        worker_concurrency=max(1, int(_optional("WORKER_CONCURRENCY", "2"))),
        queue_drain_timeout_s=max(1.0, float(_optional("QUEUE_DRAIN_TIMEOUT_S", "5"))),
        bridge_mixed_room_map=_optional("BRIDGE_MIXED_ROOM_MAP", ""),
        bridge_mixed_defaults=_optional("BRIDGE_MIXED_DEFAULTS", ""),
        max_agents_per_mixed_room=max(1, int(_optional("MAX_AGENTS_PER_MIXED_ROOM", "5"))),
        max_slash_len=max(4, int(_optional("MAX_SLASH_LEN", "32"))),
        unknown_agent_behavior=_optional("UNKNOWN_AGENT_BEHAVIOR", "ignore"),
        mixed_concurrency_cap=max(0, int(_optional("MIXED_CONCURRENCY_CAP", "1"))),
        bridge_operator_allowlist=_optional("BRIDGE_OPERATOR_ALLOWLIST", ""),
        bridge_control_rooms=_optional("BRIDGE_CONTROL_ROOMS", ""),
        control_unauthorized_behavior=_optional("CONTROL_UNAUTHORIZED_BEHAVIOR", "ignore"),
        sofiia_control_token=_optional("SOFIIA_CONTROL_TOKEN", ""),
        control_room_rpm=max(0, int(_optional("CONTROL_ROOM_RPM", "60"))),
        control_operator_rpm=max(0, int(_optional("CONTROL_OPERATOR_RPM", "30"))),
        control_run_next_rpm=max(0, int(_optional("CONTROL_RUN_NEXT_RPM", "20"))),
        control_cooldown_s=max(0.0, float(_optional("CONTROL_COOLDOWN_S", "2.0"))),
        persistent_dedupe=_optional("PERSISTENT_DEDUPE", "1").strip() not in ("0", "false", ""),
        bridge_data_dir=_optional("BRIDGE_DATA_DIR", "/app/data"),
        processed_events_ttl_h=max(1, int(_optional("PROCESSED_EVENTS_TTL_H", "48"))),
        processed_events_prune_batch=max(1, int(_optional("PROCESSED_EVENTS_PRUNE_BATCH", "5000"))),
        processed_events_prune_interval_s=max(0, int(_optional("PROCESSED_EVENTS_PRUNE_INTERVAL_S", "3600"))),
        discovery_rpm=max(0, int(_optional("DISCOVERY_RPM", "20"))),
        bridge_allowed_nodes=_optional("BRIDGE_ALLOWED_NODES", "NODA1"),
        bridge_default_node=_optional("BRIDGE_DEFAULT_NODE", "NODA1"),
        bridge_room_node_map=_optional("BRIDGE_ROOM_NODE_MAP", ""),
        # M8.0: node health thresholds
        node_fail_consecutive=max(1, int(_optional("NODE_FAIL_CONSEC", "3"))),
        node_lat_ewma_s=max(0.5, float(_optional("NODE_LAT_EWMA_S", "12.0"))),
        node_ewma_alpha=min(1.0, max(0.01, float(_optional("NODE_EWMA_ALPHA", "0.3")))),
        # M8.1: sticky failover TTL (0 = disabled)
        failover_sticky_ttl_s=max(0.0, float(_optional("FAILOVER_STICKY_TTL_S", "300.0"))),
        # M8.2: HA state persistence
        ha_health_snapshot_interval_s=max(0, int(_optional("HA_HEALTH_SNAPSHOT_INTERVAL_S", "60"))),
        ha_health_max_age_s=max(0, int(_optional("HA_HEALTH_MAX_AGE_S", "600"))),
        # M9.0: Two-step confirmation TTL (0 = disabled)
        confirm_ttl_s=max(0.0, float(_optional("CONFIRM_TTL_S", "120.0"))),
        policy_export_retention_days=max(0, int(_optional("POLICY_EXPORT_RETENTION_DAYS", "30"))),
        policy_history_limit=max(0, int(_optional("POLICY_HISTORY_LIMIT", "100"))),
        debug_inject_enabled=_optional("DEBUG_INJECT_ENABLED", "false").lower()
            in ("1", "true", "yes"),
        node_id=_optional("NODE_ID", "NODA1"),
        build_sha=_optional("BUILD_SHA", "dev"),
        build_time=_optional("BUILD_TIME", "local"),
    )
