"""
4seen MCP server — exposes 4seen's audience-simulation engine as tools an agent can call.

An agent (e.g. Hermes) that already knows the brand, the audience, and the goal crafts the content,
then calls these tools to pre-flight it: predict how a real audience reacts, flag problems, rewrite,
check community/flame risk, check AI-citability, and critique a creative — all before publishing.

Design note: 4seen does NOT profile the audience itself. The CALLING AGENT supplies the context
(personas, positioning, voice, claims) because it already holds it — more accurate than 4seen
crawling data sources, and no external profiling integrations required.

Transport: stdio by default (works with Claude Desktop / Claude Code / most agents). Set
FOURSEEN_MCP_HTTP=1 to serve over streamable HTTP instead.

Env:
  FOURSEEN_API_KEY   (required)  a 4seen partner Bearer key
  FOURSEEN_BASE_URL  (optional)  default https://4seenai.com/v1
"""

import os
from typing import Any, Optional

import httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("4seen")

BASE_URL = os.environ.get("FOURSEEN_BASE_URL", "https://4seenai.com/v1").rstrip("/")
API_KEY = os.environ.get("FOURSEEN_API_KEY", "")

# Channels 4seen understands. The tools uppercase/alias common inputs to these.
_CHANNELS = {"X", "LINKEDIN_COMPANY", "LINKEDIN_FOUNDER", "REDDIT", "TELEGRAM", "INSTAGRAM",
             "TIKTOK", "YOUTUBE_SHORTS", "YOUTUBE_LONG", "THREADS", "BLUESKY", "DISCORD",
             "BLOG", "EMAIL", "AEO", "PRODUCT_HUNT", "HACKER_NEWS", "DIRECTORIES"}
_ALIASES = {"TWITTER": "X", "LINKEDIN": "LINKEDIN_FOUNDER", "HN": "HACKER_NEWS",
            "HACKERNEWS": "HACKER_NEWS", "YOUTUBE": "YOUTUBE_LONG", "SHORTS": "YOUTUBE_SHORTS",
            "LANDING_PAGE": "BLOG", "LANDING": "BLOG", "GENERIC": "X", "GENERAL": "X"}


def _channel(ch: str) -> str:
    c = (ch or "X").strip().upper().replace(" ", "_").replace("/", "")
    c = _ALIASES.get(c, c)
    return c if c in _CHANNELS else "X"


def _brand(positioning, voice_guide, personas, banned_claims, approved_claims, pillars):
    return {
        "positioning": positioning or "",
        "voice_guide": voice_guide or "",
        "personas": personas or [],
        "banned_claims": banned_claims or [],
        "approved_claims": approved_claims or [],
        "pillars": pillars or [],
    }


def _call(path: str, payload: dict, method: str = "POST") -> Any:
    if not API_KEY:
        return {"error": "FOURSEEN_API_KEY is not set. Get a partner key and set it in the env."}
    headers = {"Authorization": f"Bearer {API_KEY}"}
    try:
        with httpx.Client(timeout=90) as c:
            if method == "GET":
                r = c.get(f"{BASE_URL}{path}", params=payload, headers=headers)
            else:
                r = c.post(f"{BASE_URL}{path}", json=payload, headers=headers)
        if r.status_code >= 400:
            try:
                return {"error": r.json()}
            except Exception:
                return {"error": f"HTTP {r.status_code}: {r.text[:200]}"}
        return r.json()
    except Exception as e:
        return {"error": f"request failed: {e}"}


