#!/usr/bin/env python3
"""
BTC Bottom Indicators Data Fetcher
Pulls data from multiple sources and computes 6 bottom indicators.

Data sources:
- Price: Binance API (data-api.binance.vision)
- Hashrate: blockchain.info API
- Fear & Greed: alternative.me API
- Funding rate: Approximate from market data
"""

import json
import os
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path

import requests

# Paths
ROOT = Path(__file__).parent.parent
DATA_DIR = ROOT / "data"
DATA_DIR.mkdir(exist_ok=True)

# ── API Endpoints ──────────────────────────────────────────────────────────
BINANCE_KLINES = "https://data-api.binance.vision/api/v3/klines"
BLOCKCHAIN_HASHRATE = "https://api.blockchain.info/charts/hash-rate"
FEAR_GREED_API = "https://api.alternative.me/fng/"


def now_utc() -> datetime:
    return datetime.now(timezone.utc)


def save_json(path: Path, data: dict):
    path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")


def load_json(path: Path) -> dict:
    if path.exists():
        return json.loads(path.read_text(encoding="utf-8"))
    return {}


def cached_request(url: str, params: dict = None, cache_key: str = "", ttl_hours: int = 6) -> dict | list:
    """Cache API responses to avoid rate limits."""
    cache_file = DATA_DIR / f"cache_{cache_key}.json"
    cache = load_json(cache_file)

    if cache.get("timestamp"):
        try:
            cached_time = datetime.fromisoformat(cache["timestamp"])
            # Handle both aware and naive timestamps
            if cached_time.tzinfo is None:
                cached_time = cached_time.replace(tzinfo=timezone.utc)
            if now_utc() - cached_time < timedelta(hours=ttl_hours):
                return cache.get("data", {})
        except (ValueError, TypeError):
            pass

    try:
        resp = requests.get(url, params=params or {}, timeout=30)
        resp.raise_for_status()
        data = resp.json()
        save_json(cache_file, {"timestamp": now_utc().isoformat(), "data": data})
        return data
    except Exception as e:
        print(f"[WARN] API error for {cache_key}: {e}")
        if cache.get("data"):
            print(f"[INFO] Using cached data for {cache_key}")
            return cache["data"]
        return {}


# ── Price Data ─────────────────────────────────────────────────────────────


def fetch_binance_klines(symbol: str = "BTCUSDT", interval: str = "1d", limit: int = 500) -> list:
    """Fetch kline data from Binance."""
    params = {"symbol": symbol, "interval": interval, "limit": limit}
    data = cached_request(BINANCE_KLINES, params, cache_key=f"binance_{symbol}_{interval}_{limit}", ttl_hours=6)
    if not data or not isinstance(data, list):
        return []
    return data


def parse_klines(klines: list) -> list[dict]:
    """Parse Binance kline format into structured dicts."""
    result = []
    for row in klines:
        result.append({
            "open_time": row[0],
            "open": float(row[1]),
            "high": float(row[2]),
            "low": float(row[3]),
            "close": float(row[4]),
            "volume": float(row[5]),
            "close_time": row[6],
        })
    return result


# ── Indicator 1: Weekly MA200 ──────────────────────────────────────────────


def fetch_ma200() -> dict:
    """
    Weekly MA200. Fetch daily prices, resample to weekly.
    Signal: price < weekly MA200, and MA slope turning negative.
    """
    klines = fetch_binance_klines("BTCUSDT", "1d", 500)
    if len(klines) < 200:
        return {"error": f"Insufficient data: {len(klines)} candles"}

    daily = parse_klines(klines)

    # Resample to weekly (Sunday-Saturday, take last close of each week)
    weekly_closes = []
    current_week = None
    for candle in daily:
        dt = datetime.fromtimestamp(candle["open_time"] / 1000, tz=timezone.utc)
        week_key = dt.isocalendar()[:2]  # (year, week)
        if week_key != current_week:
            current_week = week_key
            weekly_closes.append(candle["close"])
        else:
            weekly_closes[-1] = candle["close"]

    if len(weekly_closes) < 50:
        return {"error": f"Insufficient weekly data: {len(weekly_closes)} weeks"}

    window = min(200, len(weekly_closes))
    ma200 = sum(weekly_closes[-window:]) / window
    prev_window = min(200, len(weekly_closes) - 1)
    prev_ma200 = sum(weekly_closes[-(prev_window + 1):-1]) / prev_window if prev_window > 0 else ma200

    current_price = daily[-1]["close"]

    return {
        "current_price": round(current_price, 2),
        "weekly_ma200": round(ma200, 2),
        "price_below_ma200": current_price < ma200,
        "ma_slope_negative": ma200 < prev_ma200,
        "signal": current_price < ma200 and ma200 < prev_ma200,
        "updated_at": now_utc().isoformat(),
    }


