"""lesson_reflector.py - Continuous reflection engine.

Analyses closed trade batches to extract winning and losing patterns,
generates concise lessons (offensive + defensive), and persists them
for prompt injection into future LLM calls.
"""

from __future__ import annotations

import json
import logging
import time
from collections import defaultdict
from pathlib import Path
from typing import Any

import config_manager

logger = logging.getLogger(__name__)

_MIN_TRADES_PER_REFLECTION = 3
_MAX_LESSONS_PER_TYPE = 8
_LESSON_TTL_DAYS = 30
_LESSON_TTL_SEC = _LESSON_TTL_DAYS * 86400
_CURSOR_FILE = "reflection_cursor.txt"
_LESSON_FILE = "lessons.json"

# ── LLM prompt templates ──────────────────────────────────────

_OFFENSIVE_PROMPT = """You are a trading coach identifying what strategies are working for a gold (XAUUSD) trader.

Review these recent winning trades and write ONE concise, actionable offensive lesson.
Be specific: mention the market condition, direction, and what to do.

Recent wins in {context_label} ({n} trades, win_rate={wr:.0%}, total_pnl={total_pnl:+.1f}):
{trade_summary}

Rules:
- Be brief (1 sentence, under 100 chars).
- Focus on what to REPLICATE, not avoid.
- Include specific conditions (regime, session, ADX level if consistent).
- If no clear pattern: output {"skip": true}

Output JSON: {{"lesson_type":"OFFENSIVE_FAVOR","lesson_text":"...","confidence":0.0-1.0}}"""

_DEFENSIVE_PROMPT = """You are a trading coach identifying what's causing losses for a gold (XAUUSD) trader.

Review these recent losing trades and write ONE concise, actionable defensive lesson.
Be specific: mention the market condition, direction, and what to do differently.

Recent losses in {context_label} ({n} trades, win_rate={wr:.0%}, total_pnl={total_pnl:+.1f}):
{trade_summary}

Rules:
- Be brief (1 sentence, under 100 chars).
- Focus on what to AVOID or how to ADAPT.
- Include specific conditions (regime, session, ADX level if consistent).
- If no clear pattern: output {"skip": true}

Output JSON: {{"lesson_type":"DEFENSIVE_AVOID","lesson_text":"...","confidence":0.0-1.0}}"""


# ── Persistence ────────────────────────────────────────────────

def _data_dir() -> Path:
    d = config_manager.get_data_dir() / "learning"
    d.mkdir(parents=True, exist_ok=True)
    return d


def _cursor_path() -> Path:
    return _data_dir() / _CURSOR_FILE


def _lessons_path() -> Path:
    return _data_dir() / _LESSON_FILE


def _read_cursor() -> int:
    try:
        p = _cursor_path()
        if p.exists():
            return int(p.read_text(encoding="utf-8").strip())
    except Exception:
        pass
    return 0


def _save_cursor(trade_id: int) -> None:
    try:
        _cursor_path().write_text(str(trade_id), encoding="utf-8")
    except Exception as e:
        logger.warning("[lesson_reflector] save_cursor failed: %s", e)


def _load_lessons() -> list[dict[str, Any]]:
    try:
        p = _lessons_path()
        if p.exists():
            data = json.loads(p.read_text(encoding="utf-8"))
            if isinstance(data, list):
                return data
    except Exception as e:
        logger.warning("[lesson_reflector] load_lessons failed: %s", e)
    return []


def _decay_confidence(lesson: dict) -> float:
    """Apply time-based decay. Confidence reaches zero at _LESSON_TTL_SEC (30 days)."""
    age = time.time() - lesson.get("created_at", time.time())
    if age >= _LESSON_TTL_SEC:
        return 0.0
    ratio = age / _LESSON_TTL_SEC
    return round(max(0.0, lesson.get("confidence", 0.5) * (1.0 - ratio)), 2)



