from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal
from pathlib import Path
from typing import Any

_ALLOWED_BIAS = {"bullish", "bearish", "neutral", "mixed", "unknown"}
_ALLOWED_TREND = {"aligned", "mixed", "conflict", "unknown"}
_ALLOWED_VOLUME = {"breakout", "normal", "weak", "unknown"}
_ALLOWED_VOLATILITY = {"squeeze", "expanding", "high-risk", "normal", "unknown"}


@dataclass(frozen=True)
class TradingViewContext:
    coin: str
    symbol: str
    exchange: str = "BINANCE"
    bias: str = "unknown"
    confidence: Decimal = Decimal("0")
    trend_alignment: str = "unknown"
    volume_state: str = "unknown"
    volatility_state: str = "unknown"
    no_trade_reasons: tuple[str, ...] = ()
    nearest_support: Decimal | None = None
    nearest_resistance: Decimal | None = None
    source_age_seconds: int | None = None
    research_only: bool = True
    source: str = "tradingview_mcp"

    @property
    def stale(self) -> bool:
        return self.source_age_seconds is None or self.source_age_seconds > 3600


def _decimal_or_none(value: Any) -> Decimal | None:
    if value is None or value == "":
        return None
    try:
        return Decimal(str(value))
    except Exception:
        return None


def _decimal(value: Any, default: str = "0") -> Decimal:
    parsed = _decimal_or_none(value)
    return parsed if parsed is not None else Decimal(default)


def _clean_choice(value: Any, allowed: set[str], default: str = "unknown") -> str:
    val = str(value or default).strip().lower()
    return val if val in allowed else default


def _age_seconds(row: dict[str, Any], now: datetime | None = None) -> int | None:
    now = now or datetime.now(timezone.utc)
    raw = row.get("ts") or row.get("timestamp") or row.get("created_at")
    if not raw:
        return None
    try:
        stamp = datetime.fromisoformat(str(raw).replace("Z", "+00:00"))
        if stamp.tzinfo is None:
            stamp = stamp.replace(tzinfo=timezone.utc)
        return max(0, int((now - stamp.astimezone(timezone.utc)).total_seconds()))
    except Exception:
        return None


def normalize_tradingview_context(row: dict[str, Any], *, now: datetime | None = None) -> TradingViewContext:
    """Whitelist and normalize a TradingView MCP research row.

    Free text, tool prose, and non-whitelisted fields are intentionally ignored.
    """
    coin = str(row.get("coin") or row.get("base") or row.get("symbol") or "").upper().replace("USDT", "").replace("/USDC:USDC", "")
    symbol = str(row.get("symbol") or f"{coin}USDT").upper()
    levels_raw = row.get("support_resistance")
    levels: dict[str, Any] = levels_raw if isinstance(levels_raw, dict) else {}
    reasons_raw = row.get("no_trade_reasons")
    reasons: list[Any] = reasons_raw if isinstance(reasons_raw, list) else []
    return TradingViewContext(
        coin=coin,
        symbol=symbol,
        exchange=str(row.get("exchange") or "BINANCE").upper(),
        bias=_clean_choice(row.get("bias"), _ALLOWED_BIAS),
        confidence=max(Decimal("0"), min(Decimal("1"), _decimal(row.get("confidence")))),
        trend_alignment=_clean_choice(row.get("trend_alignment"), _ALLOWED_TREND),
        volume_state=_clean_choice(row.get("volume_state"), _ALLOWED_VOLUME),
        volatility_state=_clean_choice(row.get("volatility_state"), _ALLOWED_VOLATILITY),
        no_trade_reasons=tuple(str(x)[:80] for x in reasons if isinstance(x, str)),
        nearest_support=_decimal_or_none(levels.get("nearest_support") or row.get("nearest_support")),
        nearest_resistance=_decimal_or_none(levels.get("nearest_resistance") or row.get("nearest_resistance")),
        source_age_seconds=_age_seconds(row, now=now),
        research_only=bool(row.get("research_only", True)) is True,
        source="tradingview_mcp" if str(row.get("source") or "tradingview_mcp") == "tradingview_mcp" else "unknown",
    )


def load_tradingview_latest(path: Path | str = Path("runtime/research/tradingview_latest.json"), *, now: datetime | None = None) -> dict[str, TradingViewContext]:
    path = Path(path)
    if not path.exists():
        return {}
    try:
        raw = json.loads(path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}
    rows: list[dict[str, Any]]
    if isinstance(raw, dict) and isinstance(raw.get("coins"), list):
        rows = [x for x in raw["coins"] if isinstance(x, dict)]
    elif isinstance(raw, list):
        rows = [x for x in raw if isinstance(x, dict)]
    else:
        rows = []
    out: dict[str, TradingViewContext] = {}
    for row in rows:
        ctx = normalize_tradingview_context(row, now=now)
        if ctx.coin:
            out[ctx.coin] = ctx
    return out


def tradingview_blockers(ctx: TradingViewContext | None, *, side: str = "long", max_age_seconds: int = 3600) -> list[str]:
    if ctx is None:
        return []
    blockers: list[str] = []
    if not ctx.research_only:
        blockers.append("tv_context_not_research_only")
    if ctx.source != "tradingview_mcp":
        blockers.append("tv_context_untrusted_source")
    if ctx.source_age_seconds is None or ctx.source_age_seconds > max_age_seconds:
        blockers.append("tv_context_stale")
    side = side.lower()
    if side == "long":
        if ctx.bias == "bearish":
            blockers.append("tv_bias_bearish")
        if ctx.trend_alignment == "conflict":
            blockers.append("tv_mtf_conflict")
        if ctx.volume_state == "weak":
            blockers.append("tv_volume_not_confirmed")
    elif side == "short":
        if ctx.bias == "bullish":
            blockers.append("tv_bias_bullish")
        if ctx.trend_alignment == "conflict":
            blockers.append("tv_mtf_conflict")
    blockers.extend(f"tv_no_trade_{reason}" for reason in ctx.no_trade_reasons[:5])
    return list(dict.fromkeys(blockers))