# ── Indicator 2: LTH Cost Basis ────────────────────────────────────────────


def fetch_lth_cost_basis() -> dict:
    """
    Long-term holder realized price.
    Signal: BTC price < LTH cost basis.
    Without Glassnode, approximate using 200-day realized price.
    """
    klines = fetch_binance_klines("BTCUSDT", "1d", 250)
    if len(klines) < 200:
        return {"error": f"Insufficient data: {len(klines)} candles"}

    daily = parse_klines(klines)
    closes = [c["close"] for c in daily]
    volumes = [c["volume"] for c in daily]

    current_price = closes[-1]

    # Approximate realized price: volume-weighted average of last 200 days
    # (Rough proxy when Glassnode unavailable)
    vw_realized = sum(c * v for c, v in zip(closes[-200:], volumes[-200:])) / sum(volumes[-200:])

    return {
        "lth_cost_basis_approx": round(vw_realized, 2),
        "current_price": round(current_price, 2),
        "price_below_lth": current_price < vw_realized,
        "signal": current_price < vw_realized,
        "note": "Approximate using 200d VWAP (Glassnode API key not configured)",
        "updated_at": now_utc().isoformat(),
    }


# ── Indicator 3: Hash Ribbons ──────────────────────────────────────────────


def fetch_hash_ribbons() -> dict:
    """
    30-day MA vs 60-day MA of hashrate.
    Signal: 30d MA < 60d MA (miner capitulation).
    """
    data = cached_request(
        BLOCKCHAIN_HASHRATE,
        {"timespan": "120days", "format": "json"},
        cache_key="hashrate_120d",
        ttl_hours=12,
    )

    if not data or not isinstance(data, dict) or "values" not in data:
        return {"error": "Hashrate data unavailable"}

    values = data.get("values", [])
    if len(values) < 60:
        return {"error": f"Insufficient hashrate data: {len(values)} points"}

    hashrates = [v["y"] for v in values]

    ma30 = sum(hashrates[-30:]) / 30
    ma60 = sum(hashrates[-60:]) / 60
    prev_ma30 = sum(hashrates[-31:-1]) / 30 if len(hashrates) >= 31 else ma30
    prev_ma60 = sum(hashrates[-61:-1]) / 60 if len(hashrates) >= 61 else ma60

    in_capitulation = ma30 < ma60
    new_capitulation = ma30 < ma60 and prev_ma30 >= prev_ma60

    return {
        "hashrate_30d_ma": round(ma30, 2),
        "hashrate_60d_ma": round(ma60, 2),
        "in_capitulation_zone": in_capitulation,
        "new_capitulation": new_capitulation,
        "signal": in_capitulation,
        "updated_at": now_utc().isoformat(),
    }


# ── Indicator 4: Puell Multiple ────────────────────────────────────────────


def fetch_puell_multiple() -> dict:
    """
    Daily BTC issuance (USD) / 365-day MA of daily issuance.
    Signal: < 0.5 (green zone).
    """
    klines = fetch_binance_klines("BTCUSDT", "1d", 400)
    if len(klines) < 365:
        return {"error": f"Insufficient data: {len(klines)} candles"}

    daily = parse_klines(klines)

    # Block reward schedule
    HALVING_2024 = datetime(2024, 4, 20, tzinfo=timezone.utc)

    daily_issuance_usd = []
    for candle in daily:
        dt = datetime.fromtimestamp(candle["open_time"] / 1000, tz=timezone.utc)
        reward = 3.125 if dt >= HALVING_2024 else 6.25
        daily_btc = reward * 144  # ~144 blocks/day
        daily_issuance_usd.append(daily_btc * candle["close"])

    if len(daily_issuance_usd) < 365:
        return {"error": "Insufficient issuance data"}

    current = daily_issuance_usd[-1]
    avg_365 = sum(daily_issuance_usd[-365:]) / 365
    puell = current / avg_365 if avg_365 > 0 else 0

    return {
        "puell_multiple": round(puell, 4),
        "in_green_zone": puell < 0.5,
        "signal": puell < 0.5,
        "current_issuance_usd": round(current, 2),
        "avg_365_issuance_usd": round(avg_365, 2),
        "updated_at": now_utc().isoformat(),
    }


