"""Historical backtest engine for Bitcoin Accumulation Zone scoring."""

import json
import logging
import os
import sys
from collections import defaultdict
from datetime import datetime, timedelta

log = logging.getLogger(__name__)

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE_DIR)

HISTORY_PATH = os.path.join(BASE_DIR, "data", "history.json")
CACHE_PATH = os.path.join(BASE_DIR, "data", "cache.json")

# Score brackets matching the dashboard assessment levels
BRACKETS = [
    (0, 20, "Extreme Caution"),
    (21, 40, "Caution"),
    (41, 55, "Neutral"),
    (56, 70, "Moderate Opportunity"),
    (71, 85, "Strong Accumulation"),
    (86, 100, "Extreme Accumulation"),
]

# Scoring thresholds — replicated from scoring/engine.py for standalone use
METRIC_SCORERS = {
    "fear_greed": {
        "ranges": [[None, 10, 10], [10, 25, 7], [25, 45, 4], [45, 55, 2], [55, 75, 1], [75, None, 0]],
    },
    "puell_multiple": {
        "ranges": [[None, 0.3, 10], [0.3, 0.5, 8], [0.5, 0.8, 5], [0.8, 1.2, 3], [1.2, 2.0, 1], [2.0, None, 0]],
    },
    "mvrv_zscore": {
        "ranges": [[None, 0, 10], [0, 0.5, 8], [0.5, 1.5, 5], [1.5, 3, 2], [3, 5, 1], [5, None, 0]],
    },
    "reserve_risk": {
        "ranges": [[None, 0.002, 10], [0.002, 0.005, 7], [0.005, 0.01, 4], [0.01, 0.02, 2], [0.02, None, 0]],
    },
    "rhodl_ratio": {
        "ranges": [[None, 100, 10], [100, 500, 7], [500, 2000, 4], [2000, 10000, 1], [10000, None, 0]],
    },
    "nupl": {
        "ranges": [[None, 0, 10], [0, 0.25, 7], [0.25, 0.5, 4], [0.5, 0.75, 1], [0.75, None, 0]],
    },
}

# Ratio-based metrics: score based on price vs reference value
RATIO_SCORERS = {
    "price_vs_200w_sma": {
        # pct_above ranges
        "ranges": [[None, 0, 10], [0, 20, 6], [20, 50, 3], [50, 100, 1], [100, None, 0]],
        "price_key": "btc_price",
        "ref_key": "200w_sma",
    },
    "lth_realized_price": {
        "ranges": [[None, 0, 10], [0, 20, 6], [20, 50, 3], [50, None, 1]],
        "price_key": "btc_price",
        "ref_key": "lth_realized_price",
    },
}

# Drawdown scoring
DRAWDOWN_RANGES = [[70, None, 10], [50, 70, 8], [30, 50, 6], [20, 30, 4], [10, 20, 2], [None, 10, 0]]


def _score_range(value, ranges):
    """Score a value using range-based thresholds."""
    if value is None:
        return None
    for low, high, score in ranges:
        low_ok = low is None or value >= low
        high_ok = high is None or value < high
        if low_ok and high_ok:
            return score
    return 0


def _build_daily_index(history):
    """Build a dict mapping metric_key -> {date_str: value} for fast lookup."""
    index = {}
    for key, data in history.items():
        if key.startswith("_") or not isinstance(data, dict) or "dates" not in data:
            continue
        lookup = {}
        for d, v in zip(data["dates"], data["values"]):
            lookup[d] = v
        index[key] = lookup
    return index


def _get_all_dates(index):
    """Get sorted union of all dates across all metrics."""
    all_dates = set()
    for lookup in index.values():
        all_dates.update(lookup.keys())
    return sorted(all_dates)


def _last_known_value(lookup, date, max_lookback=30):
    """Get value for date, or most recent prior value within lookback window."""
    if date in lookup:
        return lookup[date]
    d = datetime.strptime(date, "%Y-%m-%d")
    for i in range(1, max_lookback + 1):
        prev = (d - timedelta(days=i)).strftime("%Y-%m-%d")
        if prev in lookup:
            return lookup[prev]
    return None


def _compute_ath_series(price_lookup, dates):
    """Compute running ATH and drawdown for each date."""
    ath = 0
    drawdowns = {}
    for d in dates:
        p = price_lookup.get(d)
        if p is None:
            continue
        if p > ath:
            ath = p
        if ath > 0:
            drawdowns[d] = ((ath - p) / ath) * 100
    return drawdowns


