"""Hypothesis lifecycle framework with SQLite persistence and Bayesian updating."""

from __future__ import annotations

import json
import logging
import os
import sqlite3
import threading
import time
import uuid
from dataclasses import dataclass, field
from enum import Enum

from horizon._horizon import benjamini_hochberg, deflated_sharpe

logger = logging.getLogger("horizon.fund.hypothesis")


class HypothesisState(Enum):
    """Lifecycle states for a trading hypothesis."""
    FORMED = "FORMED"
    BACKTESTING = "BACKTESTING"
    VALIDATED = "VALIDATED"
    MONITORING = "MONITORING"
    DECAYING = "DECAYING"
    RETIRED = "RETIRED"
    INVALIDATED = "INVALIDATED"


@dataclass
class Hypothesis:
    """A trading hypothesis with Bayesian confidence tracking."""
    hypothesis_id: str
    market_id: str
    statement: str
    direction: str  # "yes" or "no"
    state: HypothesisState
    edge_estimate: float
    confidence: float  # Bayesian posterior
    evidence_count: int
    source: str
    backtest_sharpe: float | None
    backtest_pvalue: float | None
    walk_forward_pct: float | None
    competing_ids: list[str]
    formed_at: float
    updated_at: float


class HypothesisManager:
    """Manages the full hypothesis lifecycle with SQLite persistence.

    Hypotheses progress through states:
        FORMED -> BACKTESTING -> VALIDATED -> MONITORING -> DECAYING -> RETIRED
                                                                    -> INVALIDATED

    Bayesian confidence updates incorporate observed returns as evidence.
    Multiple testing correction via Benjamini-Hochberg prevents data snooping.
    Deflated Sharpe ratio adjusts backtest results for the number of trials.
    """

    _SCHEMA = """
    CREATE TABLE IF NOT EXISTS hypotheses (
        hypothesis_id TEXT PRIMARY KEY,
        market_id TEXT NOT NULL,
        statement TEXT NOT NULL,
        direction TEXT NOT NULL,
        state TEXT NOT NULL,
        edge_estimate REAL NOT NULL,
        confidence REAL NOT NULL,
        evidence_count INTEGER DEFAULT 0,
        source TEXT DEFAULT '',
        backtest_sharpe REAL,
        backtest_pvalue REAL,
        walk_forward_pct REAL,
        competing_ids TEXT DEFAULT '[]',
        formed_at REAL NOT NULL,
        updated_at REAL NOT NULL
    );
    CREATE INDEX IF NOT EXISTS idx_hyp_market ON hypotheses(market_id);
    CREATE INDEX IF NOT EXISTS idx_hyp_state ON hypotheses(state);
    """

    def __init__(self, db_path: str | None = None) -> None:
        self._db_path = db_path or os.path.join(
            os.path.expanduser("~"), ".horizon", "fund_memory.db"
        )
        db_dir = os.path.dirname(self._db_path)
        if db_dir:
            os.makedirs(db_dir, exist_ok=True)
        self._lock = threading.Lock()
        self._conn = sqlite3.connect(self._db_path, check_same_thread=False)
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._conn.execute("PRAGMA foreign_keys=ON")
        self._conn.executescript(self._SCHEMA)
        self._conn.commit()
        if db_dir:
            os.chmod(self._db_path, 0o600)

    def close(self) -> None:
        """Close the SQLite connection."""
        with self._lock:
            self._conn.close()

    # ------------------------------------------------------------------
    # Lifecycle methods
    # ------------------------------------------------------------------

    def form(
        self,
        market_id: str,
        statement: str,
        direction: str,
        edge: float,
        confidence: float,
        source: str = "",
    ) -> Hypothesis:
        """Form a new hypothesis.

        Args:
            market_id: Target market identifier.
            statement: Human-readable hypothesis statement.
            direction: Expected direction ("yes" or "no").
            edge: Estimated edge (0-1 scale).
            confidence: Initial confidence (Bayesian prior).
            source: Origin of the hypothesis (e.g., "research", "signal").

        Returns:
            The newly created Hypothesis.
        """
        now = time.time()
        hyp = Hypothesis(
            hypothesis_id=str(uuid.uuid4()),
            market_id=market_id,
            statement=statement,
            direction=direction,
            state=HypothesisState.FORMED,
            edge_estimate=edge,
            confidence=max(0.05, min(0.95, confidence)),
            evidence_count=0,
            source=source,
            backtest_sharpe=None,
            backtest_pvalue=None,
            walk_forward_pct=None,
            competing_ids=[],
            formed_at=now,
            updated_at=now,
        )
        self._save(hyp)
        logger.info(
            "Formed hypothesis %s for %s: '%s' (edge=%.3f conf=%.3f)",
            hyp.hypothesis_id[:8], market_id, statement[:60], edge, hyp.confidence,
        )
        return hyp

    def update_evidence(
        self,
        hypothesis_id: str,
        observed_return: float,
    ) -> Hypothesis | None:
        """Update hypothesis confidence with Bayesian evidence.

        Likelihood is 0.7 if observed return aligns with direction
        (positive for "yes", negative for "no"), else 0.3.

        Args:
            hypothesis_id: ID of the hypothesis to update.
            observed_return: Observed market return.

        Returns:
            Updated Hypothesis, or None if not found.
        """
        hyp = self._load(hypothesis_id)
        if hyp is None:
            return None

        prior = hyp.confidence

        # Determine if evidence aligns with hypothesis direction
        if hyp.direction == "yes":
            aligns = observed_return > 0
        else:
            aligns = observed_return < 0

        likelihood = 0.7 if aligns else 0.3

        # Bayesian update
        posterior = (prior * likelihood) / (
            prior * likelihood + (1 - prior) * (1 - likelihood)
        )

        hyp.confidence = max(0.05, min(0.95, posterior))
        hyp.evidence_count += 1
        hyp.updated_at = time.time()
        self._save(hyp)

        logger.debug(
            "Evidence update %s: prior=%.3f -> posterior=%.3f (return=%.4f, aligns=%s)",
            hypothesis_id[:8], prior, hyp.confidence, observed_return, aligns,
        )
        return hyp

    def validate(
        self,
        hypothesis_id: str,
        backtest_sharpe: float,
        p_value: float,
        walk_forward_pct: float,
    ) -> Hypothesis | None:
        """Validate a hypothesis against backtest criteria.

        Criteria for VALIDATED state:
        - backtest_sharpe >= 1.0
        - p_value <= 0.05
        - walk_forward_pct >= 0.6

        Also applies deflated Sharpe with n_trials from the count
        of all hypotheses for the same market.

        Args:
            hypothesis_id: ID of the hypothesis.
            backtest_sharpe: Sharpe ratio from backtest.
            p_value: Statistical significance of backtest.
            walk_forward_pct: Fraction of walk-forward windows positive.

        Returns:
            Updated Hypothesis, or None if not found.
        """
        hyp = self._load(hypothesis_id)
        if hyp is None:
            return None

        hyp.backtest_sharpe = backtest_sharpe
        hyp.backtest_pvalue = p_value
        hyp.walk_forward_pct = walk_forward_pct
        hyp.updated_at = time.time()

        # Count all hypotheses for the same market (n_trials for deflated Sharpe)
        with self._lock:
            row = self._conn.execute(
                "SELECT COUNT(*) FROM hypotheses WHERE market_id = ?",
                (hyp.market_id,),
            ).fetchone()
        n_trials = max(row[0] if row else 1, 1)

        # Apply deflated Sharpe ratio
        ds_pvalue = deflated_sharpe(
            sharpe=backtest_sharpe,
            n_obs=max(hyp.evidence_count, 100),
            n_trials=n_trials,
            skew=0.0,
            kurt=3.0,
        )

        # Check validation criteria
        passes = (
            backtest_sharpe >= 1.0
            and p_value <= 0.05
            and walk_forward_pct >= 0.6
            and ds_pvalue <= 0.05
        )

        if passes:
            hyp.state = HypothesisState.VALIDATED
            logger.info(
                "Hypothesis %s VALIDATED: sharpe=%.2f p=%.4f wf=%.2f ds_p=%.4f",
                hypothesis_id[:8], backtest_sharpe, p_value,
                walk_forward_pct, ds_pvalue,
            )
        else:
            logger.info(
                "Hypothesis %s validation failed: sharpe=%.2f p=%.4f wf=%.2f ds_p=%.4f",
                hypothesis_id[:8], backtest_sharpe, p_value,
                walk_forward_pct, ds_pvalue,
            )

        self._save(hyp)
        return hyp

    def start_monitoring(self, hypothesis_id: str) -> Hypothesis | None:
        """Transition a hypothesis to MONITORING state.

        Args:
            hypothesis_id: ID of the hypothesis.

        Returns:
            Updated Hypothesis, or None if not found.
        """
        hyp = self._load(hypothesis_id)
        if hyp is None:
            return None

        hyp.state = HypothesisState.MONITORING
        hyp.updated_at = time.time()
        self._save(hyp)
        logger.info("Hypothesis %s -> MONITORING", hypothesis_id[:8])
        return hyp

    def check_decay(self, hypothesis_id: str, current_edge: float) -> bool:
        """Check if a hypothesis edge has decayed significantly.

        If current_edge < 30% of original edge_estimate, transitions
        to DECAYING state.

        Args:
            hypothesis_id: ID of the hypothesis.
            current_edge: Current observed edge.

        Returns:
            True if decaying, False otherwise.
        """
        hyp = self._load(hypothesis_id)
        if hyp is None:
            return False

        if current_edge < 0.3 * hyp.edge_estimate:
            hyp.state = HypothesisState.DECAYING
            hyp.updated_at = time.time()
            self._save(hyp)
            logger.warning(
                "Hypothesis %s DECAYING: edge %.4f < 30%% of original %.4f",
                hypothesis_id[:8], current_edge, hyp.edge_estimate,
            )
            return True

        # Only ratchet the baseline upward, never down — prevents decay threshold erosion
        hyp.edge_estimate = max(hyp.edge_estimate, current_edge)
        hyp.updated_at = time.time()
        self._save(hyp)
        return False

    def retire(self, hypothesis_id: str, reason: str = "") -> Hypothesis | None:
        """Retire a hypothesis.

        Args:
            hypothesis_id: ID of the hypothesis.
            reason: Optional reason for retirement.

        Returns:
            Updated Hypothesis, or None if not found.
        """
        hyp = self._load(hypothesis_id)
        if hyp is None:
            return None

        hyp.state = HypothesisState.RETIRED
        hyp.updated_at = time.time()
        self._save(hyp)
        logger.info("Hypothesis %s RETIRED: %s", hypothesis_id[:8], reason or "no reason")
        return hyp

    def invalidate(self, hypothesis_id: str, reason: str = "") -> Hypothesis | None:
        """Invalidate a hypothesis.

        Args:
            hypothesis_id: ID of the hypothesis.
            reason: Optional reason for invalidation.

        Returns:
            Updated Hypothesis, or None if not found.
        """
        hyp = self._load(hypothesis_id)
        if hyp is None:
            return None

        hyp.state = HypothesisState.INVALIDATED
        hyp.updated_at = time.time()
        self._save(hyp)
        logger.info("Hypothesis %s INVALIDATED: %s", hypothesis_id[:8], reason or "no reason")
        return hyp

    # ------------------------------------------------------------------
    # Query methods
    # ------------------------------------------------------------------

    def best_hypothesis(self, market_id: str) -> Hypothesis | None:
        """Get the best active hypothesis for a market.

        Ranks by confidence * edge_estimate among non-retired/invalidated
        hypotheses.

        Args:
            market_id: Market to query.

        Returns:
            Best Hypothesis, or None if none found.
        """
        excluded = (HypothesisState.RETIRED.value, HypothesisState.INVALIDATED.value)
        with self._lock:
            rows = self._conn.execute(
                "SELECT * FROM hypotheses WHERE market_id = ? AND state NOT IN (?, ?)",
                (market_id, *excluded),
            ).fetchall()

        if not rows:
            return None

        hypotheses = [self._row_to_hypothesis(r) for r in rows]
        return max(hypotheses, key=lambda h: h.confidence * h.edge_estimate)

    def active_hypotheses(
        self,
        state: HypothesisState | None = None,
        limit: int = 50,
    ) -> list[Hypothesis]:
        """Get active hypotheses, optionally filtered by state.

        Args:
            state: Filter by specific state, or None for all non-terminal.
            limit: Maximum number of results.

        Returns:
            List of Hypothesis objects.
        """
        if state is not None:
            with self._lock:
                rows = self._conn.execute(
                    "SELECT * FROM hypotheses WHERE state = ? "
                    "ORDER BY updated_at DESC LIMIT ?",
                    (state.value, limit),
                ).fetchall()
        else:
            excluded = (HypothesisState.RETIRED.value, HypothesisState.INVALIDATED.value)
            with self._lock:
                rows = self._conn.execute(
                    "SELECT * FROM hypotheses WHERE state NOT IN (?, ?) "
                    "ORDER BY updated_at DESC LIMIT ?",
                    (*excluded, limit),
                ).fetchall()

        return [self._row_to_hypothesis(r) for r in rows]

    def to_deploy_candidate(self, hypothesis: Hypothesis) -> dict:
        """Convert a hypothesis to a dict compatible with DeployCandidate fields.

        Args:
            hypothesis: The hypothesis to convert.

        Returns:
            Dict with fields matching DeployCandidate.
        """
        return {
            "market_id": hypothesis.market_id,
            "exchange": "",
            "strategy_type": "",
            "edge_estimate": hypothesis.edge_estimate,
            "confidence": hypothesis.confidence,
            "backtest_sharpe": hypothesis.backtest_sharpe or 0.0,
            "backtest_trades": hypothesis.evidence_count,
            "backtest_drawdown_pct": 0.0,
            "backtest_profit_factor": 0.0,
            "robustness_p_value": hypothesis.backtest_pvalue or 1.0,
            "walk_forward_positive_pct": hypothesis.walk_forward_pct or 0.0,
        }

    def multiple_testing_correction(self, market_id: str) -> list[Hypothesis]:
        """Apply Benjamini-Hochberg correction across hypotheses for a market.

        Filters hypotheses that have backtest p-values and returns only
        those that survive multiple testing correction at alpha=0.05.

        Args:
            market_id: Market to apply correction for.

        Returns:
            List of hypotheses that survive correction.
        """
        with self._lock:
            rows = self._conn.execute(
                "SELECT * FROM hypotheses WHERE market_id = ? AND backtest_pvalue IS NOT NULL",
                (market_id,),
            ).fetchall()

        if not rows:
            return []

        hypotheses = [self._row_to_hypothesis(r) for r in rows]
        p_values = [h.backtest_pvalue for h in hypotheses]

        # All p-values are non-None (filtered by SQL), but satisfy type checker
        p_values_clean = [p for p in p_values if p is not None]
        if not p_values_clean:
            return []

        rejected = benjamini_hochberg(p_values_clean, 0.05)

        survivors = []
        for hyp, survives in zip(hypotheses, rejected):
            if survives:
                survivors.append(hyp)

        logger.info(
            "Multiple testing correction for %s: %d/%d survive BH at alpha=0.05",
            market_id, len(survivors), len(hypotheses),
        )
        return survivors

    # ------------------------------------------------------------------
    # Persistence helpers
    # ------------------------------------------------------------------

    def _load(self, hypothesis_id: str) -> Hypothesis | None:
        """Load a hypothesis from SQLite by ID."""
        with self._lock:
            row = self._conn.execute(
                "SELECT * FROM hypotheses WHERE hypothesis_id = ?",
                (hypothesis_id,),
            ).fetchone()

        if row is None:
            return None
        return self._row_to_hypothesis(row)

    def _save(self, hypothesis: Hypothesis) -> None:
        """Upsert a hypothesis to SQLite."""
        competing_json = json.dumps(hypothesis.competing_ids)
        with self._lock:
            self._conn.execute(
                """INSERT INTO hypotheses (
                    hypothesis_id, market_id, statement, direction, state,
                    edge_estimate, confidence, evidence_count, source,
                    backtest_sharpe, backtest_pvalue, walk_forward_pct,
                    competing_ids, formed_at, updated_at
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                ON CONFLICT(hypothesis_id) DO UPDATE SET
                    state = excluded.state,
                    edge_estimate = excluded.edge_estimate,
                    confidence = excluded.confidence,
                    evidence_count = excluded.evidence_count,
                    backtest_sharpe = excluded.backtest_sharpe,
                    backtest_pvalue = excluded.backtest_pvalue,
                    walk_forward_pct = excluded.walk_forward_pct,
                    competing_ids = excluded.competing_ids,
                    updated_at = excluded.updated_at""",
                (
                    hypothesis.hypothesis_id,
                    hypothesis.market_id,
                    hypothesis.statement,
                    hypothesis.direction,
                    hypothesis.state.value,
                    hypothesis.edge_estimate,
                    hypothesis.confidence,
                    hypothesis.evidence_count,
                    hypothesis.source,
                    hypothesis.backtest_sharpe,
                    hypothesis.backtest_pvalue,
                    hypothesis.walk_forward_pct,
                    competing_json,
                    hypothesis.formed_at,
                    hypothesis.updated_at,
                ),
            )
            self._conn.commit()

    def _row_to_hypothesis(self, row: tuple) -> Hypothesis:
        """Convert a SQLite row tuple to a Hypothesis object."""
        return Hypothesis(
            hypothesis_id=row[0],
            market_id=row[1],
            statement=row[2],
            direction=row[3],
            state=HypothesisState(row[4]),
            edge_estimate=row[5],
            confidence=row[6],
            evidence_count=row[7],
            source=row[8],
            backtest_sharpe=row[9],
            backtest_pvalue=row[10],
            walk_forward_pct=row[11],
            competing_ids=json.loads(row[12]) if row[12] else [],
            formed_at=row[13],
            updated_at=row[14],
        )
