"""News Intelligence (docs/PLATFORM_EXPANSION_V2.md, Module 1).

Design principle from the spec: news NEVER auto-overrides a technical setup.
The NewsAgent produces a score that the ensemble decision layer weighs
alongside technicals — an adjustment, not a veto.

Providers:
- MockNewsProvider: deterministic headlines (per symbol per day) so the
  pipeline and tests work keyless.
- AlpacaNewsProvider: real headlines via Alpaca's News API (same keys as
  market data, free tier). Any failure falls back to mock.

Scoring:
- LLM mode when ANTHROPIC/OPENAI key present (via services.llm).
- Heuristic keyword mode otherwise — transparent and auditable.
"""
import logging
import os
import random
from datetime import date

import httpx

logger = logging.getLogger(__name__)

ALPACA_NEWS_URL = "https://data.alpaca.markets/v1beta1/news"
TIMEOUT = 8.0

POSITIVE_KEYWORDS = [
    "beat", "beats", "raise", "raises", "upgrade", "upgraded", "partnership",
    "record", "surge", "growth", "approval", "approved", "buyback", "contract",
    "expands", "launch", "strong", "outperform", "breakthrough", "acquisition",
]
NEGATIVE_KEYWORDS = [
    "miss", "misses", "cut", "cuts", "downgrade", "downgraded", "investigation",
    "lawsuit", "sec probe", "recall", "layoffs", "warning", "plunge", "fraud",
    "delisting", "bankruptcy", "halt", "underperform", "weak", "decline",
]

_MOCK_TEMPLATES = [
    ("{c} beats quarterly revenue estimates, raises full-year guidance", 1),
    ("Analysts upgrade {c} citing strong product demand", 1),
    ("{c} announces strategic partnership to expand market reach", 1),
    ("{c} shares steady as sector trades mixed", 0),
    ("{c} schedules investor day; no guidance changes expected", 0),
    ("Options activity in {c} in line with recent averages", 0),
    ("{c} faces analyst downgrade on valuation concerns", -1),
    ("Report: {c} under regulatory investigation over disclosures", -1),
    ("{c} warns of near-term margin pressure", -1),
]


class NewsProvider:
    def get_news(self, symbol: str, limit: int = 8) -> list[dict]:
        raise NotImplementedError


class MockNewsProvider(NewsProvider):
    """Deterministic mock headlines, stable per symbol per day."""

    def get_news(self, symbol: str, limit: int = 8) -> list[dict]:
        rng = random.Random(f"{symbol}:{date.today().isoformat()}:headlines")
        company = symbol.replace("/USD", "")
        picks = rng.sample(_MOCK_TEMPLATES, k=min(limit, 5))
        return [
            {
                "headline": tpl.format(c=company),
                "summary": "",
                "source": "mock-wire",
                "published": date.today().isoformat(),
                "tone_hint": tone,  # test/debug aid; scoring never reads this
            }
            for tpl, tone in picks
        ]


class AlpacaNewsProvider(NewsProvider):
    def __init__(self):
        self._fallback = MockNewsProvider()

    def get_news(self, symbol: str, limit: int = 8) -> list[dict]:
        key, secret = os.getenv("ALPACA_API_KEY", ""), os.getenv("ALPACA_API_SECRET", "")
        if not (key and secret):
            return self._fallback.get_news(symbol, limit)
        try:
            r = httpx.get(
                ALPACA_NEWS_URL,
                params={"symbols": symbol, "limit": limit, "sort": "desc"},
                headers={"APCA-API-KEY-ID": key, "APCA-API-SECRET-KEY": secret},
                timeout=TIMEOUT,
            )
            r.raise_for_status()
            items = r.json().get("news") or []
            out = [
                {
                    "headline": n.get("headline", ""),
                    "summary": (n.get("summary") or "")[:300],
                    "source": n.get("source", "alpaca"),
                    "published": str(n.get("created_at", ""))[:10],
                }
                for n in items if n.get("headline")
            ]
            return out or self._fallback.get_news(symbol, limit)
        except Exception as exc:  # noqa: BLE001 — news failure must not break scans
            logger.warning("Alpaca news failed for %s: %s — using mock", symbol, exc)
            return self._fallback.get_news(symbol, limit)