def _update_from_trades(trades: list[dict], lessons: list[dict]) -> None:
    """Update lesson effectiveness based on actual trade outcomes."""
    updated = False
    for t in trades:
        r = str(t.get("regime", "") or "")
        s = str(t.get("session", "") or "")
        d = str(t.get("side", "") or "")
        win = bool(t.get("is_win", False))
        pnl = abs(float(t.get("pnl", 0.0) or 0.0))
        mag = min(1.0, pnl / 50.0)
        for l in lessons:
            if not (l["regime"] == r and l["session"] == s and l["side"] == d):
                continue
            off = l["lesson_type"].startswith("OFFENSIVE")
            if win and off:
                delta = 0.1 * mag
            elif not win and not off:
                delta = 0.1 * mag
            elif win and not off:
                delta = -0.05
            else:
                delta = -0.1 * mag
            l["effectiveness"] = round(max(-1.0, min(1.0, l.get("effectiveness", 0.0) + delta)), 3)
            l["hit_count"] = l.get("hit_count", 0) + 1
            updated = True
    if updated:
        _save_lessons(lessons)
def _save_lessons(lessons: list[dict[str, Any]]) -> None:
    now = time.time()
    # Prune stale + keep max per type
    fresh = [l for l in lessons if (now - l.get("created_at", 0)) < _LESSON_TTL_SEC]
    type_counts: dict[str, int] = defaultdict(int)
    keep: list[dict[str, Any]] = []
    for l in sorted(fresh, key=lambda x: -x.get("effectiveness", 0)):
        lt = l.get("lesson_type", "DEFENSIVE_AVOID")
        if type_counts[lt] < _MAX_LESSONS_PER_TYPE:
            keep.append(l)
            type_counts[lt] += 1
    try:
        _str = json.dumps(keep, ensure_ascii=False, indent=2)
        _lessons_path().write_text(_str, encoding="utf-8")
    except Exception as e:
        logger.warning("[lesson_reflector] save_lessons failed: %s", e)


# ── Pattern extraction ─────────────────────────────────────────

def _extract_offensive_patterns(trades: list[dict]) -> list[dict]:
    """Find clusters of winning trades that suggest a replicable pattern."""
    groups: dict[tuple, list[dict]] = defaultdict(list)
    for t in trades:
        key = (t.get("regime", ""), t.get("session", ""), t.get("side", ""))
        groups[key].append(t)

    patterns = []
    for (regime, session, side), cluster in groups.items():
        if len(cluster) < _MIN_TRADES_PER_RELECTION:
            continue
        wins = sum(1 for t in cluster if t.get("is_win"))
        if wins < 2 or (wins / len(cluster)) < 0.60:
            continue
        total_pnl = sum(float(t.get("pnl", 0.0) or 0.0) for t in cluster)
        if total_pnl <= 0:
            continue
        patterns.append({
            "type": "OFFENSIVE",
            "regime": regime,
            "session": session,
            "side": side,
            "n": len(cluster),
            "wins": wins,
            "wr": wins / len(cluster),
            "total_pnl": total_pnl,
            "trades": cluster,
        })
    return patterns


def _extract_defensive_patterns(trades: list[dict]) -> list[dict]:
    """Find clusters of losing trades that suggest a pattern to avoid."""
    groups: dict[tuple, list[dict]] = defaultdict(list)
    for t in trades:
        key = (t.get("regime", ""), t.get("session", ""), t.get("side", ""))
        groups[key].append(t)

    patterns = []
    for (regime, session, side), cluster in groups.items():
        if len(cluster) < _MIN_TRADES_PER_RELECTION:
            continue
        wins = sum(1 for t in cluster if t.get("is_win"))
        losses = len(cluster) - wins
        if losses < 2 or (wins / len(cluster)) > 0.40:
            continue
        total_pnl = sum(float(t.get("pnl", 0.0) or 0.0) for t in cluster)
        if total_pnl >= 0:
            continue
        patterns.append({
            "type": "DEFENSIVE",
            "regime": regime,
            "session": session,
            "side": side,
            "n": len(cluster),
            "losses": losses,
            "wr": wins / len(cluster),
            "total_pnl": total_pnl,
            "trades": cluster,
        })
    return patterns