# ── Indicator 5: Funding Rate ──────────────────────────────────────────────


def fetch_funding_rate() -> dict:
    """
    BTC perpetual funding rate.
    Signal: negative for 2+ weeks.
    Uses Binance funding rate API.
    """
    try:
        url = "https://fapi.binance.com/fapi/v1/fundingRate"
        params = {"symbol": "BTCUSDT", "limit": 90}
        data = cached_request(url, params, cache_key="binance_funding_90d", ttl_hours=6)

        if not data or not isinstance(data, list):
            return {"error": "Funding rate data unavailable"}

        # Binance returns funding rates every 8 hours (3 per day)
        rates = []
        for item in data:
            rates.append({
                "time": item.get("fundingTime"),
                "rate": float(item.get("fundingRate", 0)),
            })

        if not rates:
            return {"error": "No funding rate data"}

        current_rate = rates[-1]["rate"]

        # Count consecutive negative funding periods (from end)
        negative_streak = 0
        for r in reversed(rates):
            if r["rate"] < 0:
                negative_streak += 1
            else:
                break

        # Convert to days (3 periods = 1 day)
        negative_streak_days = negative_streak // 3

        return {
            "current_rate": round(current_rate * 100, 4),  # as percentage
            "negative_streak_periods": negative_streak,
            "negative_streak_days_approx": negative_streak_days,
            "signal": negative_streak >= 42,  # ~2 weeks (3*14)
            "updated_at": now_utc().isoformat(),
        }
    except Exception as e:
        print(f"[WARN] Funding rate error: {e}")
        return {
            "current_rate": None,
            "negative_streak_days_approx": 0,
            "signal": False,
            "note": "Funding rate data unavailable",
            "updated_at": now_utc().isoformat(),
        }


# ── Indicator 6: Cycle Timing ──────────────────────────────────────────────


def fetch_cycle_timing() -> dict:
    """
    Historical pattern check.
    - Deep drawdown from ATH (>50%)
    - Time since ATH
    - Time to next halving
    """
    klines = fetch_binance_klines("BTCUSDT", "1d", 500)
    if not klines:
        return {"error": "No price data"}

    daily = parse_klines(klines)
    current_price = daily[-1]["close"]

    # Compute ATH dynamically from available data
    ath_price = 0.0
    ath_date = None
    for candle in daily:
        if candle["high"] > ath_price:
            ath_price = candle["high"]
            ath_date = datetime.fromtimestamp(candle["open_time"] / 1000, tz=timezone.utc)

    # Fallback: if ATH is not in the 500-day window, use a known recent ATH
    if ath_price < 90000:
        ath_price = 109000
        ath_date = datetime(2025, 1, 20, tzinfo=timezone.utc)

    # Next halving: ~April 2028
    NEXT_HALVING = datetime(2028, 4, 1, tzinfo=timezone.utc)

    now = now_utc()
    months_since_ath = (now - ath_date).days / 30.44 if ath_date else None
    months_to_halving = (NEXT_HALVING - now).days / 30.44
    drawdown = (ath_price - current_price) / ath_price * 100

    return {
        "last_ath_date": ath_date.strftime("%Y-%m-%d") if ath_date else "unknown",
        "last_ath_price": round(ath_price, 2),
        "current_price": round(current_price, 2),
        "drawdown_from_ath": round(drawdown, 2),
        "months_since_last_ath": round(months_since_ath, 1) if months_since_ath is not None else None,
        "next_halving_date": "2028-04-01 (estimated)",
        "months_to_next_halving": round(months_to_halving, 1),
        "deep_correction": drawdown > 50,
        "signal": drawdown > 50,
        "updated_at": now_utc().isoformat(),
    }