def score_day(date, index, drawdowns):
    """Score a single day using all available metrics. Returns (composite_score, individual_scores, n_metrics)."""
    scores = []
    details = {}

    # Simple range-based metrics
    for metric_key, cfg in METRIC_SCORERS.items():
        val = _last_known_value(index.get(metric_key, {}), date)
        if val is not None:
            s = _score_range(val, cfg["ranges"])
            if s is not None:
                scores.append(s)
                details[metric_key] = {"value": val, "score": s}

    # Ratio-based metrics (price vs reference)
    for metric_key, cfg in RATIO_SCORERS.items():
        price_val = _last_known_value(index.get(cfg["price_key"], {}), date)
        # Try alternate price keys
        if price_val is None:
            for pk in ["btc_price_coingecko", "btc_price_sma", "btc_price_lth"]:
                price_val = _last_known_value(index.get(pk, {}), date)
                if price_val is not None:
                    break
        ref_val = _last_known_value(index.get(cfg["ref_key"], {}), date)
        if price_val is not None and ref_val is not None and ref_val > 0:
            pct_above = ((price_val - ref_val) / ref_val) * 100
            s = _score_range(pct_above, cfg["ranges"])
            if s is not None:
                scores.append(s)
                details[metric_key] = {"value": pct_above, "score": s}

    # Drawdown
    dd = drawdowns.get(date)
    if dd is not None:
        s = _score_range(dd, DRAWDOWN_RANGES)
        if s is not None:
            scores.append(s)
            details["drawdown"] = {"value": dd, "score": s}

    if not scores:
        return None, details, 0

    composite = sum(scores) / len(scores) * 10
    return round(composite, 1), details, len(scores)


def compute_forward_returns(price_lookup, dates_sorted):
    """Precompute forward returns for all dates."""
    periods = [30, 90, 180, 365]
    returns = {}
    for d in dates_sorted:
        p0 = price_lookup.get(d)
        if p0 is None or p0 <= 0:
            continue
        r = {}
        dt = datetime.strptime(d, "%Y-%m-%d")
        for days in periods:
            future = (dt + timedelta(days=days)).strftime("%Y-%m-%d")
            pf = price_lookup.get(future)
            if pf is not None:
                r[f"{days}d"] = round(((pf - p0) / p0) * 100, 2)
        if r:
            returns[d] = r
    return returns


def compute_max_drawdown_forward(price_lookup, date, window=90):
    """Compute max drawdown within N days after a given date."""
    dt = datetime.strptime(date, "%Y-%m-%d")
    p0 = price_lookup.get(date)
    if p0 is None or p0 <= 0:
        return None
    peak = p0
    max_dd = 0
    for i in range(1, window + 1):
        future = (dt + timedelta(days=i)).strftime("%Y-%m-%d")
        pf = price_lookup.get(future)
        if pf is None:
            continue
        if pf > peak:
            peak = pf
        dd = ((peak - pf) / peak) * 100
        if dd > max_dd:
            max_dd = dd
    return round(max_dd, 2) if max_dd > 0 else 0