def _trade_summary_text(trades: list[dict], limit: int = 5) -> str:
    lines = []
    for t in trades[-limit:]:
        side = t.get("side", "?").upper()
        pnl = float(t.get("pnl", 0.0) or 0.0)
        adx = t.get("adx_value", "?")
        outcome = "WIN" if t.get("is_win") else "LOSS"
        lines.append(f"  {side} PnL={pnl:+.1f} ADX={adx} outcome={outcome}")
    if opportunities:
        lines.append("\n## Low-Experience Opportunities (test with 0.01 lots)")
        for op in opportunities:
            lines.append(f"  \u2192 {op['session']}/{op['side']} in {op['regime']}: {op['wins']}/{op['n']} wins ({op['win_rate']:.0%}), avg PnL {op['avg_pnl']:+.1f}  — consider mini position")
    return "\n".join(lines)


# ── LLM lesson generation ─────────────────────────────────────

def _generate_lesson(pattern: dict, existing_lessons: list[dict]) -> dict | None:
    """Call LLM to generate a concise lesson from a trade cluster."""
    # Deduplicate: skip if similar lesson already exists
    for el in existing_lessons:
        if (el.get("regime") == pattern["regime"]
                and el.get("session") == pattern["session"]
                and el.get("side") == pattern["side"]
                and abs(el.get("created_at", 0) - time.time()) < 86400):
            return None

    prompt_template = _OFFENSIVE_PROMPT if pattern["type"] == "OFFENSIVE" else _DEFENSIVE_PROMPT
    context_label = f"{pattern['regime']}/{pattern['session']}/{pattern['side']}"
    prompt = prompt_template.format(
        context_label=context_label,
        n=pattern["n"],
        wr=pattern["wr"],
        total_pnl=pattern["total_pnl"],
        trade_summary=_trade_summary_text(pattern["trades"]),
    )

    try:
        from src.agent_role_router import ROLE_TREND_ANALYST, call_llm

        content = call_llm(ROLE_TREND_ANALYST, prompt, temperature=0.3, max_tokens=200, timeout=30)
        if not content:
            return None
        from src.json_utils import extract_json_block as _extract
        data = _extract(content)
        if not data or not isinstance(data, dict) or data.get("skip"):
            return None
        lesson_text = str(data.get("lesson_text", "") or "").strip()
        if not lesson_text or len(lesson_text) < 10:
            return None
        return {
            "lesson_type": str(data.get("lesson_type", pattern["type"] + "_AVOID")),
            "lesson_text": lesson_text,
            "confidence": round(float(data.get("confidence", 0.5) or 0.5), 2),
            "regime": pattern["regime"],
            "session": pattern["session"],
            "side": pattern["side"],
            "created_at": time.time(),
            "last_applied": 0.0,
            "effectiveness": 0.0,
            "hit_count": pattern["n"],
            "win_count": pattern.get("wins", 0) or (pattern["n"] - pattern.get("losses", 0)),
        }
    except Exception as e:
        logger.warning("[lesson_reflector] LLM lesson generation failed: %s", e)
        return None


# ── Public API ─────────────────────────────────────────────────

