from __future__ import annotations

import argparse
import html
import json
import re
import urllib.request
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

IDEAS_PATH = "research/tradingview_community_ideas.jsonl"
DEFAULT_URLS = {
    "BTC": "https://www.tradingview.com/symbols/BTCUSDT/ideas/?exchange=BINANCE&sort=recent",
    "ETH": "https://www.tradingview.com/symbols/ETHUSDT/ideas/?exchange=BINANCE&sort=recent",
    "XRP": "https://www.tradingview.com/symbols/XRPUSDT/ideas/?exchange=BINANCE&sort=recent",
}

BULLISH_TERMS = {
    "bullish", "long", "rally", "breakout", "upside", "pump", "accumulation", "support",
    "demand", "reclaim", "higher", "bounce", "buy", "moon", "continuation",
}
BEARISH_TERMS = {
    "bearish", "short", "dump", "breakdown", "downside", "resistance", "supply", "sell",
    "lower", "crash", "correction", "rejection", "drop", "fall", "risk",
}


def _now() -> str:
    return datetime.now(timezone.utc).isoformat()


def _clean_text(value: str) -> str:
    text = re.sub(r"<[^>]+>", " ", value)
    text = html.unescape(text)
    text = re.sub(r"\s+", " ", text).strip()
    return text


def _normalize_idea_url(url: str) -> str:
    url = html.unescape(url).split("#", 1)[0]
    if url.startswith("/"):
        url = "https://www.tradingview.com" + url
    return url.rstrip("/") + "/"


def classify_idea_bias(text: str) -> str:
    words = set(re.findall(r"[a-zA-Z]+", text.lower()))
    bull = len(words & BULLISH_TERMS)
    bear = len(words & BEARISH_TERMS)
    if bull and bear:
        directional = words & ((BULLISH_TERMS | BEARISH_TERMS) - {"support", "resistance", "risk"})
        if not directional:
            return "neutral"
        # Require material imbalance; otherwise call it mixed/noisy.
        if bull > bear * 2:
            return "bullish"
        if bear > bull * 2:
            return "bearish"
        return "mixed"
    if bull:
        return "bullish"
    if bear:
        return "bearish"
    return "neutral"


def _idea_row(*, coin: str, source_url: str, url: str, title: str, excerpt: str, author: str | None = None) -> dict[str, Any]:
    combined = f"{title} {excerpt}"
    return {
        "timestamp": _now(),
        "source": "tradingview_community_ideas",
        "source_url": source_url,
        "coin": coin.upper(),
        "author": author or "unknown",
        "title": title[:220],
        "excerpt": excerpt[:500],
        "url": url,
        "bias": classify_idea_bias(combined),
        "research_only": True,
        "unverified_author_track_record": True,
        "execution_mode": "research_context",
        "live_order_allowed": False,
        "mainnet_signed_action": False,
        "order_intent_created": False,
    }


def parse_ideas_html(html_text: str, *, coin: str, source_url: str, limit: int = 20) -> list[dict[str, Any]]:
    """Parse public TradingView idea cards from HTML.

    This is intentionally research-only. The output is never an execution signal.
    TradingView page structure changes often, so this parser first tries full
    <article> cards, then falls back to chart-link grouping.
    """
    rows: list[dict[str, Any]] = []
    seen_urls: set[str] = set()
    articles = re.findall(r"<article\b.*?</article>", html_text, re.I | re.S)
    for article in articles:
        title_match = re.search(r"<a\b[^>]*href=[\"']([^\"']+/chart/[^\"']+)[\"'][^>]*data-qa-id=[\"']ui-lib-card-link-title[\"'][^>]*>(.*?)</a>", article, re.I | re.S)
        if not title_match:
            continue
        url = _normalize_idea_url(title_match.group(1))
        if url in seen_urls:
            continue
        title = _clean_text(title_match.group(2))
        excerpt_match = re.search(r"<a\b[^>]*href=[\"'][^\"']+/chart/[^\"']+[\"'][^>]*data-qa-id=[\"']ui-lib-card-link-paragraph[\"'][^>]*>(.*?)</a>", article, re.I | re.S)
        excerpt = _clean_text(excerpt_match.group(1)) if excerpt_match else ""
        author_match = re.search(r"href=[\"'](?:https://www\.tradingview\.com)?/u/([^/\"']+)/[\"'][^>]*>(.*?)</a>", article, re.I | re.S)
        author = _clean_text(author_match.group(2)).removeprefix("by ").strip() if author_match else None
        rows.append(_idea_row(coin=coin, source_url=source_url, url=url, title=title, excerpt=excerpt, author=author))
        seen_urls.add(url)
        if len(rows) >= limit:
            return rows

    anchors = re.findall(r"<a\b[^>]*href=[\"']([^\"']+/chart/[^\"']+)[\"'][^>]*>(.*?)</a>", html_text, re.I | re.S)
    grouped: dict[str, list[str]] = {}
    order: list[str] = []
    for href, raw_text in anchors:
        url = _normalize_idea_url(href)
        if "tradingview.com/chart/" not in url or url in seen_urls:
            continue
        text = _clean_text(raw_text)
        if not text or text.lower() in {"comments", "comment", "1 1"}:
            continue
        if url not in grouped:
            grouped[url] = []
            order.append(url)
        if text not in grouped[url]:
            grouped[url].append(text)

    for url in order:
        texts = grouped[url]
        if not texts:
            continue
        title = texts[0][:220]
        excerpt = next((t for t in texts[1:] if len(t) >= 30), "")[:500]
        rows.append(_idea_row(coin=coin, source_url=source_url, url=url, title=title, excerpt=excerpt))
        if len(rows) >= limit:
            break
    return rows


