"""Persistent strategy memory: records outcomes, patterns, and learnings across sessions."""

from __future__ import annotations

import json
import logging
import os
import sqlite3
import threading
import time
from dataclasses import dataclass, field
from typing import Any

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


@dataclass(frozen=True)
class MemoryEntry:
    """A single memory record."""
    entry_id: int
    category: str
    key: str
    value: str
    confidence: float
    created_at: float
    updated_at: float
    metadata: dict[str, Any] = field(default_factory=dict)


class StrategyMemory:
    """Persistent memory for the autonomous fund.

    Stores strategy outcomes, market patterns, regime sensitivities,
    and learnings in SQLite so they survive restarts. The LLM queries
    this to avoid repeating mistakes and to leverage past successes.

    Categories:
    - strategy_outcome: what happened when we ran strategy X on market Y
    - market_pattern: observed pattern for a market type
    - regime_mapping: which strategies work in which regimes
    - edge_decay: tracked edge decay for a strategy/market
    - failure_reason: why a strategy failed
    - general: uncategorized learnings
    """

    _SCHEMA = """
    CREATE TABLE IF NOT EXISTS memory (
        entry_id INTEGER PRIMARY KEY AUTOINCREMENT,
        category TEXT NOT NULL,
        key TEXT NOT NULL,
        value TEXT NOT NULL,
        confidence REAL DEFAULT 0.5,
        created_at REAL NOT NULL,
        updated_at REAL NOT NULL,
        metadata_json TEXT DEFAULT '{}',
        UNIQUE(category, key)
    );
    CREATE INDEX IF NOT EXISTS idx_memory_category ON memory(category);
    CREATE INDEX IF NOT EXISTS idx_memory_key ON memory(key);
    CREATE INDEX IF NOT EXISTS idx_memory_updated ON memory(updated_at);
    """

    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:
        with self._lock:
            self._conn.close()

    def record(
        self,
        category: str,
        key: str,
        value: str,
        confidence: float = 0.5,
        metadata: dict[str, Any] | None = None,
    ) -> int:
        """Record or update a memory entry. Returns entry_id."""
        now = time.time()
        conf = max(0.0, min(1.0, confidence))
        meta_json = json.dumps(metadata or {}, default=str)

        with self._lock:
            cursor = self._conn.execute(
                """INSERT INTO memory (category, key, value, confidence, created_at, updated_at, metadata_json)
                   VALUES (?, ?, ?, ?, ?, ?, ?)
                   ON CONFLICT(category, key) DO UPDATE SET
                       value = excluded.value,
                       confidence = excluded.confidence,
                       updated_at = excluded.updated_at,
                       metadata_json = excluded.metadata_json""",
                (category, key, value, conf, now, now, meta_json),
            )
            self._conn.commit()
            return cursor.lastrowid or 0

    def record_strategy_outcome(
        self,
        strategy_name: str,
        market_type: str,
        pnl: float,
        sharpe: float,
        duration_days: float,
        notes: str = "",
    ) -> int:
        """Record the outcome of a strategy deployment."""
        return self.record(
            category="strategy_outcome",
            key=f"{strategy_name}:{market_type}:{time.time():.0f}",
            value=notes or f"PnL={pnl:.2f} Sharpe={sharpe:.2f} Duration={duration_days:.1f}d",
            confidence=min(max(sharpe / 3.0, 0.0), 1.0),
            metadata={
                "strategy": strategy_name,
                "market_type": market_type,
                "pnl": pnl,
                "sharpe": sharpe,
                "duration_days": duration_days,
            },
        )

    def record_failure(
        self,
        strategy_name: str,
        reason: str,
        market_id: str = "",
        details: dict[str, Any] | None = None,
    ) -> int:
        """Record why a strategy failed."""
        return self.record(
            category="failure_reason",
            key=f"{strategy_name}:{reason}:{time.time():.0f}",
            value=reason,
            confidence=0.7,
            metadata={"strategy": strategy_name, "market_id": market_id, **(details or {})},
        )

    def record_pattern(
        self,
        market_type: str,
        pattern: str,
        confidence: float = 0.5,
    ) -> int:
        """Record an observed market pattern."""
        return self.record(
            category="market_pattern",
            key=f"{market_type}:{pattern[:64]}",
            value=pattern,
            confidence=confidence,
        )

    def record_regime_mapping(
        self,
        regime: str,
        strategy_type: str,
        effectiveness: float,
        notes: str = "",
    ) -> int:
        """Record which strategies work in which regimes."""
        return self.record(
            category="regime_mapping",
            key=f"{regime}:{strategy_type}",
            value=notes or f"Effectiveness={effectiveness:.2f}",
            confidence=effectiveness,
            metadata={"regime": regime, "strategy_type": strategy_type, "effectiveness": effectiveness},
        )

    def record_edge_decay(
        self,
        strategy_name: str,
        initial_edge: float,
        current_edge: float,
        days_elapsed: float,
    ) -> int:
        """Track edge decay for a strategy."""
        decay_rate = (initial_edge - current_edge) / max(days_elapsed, 1.0)
        return self.record(
            category="edge_decay",
            key=strategy_name,
            value=f"Edge {initial_edge:.3f} -> {current_edge:.3f} over {days_elapsed:.0f}d",
            confidence=max(0.0, current_edge),
            metadata={
                "strategy": strategy_name,
                "initial_edge": initial_edge,
                "current_edge": current_edge,
                "days_elapsed": days_elapsed,
                "decay_rate_per_day": decay_rate,
            },
        )

    def query(
        self,
        category: str | None = None,
        key_contains: str | None = None,
        min_confidence: float = 0.0,
        limit: int = 50,
        apply_decay: bool = True,
        memory_half_life_days: float = 90.0,
    ) -> list[MemoryEntry]:
        """Query memory entries with optional filters.

        Args:
            category: Filter by memory category.
            key_contains: Filter by key substring.
            min_confidence: Minimum confidence threshold (applied AFTER decay if enabled).
            limit: Max entries to return.
            apply_decay: If True, decay confidence based on age (recency bias prevention).
            memory_half_life_days: Half-life for confidence decay in days.

        Returns:
            List of MemoryEntry, with confidence decayed if apply_decay=True.
        """
        conditions = []
        params: list[Any] = []

        if category is not None:
            conditions.append("category = ?")
            params.append(category)
        if key_contains is not None:
            conditions.append("key LIKE ?")
            params.append(f"%{key_contains}%")
        # When decay is active, fetch more rows and filter after decay
        if min_confidence > 0 and not apply_decay:
            conditions.append("confidence >= ?")
            params.append(min_confidence)

        where = " AND ".join(conditions) if conditions else "1=1"
        fetch_limit = limit * 3 if apply_decay and min_confidence > 0 else limit
        sql = f"SELECT entry_id, category, key, value, confidence, created_at, updated_at, metadata_json FROM memory WHERE {where} ORDER BY updated_at DESC LIMIT ?"
        params.append(fetch_limit)

        with self._lock:
            rows = self._conn.execute(sql, params).fetchall()

        now = time.time()
        entries: list[MemoryEntry] = []
        for r in rows:
            conf = r[4]
            if apply_decay and memory_half_life_days > 0:
                age_days = (now - r[6]) / 86400.0
                if age_days > 0:
                    import math
                    conf = conf * math.pow(0.5, age_days / memory_half_life_days)

            if min_confidence > 0 and conf < min_confidence:
                continue

            entries.append(MemoryEntry(
                entry_id=r[0],
                category=r[1],
                key=r[2],
                value=r[3],
                confidence=round(conf, 6),
                created_at=r[5],
                updated_at=r[6],
                metadata=json.loads(r[7]) if r[7] else {},
            ))
            if len(entries) >= limit:
                break

        return entries

    def similar_strategies(self, market_type: str, limit: int = 10) -> list[MemoryEntry]:
        """Find past strategy outcomes for similar market types."""
        return self.query(
            category="strategy_outcome",
            key_contains=market_type,
            limit=limit,
        )

    def past_failures(self, strategy_name: str | None = None, limit: int = 20) -> list[MemoryEntry]:
        """Retrieve past failure records."""
        return self.query(
            category="failure_reason",
            key_contains=strategy_name,
            limit=limit,
        )

    def regime_strategies(self, regime: str) -> list[MemoryEntry]:
        """Find strategies that worked well in a given regime."""
        return self.query(
            category="regime_mapping",
            key_contains=regime,
            min_confidence=0.3,
        )

    def forget(self, entry_id: int) -> bool:
        """Delete a specific memory entry."""
        with self._lock:
            cursor = self._conn.execute("DELETE FROM memory WHERE entry_id = ?", (entry_id,))
            self._conn.commit()
            return cursor.rowcount > 0

    def prune(self, max_age_days: float = 365.0, min_confidence: float = 0.1) -> int:
        """Remove old, low-confidence entries."""
        cutoff = time.time() - max_age_days * 86400
        with self._lock:
            cursor = self._conn.execute(
                "DELETE FROM memory WHERE updated_at < ? AND confidence < ?",
                (cutoff, min_confidence),
            )
            self._conn.commit()
            return cursor.rowcount

    def auto_prune(self, max_entries: int = 5000) -> int:
        """Prune if total entries exceed max. Removes oldest low-confidence first."""
        stats = self.stats()
        total = stats["total_entries"]
        if total <= max_entries:
            return 0
        excess = total - max_entries
        with self._lock:
            cursor = self._conn.execute(
                "DELETE FROM memory WHERE entry_id IN "
                "(SELECT entry_id FROM memory ORDER BY confidence ASC, updated_at ASC LIMIT ?)",
                (excess,),
            )
            self._conn.commit()
            return cursor.rowcount

    def stats(self) -> dict[str, Any]:
        """Return memory statistics."""
        with self._lock:
            rows = self._conn.execute(
                "SELECT category, COUNT(*), AVG(confidence) FROM memory GROUP BY category"
            ).fetchall()
        total = sum(r[1] for r in rows)
        return {
            "total_entries": total,
            "by_category": {r[0]: {"count": r[1], "avg_confidence": round(r[2], 3)} for r in rows},
        }

    def export(self) -> list[dict[str, Any]]:
        """Export all memory entries as dicts."""
        entries = self.query(limit=10_000)
        return [
            {
                "entry_id": e.entry_id,
                "category": e.category,
                "key": e.key,
                "value": e.value,
                "confidence": e.confidence,
                "created_at": e.created_at,
                "updated_at": e.updated_at,
                "metadata": e.metadata,
            }
            for e in entries
        ]