def run_backtest():
    """Run the full backtest and return comprehensive results."""
    log.info("Loading historical data...")
    if not os.path.exists(HISTORY_PATH):
        return {"error": "No historical data found. Run history collector first."}

    with open(HISTORY_PATH) as f:
        history = json.load(f)

    index = _build_daily_index(history)

    # Build price lookup (prefer coingecko for completeness)
    price_lookup = {}
    for pk in ["btc_price_coingecko", "btc_price", "btc_price_sma", "btc_price_lth"]:
        if pk in index:
            for d, v in index[pk].items():
                if d not in price_lookup:
                    price_lookup[d] = v

    all_dates = _get_all_dates(index)
    if not all_dates:
        return {"error": "No date data available."}

    log.info("Date range: %s to %s (%d days)", all_dates[0], all_dates[-1], len(all_dates))

    # Compute drawdowns
    drawdowns = _compute_ath_series(price_lookup, all_dates)

    # Precompute forward returns
    log.info("Computing forward returns...")
    fwd_returns = compute_forward_returns(price_lookup, all_dates)

    # Score each day
    log.info("Scoring %d days...", len(all_dates))
    daily_scores = []
    for d in all_dates:
        composite, details, n_metrics = score_day(d, index, drawdowns)
        if composite is not None and n_metrics >= 3:  # Require at least 3 metrics
            price = price_lookup.get(d)
            entry = {
                "date": d,
                "score": composite,
                "n_metrics": n_metrics,
                "price": price,
                "forward_returns": fwd_returns.get(d, {}),
            }
            daily_scores.append(entry)

    if not daily_scores:
        return {"error": "No scored days (insufficient metric overlap)."}

    log.info("Scored %d days with 3+ metrics", len(daily_scores))

    # --- Bracket statistics ---
    bracket_stats = []
    for low, high, label in BRACKETS:
        days_in = [d for d in daily_scores if low <= d["score"] <= high]
        if not days_in:
            bracket_stats.append({
                "range": f"{low}-{high}", "label": label, "days": 0,
            })
            continue

        stats = {"range": f"{low}-{high}", "label": label, "days": len(days_in)}
        for period in ["30d", "90d", "180d", "365d"]:
            returns = [d["forward_returns"][period] for d in days_in if period in d["forward_returns"]]
            if returns:
                returns_sorted = sorted(returns)
                stats[f"avg_{period}"] = round(sum(returns) / len(returns), 2)
                stats[f"median_{period}"] = round(returns_sorted[len(returns_sorted) // 2], 2)
                stats[f"win_rate_{period}"] = round(len([r for r in returns if r > 0]) / len(returns) * 100, 1)
                stats[f"max_gain_{period}"] = round(max(returns), 2)
                stats[f"max_loss_{period}"] = round(min(returns), 2)
                stats[f"n_{period}"] = len(returns)

        # Average max drawdown within 90 days
        dd_list = []
        for d in days_in:
            dd = compute_max_drawdown_forward(price_lookup, d["date"], 90)
            if dd is not None:
                dd_list.append(dd)
        if dd_list:
            stats["avg_max_drawdown_90d"] = round(sum(dd_list) / len(dd_list), 2)

        bracket_stats.append(stats)

    # --- Peak signal events ---
    signal_events = []
    thresholds = [90, 80, 70]
    for thresh in thresholds:
        prev_score = 0
        for d in daily_scores:
            if d["score"] >= thresh and prev_score < thresh:
                event = {
                    "date": d["date"],
                    "score": d["score"],
                    "threshold": thresh,
                    "price": d["price"],
                    "forward_returns": d["forward_returns"],
                }
                # Add future prices
                if d["price"]:
                    dt = datetime.strptime(d["date"], "%Y-%m-%d")
                    for days_ahead in [30, 90, 365]:
                        future = (dt + timedelta(days=days_ahead)).strftime("%Y-%m-%d")
                        fp = price_lookup.get(future)
                        if fp:
                            event[f"price_{days_ahead}d"] = round(fp, 2)
                signal_events.append(event)
            prev_score = d["score"]

    signal_events.sort(key=lambda e: e["date"])

    # --- Current signal context ---
    all_scores_list = [d["score"] for d in daily_scores]
    all_scores_list.sort()

    # Get current score from cache
    current_score = None
    current_price = None
    if os.path.exists(CACHE_PATH):
        try:
            with open(CACHE_PATH) as f:
                cache = json.load(f)
            scored = cache.get("_scored", {})
            current_score = scored.get("composite_score")
            current_price = cache.get("price", {}).get("price")
        except Exception:
            pass

    # If no cache, use latest daily score
    if current_score is None and daily_scores:
        current_score = daily_scores[-1]["score"]
        current_price = daily_scores[-1].get("price")

    current_context = None
    if current_score is not None:
        # Percentile
        below = len([s for s in all_scores_list if s <= current_score])
        percentile = round(below / len(all_scores_list) * 100, 1)

        # Find comparable historical periods
        comparable = []
        margin = 5
        for d in daily_scores:
            if abs(d["score"] - current_score) <= margin and d["forward_returns"]:
                comparable.append(d)

        avg_1yr = None
        if comparable:
            yr_returns = [d["forward_returns"]["365d"] for d in comparable if "365d" in d["forward_returns"]]
            if yr_returns:
                avg_1yr = round(sum(yr_returns) / len(yr_returns), 2)

        # Best comparable examples (most recent 5)
        examples = []
        for d in comparable[-5:]:
            examples.append({
                "date": d["date"],
                "score": d["score"],
                "price": d["price"],
                "forward_returns": d["forward_returns"],
            })

        current_context = {
            "current_score": current_score,
            "current_price": current_price,
            "percentile": percentile,
            "comparable_days": len(comparable),
            "avg_1yr_return": avg_1yr,
            "examples": examples,
        }

    # --- Build time series for charting ---
    # Downsample to weekly for chart efficiency
    chart_data = []
    for i, d in enumerate(daily_scores):
        # Include every 7th day + last day
        if i % 7 == 0 or i == len(daily_scores) - 1:
            chart_data.append({
                "date": d["date"],
                "score": d["score"],
                "price": d["price"],
            })

    result = {
        "date_range": {"start": daily_scores[0]["date"], "end": daily_scores[-1]["date"]},
        "total_days_scored": len(daily_scores),
        "bracket_stats": bracket_stats,
        "signal_events": signal_events,
        "current_context": current_context,
        "chart_data": chart_data,
        "computed_at": datetime.utcnow().isoformat() + "Z",
    }

    log.info("Backtest complete: %d days, %d signal events", len(daily_scores), len(signal_events))
    return result