def get_news_provider() -> NewsProvider:
    if os.getenv("DATA_PROVIDER", "mock").lower() == "live" and os.getenv("ALPACA_API_KEY"):
        return AlpacaNewsProvider()
    return MockNewsProvider()


def classify_headline(item: dict) -> str:
    """Per-article tone: positive | negative | neutral (transparent keywords)."""
    text = f"{item.get('headline', '')} {item.get('summary', '')}".lower()
    if any(k in text for k in NEGATIVE_KEYWORDS):
        return "negative"
    if any(k in text for k in POSITIVE_KEYWORDS):
        return "positive"
    return "neutral"


def get_feed(symbols: list[str] | None = None, limit: int = 30) -> dict:
    """Aggregated news feed for the News tab. Each article carries its
    symbol, tone classification, and source link when available."""
    provider = get_news_provider()
    live = isinstance(provider, AlpacaNewsProvider)

    articles: list[dict] = []
    if live and symbols is None:
        # One call for broad market news (Alpaca supports symbol-less queries)
        key, secret = os.getenv("ALPACA_API_KEY", ""), os.getenv("ALPACA_API_SECRET", "")
        try:
            r = httpx.get(
                ALPACA_NEWS_URL,
                params={"limit": min(limit, 50), "sort": "desc"},
                headers={"APCA-API-KEY-ID": key, "APCA-API-SECRET-KEY": secret},
                timeout=TIMEOUT,
            )
            r.raise_for_status()
            for n in (r.json().get("news") or []):
                if not n.get("headline"):
                    continue
                articles.append({
                    "symbol": (n.get("symbols") or [""])[0],
                    "symbols": n.get("symbols") or [],
                    "headline": n.get("headline", ""),
                    "summary": (n.get("summary") or "")[:400],
                    "source": n.get("source", "alpaca"),
                    "published": str(n.get("created_at", ""))[:16].replace("T", " "),
                    "url": n.get("url"),
                })
        except Exception as exc:  # noqa: BLE001
            logger.warning("News feed fetch failed: %s — using mock", exc)
            live = False

    if not articles:
        from .market_data import UNIVERSE

        targets = symbols or [s for s, _, _ in UNIVERSE[:12]]
        for sym in targets:
            for item in provider.get_news(sym, limit=3):
                articles.append({
                    "symbol": sym, "symbols": [sym],
                    "headline": item["headline"], "summary": item.get("summary", ""),
                    "source": item.get("source", ""), "published": item.get("published", ""),
                    "url": item.get("url"),
                })

    for a in articles:
        a["tone"] = classify_headline(a)
    return {"articles": articles[:limit], "live": live}


def heuristic_news_score(headlines: list[dict]) -> dict:
    """Keyword-based scoring: 50 neutral, >50 positive, <50 negative.
    Fully transparent: returns the matched catalysts."""
    positives, negatives = [], []
    for item in headlines:
        text = f"{item.get('headline', '')} {item.get('summary', '')}".lower()
        if any(k in text for k in NEGATIVE_KEYWORDS):
            negatives.append(item.get("headline", ""))
        elif any(k in text for k in POSITIVE_KEYWORDS):
            positives.append(item.get("headline", ""))
    n = max(1, len(headlines))
    raw = (len(positives) - len(negatives)) / n  # -1..1
    return {
        "score": round(max(0.0, min(100.0, 50 + raw * 50)), 1),
        "positive_catalysts": positives[:3],
        "negative_catalysts": negatives[:3],
        "method": "heuristic",
    }