@mcp.tool()
def simulate_audience(
    draft_text: str,
    channel: str = "REDDIT",
    goal: str = "COMMUNITY",
    positioning: str = "",
    voice_guide: str = "",
    personas: Optional[list] = None,
    banned_claims: Optional[list] = None,
    approved_claims: Optional[list] = None,
    pillars: Optional[list] = None,
) -> dict:
    """Simulate how a specific audience will react to a post/comment/thread BEFORE publishing.

    Returns persona-by-persona reactions (sentiment, predicted action, why), a 0-1 composite score,
    creative-critique flags (off_voice, unsupported_claim, banned_claim, platform_norm_violation, ...),
    directional engagement, and the highest-impact suggested edits.

    Supply as much context as you have — it makes the simulation sharper:
      channel: one of REDDIT, X, LINKEDIN_FOUNDER, HACKER_NEWS, INSTAGRAM, TIKTOK, EMAIL, BLOG, ...
      personas: list of {"name","profile","objections":[...],"vocabulary":[...]} — the actual audience.
      positioning / voice_guide / banned_claims / approved_claims / pillars: brand context.
    """
    return _call("/simulate", {
        "content": {"channel": _channel(channel), "draft_text": draft_text},
        "goal": goal,
        "brand_context": _brand(positioning, voice_guide, personas, banned_claims, approved_claims, pillars),
    })


@mcp.tool()
def improve_post(
    draft_text: str,
    channel: str = "REDDIT",
    goal: str = "COMMUNITY",
    suggested_edits: str = "",
    flags: Optional[list] = None,
) -> dict:
    """Rewrite a draft to fix its problems while keeping intent, voice, and channel norms.

    Pass the `suggested_edits` and/or `flags` you got back from simulate_audience to target the rewrite.
    Returns {"revised_text": ...}. Typical loop: simulate_audience -> improve_post -> simulate_audience.
    """
    return _call("/revise", {
        "content": {"channel": _channel(channel), "draft_text": draft_text},
        "goal": goal, "suggested_edits": suggested_edits, "flags": flags or [],
    })


@mcp.tool()
def suggest_angles(
    brief: str,
    channel: str = "REDDIT",
    goal: str = "COMMUNITY",
    positioning: str = "",
    voice_guide: str = "",
    personas: Optional[list] = None,
    n: int = 5,
) -> dict:
    """Predict the strongest ANGLES and opening HOOKS for an audience BEFORE any draft exists.

    Use this to brief a writer (or yourself). Returns a ranked list of angles, each with a literal
    opening line, why it lands with this audience, its main risk, and a predicted resonance score.
    """
    return _call("/ideate", {
        "brief": brief, "channel": _channel(channel), "goal": goal, "n": n,
        "brand_context": _brand(positioning, voice_guide, personas, None, None, None),
    })


@mcp.tool()
def check_community_risk(
    draft_text: str,
    community: str = "",
    positioning: str = "",
    personas: Optional[list] = None,
) -> dict:
    """Predict how a high-risk community (a specific subreddit, Hacker News) will really receive a post.

    Returns flame_risk, removal_risk, whether it reads as an ad, the likely top comment, rule/norm
    risks, value-first fixes, and a verdict (post | revise | do_not_post). Use before posting anywhere
    Reddit/HN — these audiences punish anything that smells like marketing.
    """
    return _call("/community-check", {
        "content": {"draft_text": draft_text}, "community": community,
        "brand_context": _brand(positioning, "", personas, None, None, None),
    })


@mcp.tool()
def check_citability(
    content_text: str,
    queries: Optional[list] = None,
    positioning: str = "",
) -> dict:
    """AEO: predict whether an AI assistant (ChatGPT/Claude/Perplexity/Gemini) would CITE this content.

    Pass the target `queries` you want to be cited for. Returns per-query {would_cite, confidence,
    what it would quote, why/why not, fixes}, an overall citability score, and the top structural fixes.
    """
    return _call("/aeo-simulate", {
        "content": {"draft_text": content_text}, "queries": queries or [],
        "brand_context": _brand(positioning, "", None, None, None, None),
    })


@mcp.tool()
def check_voice(content_text: str, voice_profile: str = "", samples: Optional[list] = None) -> dict:
    """Check whether a draft actually sounds like a specific person/brand (founder-voice fidelity).

    Provide a `voice_profile` and/or real writing `samples`. Returns a fidelity score, a verdict
    (on_voice | drifting | off_voice), the off-voice spans, and an on-voice rewrite.
    """
    return _call("/voice-check", {
        "content": {"draft_text": content_text}, "voice_profile": voice_profile, "samples": samples or [],
    })