# ── Bonus: Fear & Greed ─────────────────────────────────────────────────────


def fetch_fear_greed() -> dict:
    """
    Crypto Fear & Greed Index.
    Signal: < 20 (extreme fear).
    """
    data = cached_request(
        FEAR_GREED_API + "?limit=30",
        {},
        cache_key="fear_greed_30d",
        ttl_hours=6,
    )

    if not data or "data" not in data:
        return {"error": "Fear & Greed data unavailable"}

    values = data.get("data", [])
    if not values:
        return {"error": "No fear & greed data"}

    latest = values[0]
    current_value = int(latest.get("value", 50))
    classification = latest.get("value_classification", "Neutral")
    extreme_fear_days = sum(1 for v in values if int(v.get("value", 50)) < 20)

    return {
        "current_value": current_value,
        "classification": classification,
        "extreme_fear_days_30d": extreme_fear_days,
        "signal": current_value < 20,
        "sustained_extreme_fear": extreme_fear_days >= 7,
        "updated_at": now_utc().isoformat(),
    }


# ── Bonus: MVRV Approximation ──────────────────────────────────────────────


def fetch_mvrv_approx() -> dict:
    """
    Approximate MVRV ratio using price vs 365-day VWAP.
    Signal: < 1.0 (undervalued), < 0 (deep value - rare).
    """
    klines = fetch_binance_klines("BTCUSDT", "1d", 400)
    if len(klines) < 365:
        return {"error": f"Insufficient data: {len(klines)} candles"}

    daily = parse_klines(klines)
    closes = [c["close"] for c in daily]
    volumes = [c["volume"] for c in daily]

    current_price = closes[-1]
    vw_realized = sum(c * v for c, v in zip(closes[-365:], volumes[-365:])) / sum(volumes[-365:])
    mvrv = current_price / vw_realized if vw_realized > 0 else 0

    return {
        "mvrv_ratio_approx": round(mvrv, 4),
        "realized_price_approx": round(vw_realized, 2),
        "current_price": round(current_price, 2),
        "signal": mvrv < 1.0,
        "deep_value": mvrv < 0,
        "note": "Approximate using 365d VWAP",
        "updated_at": now_utc().isoformat(),
    }


# ── Main ───────────────────────────────────────────────────────────────────


def fetch_all() -> dict:
    """Fetch all indicators and save to data file."""
    print("[INFO] Fetching BTC bottom indicators...")

    results = {
        "timestamp": now_utc().isoformat(),
        "indicators": {
            "ma200": fetch_ma200(),
            "lth_cost_basis": fetch_lth_cost_basis(),
            "hash_ribbons": fetch_hash_ribbons(),
            "puell_multiple": fetch_puell_multiple(),
            "funding_rate": fetch_funding_rate(),
            "cycle_timing": fetch_cycle_timing(),
        },
        "bonus": {
            "mvrv_zscore": fetch_mvrv_approx(),
            "fear_greed": fetch_fear_greed(),
        },
    }

    # Count signals
    main_signals = sum(1 for v in results["indicators"].values() if v.get("signal", False))
    bonus_signals = sum(1 for v in results["bonus"].values() if v.get("signal", False))
    total_signals = main_signals + bonus_signals

    results["summary"] = {
        "main_signals_active": main_signals,
        "bonus_signals_active": bonus_signals,
        "total_signals_active": total_signals,
        "resonance_level": (
            "RED" if total_signals >= 5
            else "ORANGE" if total_signals >= 4
            else "YELLOW" if total_signals >= 3
            else "GREEN"
        ),
    }

    output_path = DATA_DIR / "indicators.json"
    save_json(output_path, results)
    print(f"[INFO] Data saved to {output_path}")
    print(f"[INFO] Main signals: {main_signals}/6, Bonus: {bonus_signals}/2, Total: {total_signals}/8")
    print(f"[INFO] Resonance level: {results['summary']['resonance_level']}")

    return results


if __name__ == "__main__":
    fetch_all()