def fetch_ideas_page(url: str, *, timeout: int = 20) -> str:
    request = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 CTB research-only"})
    with urllib.request.urlopen(request, timeout=timeout) as response:
        return response.read().decode("utf-8", errors="replace")


def fetch_hyperliquid_mids(*, timeout: int = 10) -> dict[str, str]:
    request = urllib.request.Request(
        "https://api.hyperliquid.xyz/info",
        data=json.dumps({"type": "allMids"}).encode("utf-8"),
        headers={"Content-Type": "application/json", "User-Agent": "CTB research-only"},
        method="POST",
    )
    with urllib.request.urlopen(request, timeout=timeout) as response:
        data = json.loads(response.read().decode("utf-8", errors="replace"))
    return {str(coin).upper(): str(price) for coin, price in data.items()}


def attach_price_snapshot(ideas: list[dict[str, Any]], prices: dict[str, str]) -> list[dict[str, Any]]:
    enriched: list[dict[str, Any]] = []
    for idea in ideas:
        row = dict(idea)
        coin = str(row.get("coin") or "").upper()
        price = prices.get(coin)
        if price:
            row["price_at_capture_usd"] = price
            row["price_source"] = "hyperliquid_all_mids"
        enriched.append(row)
    return enriched


def write_ideas_snapshot(ideas: list[dict[str, Any]], *, runtime_dir: str | Path = "runtime") -> Path:
    path = Path(runtime_dir) / IDEAS_PATH
    path.parent.mkdir(parents=True, exist_ok=True)
    seen = {str(row.get("url")) for row in _read_jsonl(path)}
    with path.open("a", encoding="utf-8") as fh:
        for idea in ideas:
            if str(idea.get("url")) in seen:
                continue
            fh.write(json.dumps(idea, sort_keys=True, ensure_ascii=False) + "\n")
            seen.add(str(idea.get("url")))
    return path


def _read_jsonl(path: Path) -> list[dict[str, Any]]:
    if not path.exists():
        return []
    rows: list[dict[str, Any]] = []
    for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
        if not line.strip():
            continue
        try:
            rows.append(json.loads(line))
        except json.JSONDecodeError:
            continue
    return rows


def summarize_community_ideas(runtime_dir: str | Path = "runtime", *, recent_limit: int = 200) -> dict[str, Any]:
    rows = _read_jsonl(Path(runtime_dir) / IDEAS_PATH)[-recent_limit:]
    by_coin = Counter(str(row.get("coin") or "UNKNOWN").upper() for row in rows)
    by_bias = Counter(str(row.get("bias") or "unknown") for row in rows)
    live_flags = sum(1 for row in rows if row.get("live_order_allowed") is True or row.get("mainnet_signed_action") is True)
    latest = [
        {"coin": row.get("coin"), "bias": row.get("bias"), "title": row.get("title"), "url": row.get("url")}
        for row in rows[-5:]
    ]
    return {
        "total": len(rows),
        "by_coin": dict(by_coin.most_common()),
        "by_bias": dict(by_bias.most_common()),
        "live_order_allowed_count": live_flags,
        "latest": latest,
        "recommendation": "research_only_never_trade_directly",
    }


def collect_community_ideas(*, runtime_dir: str | Path = "runtime", urls: dict[str, str] | None = None, limit_per_coin: int = 10) -> dict[str, Any]:
    urls = urls or DEFAULT_URLS
    collected: list[dict[str, Any]] = []
    errors: dict[str, str] = {}
    prices: dict[str, str] = {}
    try:
        prices = fetch_hyperliquid_mids()
    except Exception as exc:
        errors["hyperliquid_prices"] = exc.__class__.__name__
    for coin, url in urls.items():
        try:
            html_text = fetch_ideas_page(url)
            ideas = parse_ideas_html(html_text, coin=coin, source_url=url, limit=limit_per_coin)
            collected.extend(attach_price_snapshot(ideas, prices))
        except Exception as exc:  # network/parser collector should fail soft
            errors[coin] = exc.__class__.__name__
    path = write_ideas_snapshot(collected, runtime_dir=runtime_dir)
    summary = summarize_community_ideas(runtime_dir)
    return {"status": "ok" if collected or not errors else "error", "collected": len(collected), "errors": errors, "journal": str(path), "summary": summary}


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Collect TradingView community ideas as research-only context.")
    parser.add_argument("--runtime-dir", default="runtime")
    parser.add_argument("--coin-url", action="append", default=[], help="COIN=URL, repeatable")
    parser.add_argument("--limit-per-coin", type=int, default=10)
    parser.add_argument("--summary", action="store_true")
    args = parser.parse_args(argv)
    if args.summary:
        print(json.dumps(summarize_community_ideas(args.runtime_dir), indent=2, sort_keys=True))
        return 0
    urls = DEFAULT_URLS.copy()
    for item in args.coin_url:
        coin, url = item.split("=", 1)
        urls[coin.upper()] = url
    print(json.dumps(collect_community_ideas(runtime_dir=args.runtime_dir, urls=urls, limit_per_coin=args.limit_per_coin), indent=2, sort_keys=True, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