@mcp.tool()
def critique_creative(
    image_url: str = "",
    image_urls: Optional[list] = None,
    platform: str = "generic",
    goal: str = "",
) -> dict:
    """Critique a marketing creative (image/ad/screenshot/thumbnail) — non-predictive feedback.

    Provide an image_url (or image_urls[]). Returns scored dimensions (hook, clarity, scroll-stop,
    legibility, CTA, brand presence), a first-glance read, risks, strengths, and prioritized fixes.
    Feedback, not a forecast — it never predicts views or virality.
    """
    payload: dict = {"platform": platform, "goal": goal}
    if image_urls:
        payload["image_urls"] = image_urls
    elif image_url:
        payload["image_url"] = image_url
    return _call("/critique", payload)


# ---- The citability loop (B8) — chain these to run Audit → Fix → Prove on any site ----

@mcp.tool()
def audit_site(domain: str) -> dict:
    """Stage-1 AI-visibility audit of a domain: are AI crawlers blocked in robots.txt, how much
    of the content is invisible to non-JS retrieval (measured rendered-vs-raw gap), is there
    JSON-LD schema, are there visible dates. Start every visibility engagement here.

    Returns {domain, flags: [{severity: blocker|warn|ok, finding}]}."""
    return _call("/visibility-audit", {"domain": domain})


@mcp.tool()
def query_gaps(target_content: str, competitor_contents: Optional[list] = None,
               n: int = 8, category: str = "") -> dict:
    """Discover the buyer questions a brand should win — adversarially, across the WHOLE category
    space (target + competitor content), not just what the target's copy already answers.

    Pass the target's homepage/docs text and 1-3 competitors' equivalents. Returns
    {questions: [{question, likely_winner: target|competitor|neither, why}], gaps} — the
    non-target rows are the gap list to fix."""
    return _call("/query-gaps", {"target_content": target_content,
                                 "competitor_contents": competitor_contents or [],
                                 "n": n, "category": category})


@mcp.tool()
def score_content(content: str, topic: str = "") -> dict:
    """Score content on the validated citability scorecard — the features that empirically
    separate cited from ignored content in real AI answers (AUC 0.80-0.90 across models).

    Pass `topic` as the question the content should win. Returns {citability_score, features,
    weakest, fixes} — diagnosis of the content, not a ranking guarantee."""
    return _call("/citability", {"content": content, "topic": topic})


@mcp.tool()
def rewrite_grounded(content: str, topic: str = "", facts: Optional[list] = None,
                     brand: str = "") -> dict:
    """Substance-grounded guarded rewrite — the validated fix step. Rewrites content to win a
    target question using ONLY facts from the original plus the supplied `facts` list; hard
    guards reject invented numbers and padding. When the substance can't win the question it
    REFUSES with the concrete facts that are missing (ask the client for them) instead of
    producing fluff — presentation-only rewriting measured zero lift.

    Pass `brand` to entity-stamp claims (attribution hygiene). Returns {rewritten, guard_audit}
    or {refused: true, needs: [...]}."""
    return _call("/rewrite-grounded", {"content": content, "topic": topic,
                                       "facts": facts or [], "brand": brand})


@mcp.tool()
def verify_panel(question: str, domain: str, brand: str) -> dict:
    """Live grounded-engine check: ask real answer engines the question and report, per engine,
    whether the domain was LINKED (cited) and the brand NAMED, plus which third-party pages got
    cited instead (the gatekeepers). The prove step — run before and after fixes ship.

    Note: 0 citations from the Gemini/OpenAI lanes is real signal (their search is
    model-discretionary); the sonar lane always searches."""
    return _call("/verify-panel", {"question": question, "domain": domain, "brand": brand})


@mcp.tool()
def share_result(simulation_id: str) -> dict:
    """Make a simulation public and get a shareable link (from a simulate_audience result's
    `simulation_id`). Returns {"url": "https://4seenai.com/s/..."} — a branded page anyone can
    open, with a rich social preview. Useful for 'here's what 4seen said about this post'.
    (13 tools total, all wrapping the 4seen /v1 gate.)"""
    return _call(f"/share/{simulation_id}", {})


if __name__ == "__main__":
    if os.environ.get("FOURSEEN_MCP_HTTP") == "1":
        mcp.run(transport="streamable-http")
    else:
        mcp.run()