def classify_tradingview_impulse(ctx: TradingViewContext | None, *, side: str = "long", max_age_seconds: int = 3600) -> dict[str, Any]:
    """Classify TradingView MCP context into deterministic paper-decision categories.

    MCP rows stay research-only.  This category lets scorecards measure whether a
    bullish/bearish TradingView impulse helped, blocked, or only provided context;
    it never grants live permission.
    """
    if ctx is None:
        return {
            "available": False,
            "category": "missing_context",
            "action": "ignore",
            "reasons": ["tv_context_missing"],
            "confidence": "0",
            "live_order_allowed": False,
            "mainnet_signed_action": False,
        }
    blockers = tradingview_blockers(ctx, side=side, max_age_seconds=max_age_seconds)
    reasons: list[str] = []
    if blockers:
        category = "avoid_long" if side.lower() == "long" else "avoid_short"
        action = "block_or_demote"
        reasons.extend(blockers)
    elif side.lower() == "long" and ctx.bias == "bullish" and ctx.trend_alignment == "aligned" and ctx.confidence >= Decimal("0.70") and ctx.volume_state in {"breakout", "normal"} and ctx.volatility_state in {"squeeze", "expanding", "normal"}:
        category = "buy_opportunity"
        action = "paper_candidate_if_other_gates_pass"
        reasons.append("tv_bullish_aligned_confident")
        if ctx.volume_state == "breakout":
            reasons.append("tv_volume_breakout")
        if ctx.volatility_state == "squeeze":
            reasons.append("tv_squeeze_watch")
    elif side.lower() == "long" and ctx.bias in {"bullish", "mixed"} and ctx.confidence >= Decimal("0.55"):
        category = "watch_long"
        action = "watch_only"
        reasons.append("tv_constructive_but_not_confirmed")
    elif side.lower() == "short" and ctx.bias == "bearish" and ctx.trend_alignment == "aligned" and ctx.confidence >= Decimal("0.70") and ctx.volume_state in {"breakout", "normal"} and ctx.volatility_state in {"squeeze", "expanding", "normal"}:
        category = "sell_opportunity"
        action = "paper_candidate_if_other_gates_pass"
        reasons.append("tv_bearish_aligned_confident")
        if ctx.volume_state == "breakout":
            reasons.append("tv_volume_breakout")
        if ctx.volatility_state == "squeeze":
            reasons.append("tv_squeeze_watch")
    elif side.lower() == "short" and ctx.bias in {"bearish", "mixed"} and ctx.confidence >= Decimal("0.55"):
        category = "watch_short"
        action = "watch_only"
        reasons.append("tv_bearish_but_not_confirmed")
    else:
        category = "neutral_context"
        action = "context_only"
        reasons.append("tv_no_actionable_impulse")
    return {
        "available": True,
        "category": category,
        "action": action,
        "reasons": list(dict.fromkeys(reasons)),
        "bias": ctx.bias,
        "confidence": str(ctx.confidence),
        "trend_alignment": ctx.trend_alignment,
        "volume_state": ctx.volume_state,
        "volatility_state": ctx.volatility_state,
        "research_only": ctx.research_only,
        "source": ctx.source,
        "source_age_seconds": ctx.source_age_seconds,
        "live_order_allowed": False,
        "mainnet_signed_action": False,
    }


def rank_tradeable_coins(*, candidate_coins: list[str], paper_pnl_by_coin: dict[str, float], tv_contexts: dict[str, TradingViewContext]) -> list[str]:
    """Rank coins by measured evidence first, then TV context quality.

    This is intentionally conservative: prior paper/shadow evidence dominates, TV
    context can only break ties or demote conflicted/stale markets.
    """
    def score(coin: str) -> tuple[Decimal, str]:
        c = coin.upper()
        s = Decimal(str(paper_pnl_by_coin.get(c, 0.0)))
        ctx = tv_contexts.get(c)
        if ctx:
            if not tradingview_blockers(ctx, side="long"):
                s += Decimal("0.25") + ctx.confidence / Decimal("4")
            elif "tv_context_stale" in tradingview_blockers(ctx, side="long"):
                s -= Decimal("0.10")
            else:
                s -= Decimal("0.50")
        return (s, c)
    return [coin for _, coin in sorted((score(c) for c in candidate_coins), reverse=True)]


def context_to_journal_dict(ctx: TradingViewContext | None) -> dict[str, Any]:
    if ctx is None:
        return {"available": False, "source": "none", "research_only": True}
    return {
        "available": True,
        "coin": ctx.coin,
        "symbol": ctx.symbol,
        "exchange": ctx.exchange,
        "bias": ctx.bias,
        "confidence": str(ctx.confidence),
        "trend_alignment": ctx.trend_alignment,
        "volume_state": ctx.volume_state,
        "volatility_state": ctx.volatility_state,
        "no_trade_reasons": list(ctx.no_trade_reasons),
        "nearest_support": str(ctx.nearest_support) if ctx.nearest_support is not None else None,
        "nearest_resistance": str(ctx.nearest_resistance) if ctx.nearest_resistance is not None else None,
        "source_age_seconds": ctx.source_age_seconds,
        "research_only": ctx.research_only,
        "source": ctx.source,
        "live_order_allowed": False,
        "mainnet_signed_action": False,
    }