def run_reflection_cycle(brain=None) -> dict[str, Any]:
    """Analyze recent closed trades, generate lessons, persist.

    Returns: {"new_offensive": int, "new_defensive": int, "trades_analyzed": int}
    """
    try:
        from src.learning.trading_brain import get_brain
        brain = brain or get_brain()
    except Exception:
        return {"new_offensive": 0, "new_defensive": 0, "trades_analyzed": 0, "error": "no_brain"}

    cursor = _read_cursor()
    all_trades = brain.query_recent_closed_trades(n=100)
    new_trades = [t for t in all_trades if int(t.get("id", 0) or 0) > cursor]

    if new_trades:
        _update_from_trades(new_trades, _load_lessons())
    if len(new_trades) < _MIN_TRADES_PER_RELECTION:
        if new_trades:
            _save_cursor(int(new_trades[-1].get("id", 0)))
        return {"new_offensive": 0, "new_defensive": 0, "trades_analyzed": len(new_trades)}

    existing = _load_lessons()

    offensive = _extract_offensive_patterns(new_trades)
    defensive = _extract_defensive_patterns(new_trades)

    new_lessons = []
    for pattern in offensive + defensive:
        lesson = _generate_lesson(pattern, existing + new_lessons)
        if lesson:
            new_lessons.append(lesson)

    if new_lessons:
        all_lessons = existing + new_lessons
        _save_lessons(all_lessons)

    if new_trades:
        _save_cursor(int(new_trades[-1].get("id", 0)))

    return {
        "new_offensive": sum(1 for l in new_lessons if l["lesson_type"].startswith("OFFENSIVE")),
        "new_defensive": sum(1 for l in new_lessons if not l["lesson_type"].startswith("OFFENSIVE")),
        "trades_analyzed": len(new_trades),
    }


def get_active_lessons(limit: int = 4, regime: str = "", session: str = "", side: str = "") -> list[dict]:
    """Return most relevant lessons for current market context, with time-decayed confidence."""
    all_lessons = _load_lessons()
    if not all_lessons:
        return []

    scored: list[tuple[float, dict]] = []
    for l in all_lessons:
        l["confidence"] = _decay_confidence(l)
        score = l.get("effectiveness", 0) * 2.0
        if regime and l.get("regime") == regime:
            score += 3.0
        if session and l.get("session") == session:
            score += 2.0
        if side and l.get("side") == side:
            score += 1.0
        scored.append((score, l))

    scored.sort(key=lambda x: -x[0])
    return [l for _, l in scored[:limit] if l["confidence"] > 0.05]


def scan_opportunities(brain=None, limit: int = 3) -> list[dict]:
    """Find low-experience (<5 trades) but high-potential (>=60% win, profitable) patterns.
    These are signals to try small test positions and gather more data."""
    try:
        if brain is None:
            from src.learning.trading_brain import get_brain
            brain = get_brain()
        trades = brain.query_recent_closed_trades(n=200)
        from collections import defaultdict
        groups: dict[tuple, list] = defaultdict(list)
        for t in trades:
            key = (t.get("regime", ""), t.get("session", ""), t.get("side", ""))
            groups[key].append(t)
        ops = []
        for (regime, session, side), cluster in groups.items():
            n = len(cluster)
            if n >= 5 or n < 2:
                continue
            wins = sum(1 for t in cluster if t.get("is_win"))
            total_pnl = sum(float(t.get("pnl", 0.0) or 0.0) for t in cluster)
            wr = wins / n
            if wr >= 0.60 and total_pnl > 0:
                ops.append({"regime": regime, "session": session, "side": side,
                            "n": n, "wins": wins, "win_rate": round(wr, 2),
                            "avg_pnl": round(total_pnl / n, 2)})
        ops.sort(key=lambda x: (-x["win_rate"], x["n"]))
        return ops[:limit]
    except Exception:
        return []


def _format_lessons_for_prompt(lessons: list[dict], opportunities: list[dict] = None) -> str:
    if not lessons and not opportunities:
        return ""
    lines = ["\n## Trading Lessons (accumulated from your own experience)"]
    offensive = [l for l in lessons if l["lesson_type"].startswith("OFFENSIVE")]
    defensive = [l for l in lessons if not l["lesson_type"].startswith("OFFENSIVE")]
    if offensive:
        lines.append("\nWhat has been working for you:")
        for l in offensive:
            lines.append(f"  \u2192 {l['lesson_text']}  [confidence: {l.get('confidence', 0.5):.0%}, eff: {l.get('effectiveness', 0.0):+.1f}]")
    if defensive:
        lines.append("\nWhat to be careful about:")
        for l in defensive:
            lines.append(f"  \u2192 {l['lesson_text']}  [confidence: {l.get('confidence', 0.5):.0%}, eff: {l.get('effectiveness', 0.0):+.1f}]")
    return "\n".join(lines) + "\n"
