"""Public-safe content series registry and fit validation.

TrueTraceShorts now defaults to Digital Red Flags / Scam Self-Defense for
normal people: one screen, one red flag, one safer move.
"""

from __future__ import annotations

from dataclasses import dataclass

from autoshorts.ideas.candidate import ValidationResult, VideoCandidate

FORBIDDEN_PUBLIC_TOPIC_PATTERNS = {
    "jarvis": ("jarvis",),
    "hermes": ("hermes",),
    "user": ("my user",),
    "collaboration": ("collaboration", "our collaboration"),
    "private": ("private", "behind the scenes"),
    "politics": ("politics", "political", "election", "voter", "campaign"),
    "legal loophole": ("legal loophole", "loophole"),
    "bypass": ("bypass",),
    "illegal": ("illegal", "break the law"),
    "private data": ("private data", "scrape private data"),
    "morally questionable": ("morally questionable",),
    "exploit": ("exploit",),
    "hacking instructions": ("how to hack", "steal password", "build a scam", "phishing kit"),
}
FORBIDDEN_PUBLIC_TOPICS = tuple(FORBIDDEN_PUBLIC_TOPIC_PATTERNS)


@dataclass(frozen=True)
class ContentSeries:
    series_id: str
    name: str
    promise: str
    audience: str
    allowed_hook_types: tuple[str, ...]
    format_rules: tuple[str, ...]
    visual_grammar: tuple[str, ...]
    no_go_patterns: tuple[str, ...]
    follow_reason: str


DEFAULT_CONTENT_SERIES = (
    ContentSeries(
        series_id="one_screen_one_red_flag",
        name="One Screen. One Red Flag.",
        promise="Show one normal screen, the one dangerous detail, and one safer move.",
        audience="normal smartphone and internet users",
        allowed_hook_types=("visible_trap", "red_flag_reveal", "before_you_click"),
        format_rules=(
            "0-2s visible trap and short hook",
            "2-7s why it feels normal",
            "7-15s reveal one concrete red flag",
            "15-24s show one safer move",
            "24-30s screenshot checklist with max three points",
            "30-34s memorable rule and soft CTA",
        ),
        visual_grammar=("phone screen", "email", "invoice", "chat", "popup", "login", "QR situation"),
        no_go_patterns=FORBIDDEN_PUBLIC_TOPICS,
        follow_reason="A repeatable library of simple red flags before people click, pay, scan, or log in.",
    ),
    ContentSeries(
        series_id="send_this_to_your_parents",
        name="Send This To Your Parents",
        promise="Simple non-patronizing scam-defense videos people can share with family.",
        audience="families and people protecting parents, partners, teens, or colleagues",
        allowed_hook_types=("family_share", "urgent_message", "simple_rule"),
        format_rules=(
            "avoid Boomer framing externally",
            "use large readable text and everyday language",
            "show a share-worthy family-safety situation",
            "end with a screenshot checklist or rule",
        ),
        visual_grammar=("SMS", "family chat", "support popup", "payment request", "new number message"),
        no_go_patterns=FORBIDDEN_PUBLIC_TOPICS,
        follow_reason="Videos worth sending to someone you want to protect.",
    ),
    ContentSeries(
        series_id="before_you_click",
        name="Before You Click",
        promise="Quick checks before links, QR codes, logins, payments, or popups.",
        audience="normal users who want a simple pause rule",
        allowed_hook_types=("pause", "link_warning", "qr_warning", "login_warning"),
        format_rules=(
            "show the click/scan/login moment",
            "name why it feels safe",
            "reveal what to check before acting",
            "give a safer path the viewer can do immediately",
        ),
        visual_grammar=("link", "QR", "login page", "payment page", "popup warning"),
        no_go_patterns=FORBIDDEN_PUBLIC_TOPICS,
        follow_reason="One safer habit before the next risky click.",
    ),
    ContentSeries(
        series_id="money_move_red_flags",
        name="Money Move Red Flags",
        promise="Spot risky payment, invoice, QR, marketplace, and bank-route changes before money moves.",
        audience="families, workers, freelancers, small teams, and small business owners",
        allowed_hook_types=("payment_warning", "invoice_red_flag", "route_change", "marketplace_warning"),
        format_rules=(
            "show the payment/invoice/chat screen",
            "show the one money-related red flag",
            "give one verification move outside the suspicious message",
            "avoid financial guarantees or advice claims",
        ),
        visual_grammar=("invoice", "payment route", "bank-change email", "marketplace chat", "QR payment"),
        no_go_patterns=FORBIDDEN_PUBLIC_TOPICS,
        follow_reason="Small checks before money leaves the account.",
    ),
    ContentSeries(
        series_id="account_trap",
        name="Account Trap",
        promise="Explain fake logins, passwords, MFA prompts, and account-closure threats in plain language.",
        audience="beginners who want to protect accounts without security jargon",
        allowed_hook_types=("fake_login", "password_warning", "login_prompt", "account_closure"),
        format_rules=(
            "show the login/account screen",
            "replace jargon with plain words like 'stealing your login'",
            "give one safer account action",
            "avoid technical attack walkthroughs",
        ),
        visual_grammar=("login page", "password field", "MFA prompt", "account warning", "session screen"),
        no_go_patterns=FORBIDDEN_PUBLIC_TOPICS,
        follow_reason="Simple account safety rules that normal people remember.",
    ),

    ContentSeries(
        series_id="prompt_vs_system",
        name="Prompt vs System",
        promise="Legacy secondary AI format: compare prompt hacks with durable workflow/system boundaries.",
        audience="AI workflow builders",
        allowed_hook_types=("prompt_contrast", "system_boundary"),
        format_rules=("Keep as secondary specialist format, not core channel positioning.",),
        visual_grammar=("prompt card", "workflow boundary", "mechanism diagram"),
        no_go_patterns=FORBIDDEN_PUBLIC_TOPICS,
        follow_reason="Legacy AI audience can still follow for systems-over-prompts breakdowns.",
    ),
    ContentSeries(
        series_id="ai_system_autopsy",
        name="AI System Autopsy",
        promise="Legacy secondary AI format: show why an AI workflow fails and what boundary fixes it.",
        audience="AI workflow builders",
        allowed_hook_types=("failure_autopsy", "boundary_fix"),
        format_rules=("Concrete workflow failure, not generic AI tools.",),
        visual_grammar=("inbox card", "failure path", "repair boundary"),
        no_go_patterns=FORBIDDEN_PUBLIC_TOPICS,
        follow_reason="Shows practical AI system failures without hype.",
    ),
    ContentSeries(
        series_id="one_rule_one_example",
        name="One Rule, One Example",
        promise="Legacy secondary format: one simple rule demonstrated through one concrete example.",
        audience="AI/workflow users",
        allowed_hook_types=("single_rule", "example"),
        format_rules=("One rule, one example, no listicle sprawl.",),
        visual_grammar=("before/after", "single checklist", "rule card"),
        no_go_patterns=FORBIDDEN_PUBLIC_TOPICS,
        follow_reason="Simple rules viewers can remember and reuse.",
    ),
    ContentSeries(
        series_id="ai_hype_court",
        name="AI Hype Court",
        promise="Legacy secondary AI format: test AI hype claims against practical boundaries.",
        audience="AI-curious viewers",
        allowed_hook_types=("claim_verdict", "hype_check"),
        format_rules=("High-level claim check only; keep it practical and public-safe.",),
        visual_grammar=("claim card", "verdict card", "mechanism reveal"),
        no_go_patterns=FORBIDDEN_PUBLIC_TOPICS,
        follow_reason="Separates useful AI mechanisms from hype.",
    ),
)


def _is_blank(value: str) -> bool:
    return not (value or "").strip()


def _series_text(series: ContentSeries) -> str:
    return " ".join(
        (
            series.series_id,
            series.name,
            series.promise,
            series.audience,
            " ".join(series.allowed_hook_types),
            " ".join(series.format_rules),
            " ".join(series.visual_grammar),
            series.follow_reason,
        )
    ).casefold()


def _candidate_public_text(candidate: VideoCandidate) -> str:
    return " ".join(
        (
            candidate.title,
            candidate.hook,
            candidate.script_outline,
            candidate.visual_concept,
            candidate.caption,
            candidate.cta,
        )
    ).casefold()


def find_series(
    series_id: str,
    registry: tuple[ContentSeries, ...] = DEFAULT_CONTENT_SERIES,
) -> ContentSeries | None:
    for series in registry:
        if series.series_id == series_id:
            return series
    return None


def _forbidden_matches(text: str) -> tuple[str, ...]:
    matches: list[str] = []
    for canonical_topic, patterns in FORBIDDEN_PUBLIC_TOPIC_PATTERNS.items():
        if any(pattern in text for pattern in patterns):
            matches.append(canonical_topic)
    return tuple(matches)


def _candidate_matches_series(candidate: VideoCandidate, series: ContentSeries) -> bool:
    if series.series_id in {"prompt_vs_system", "ai_system_autopsy", "one_rule_one_example", "ai_hype_court"}:
        return True
    public_text = _candidate_public_text(candidate)
    series_keywords = {
        "one_screen_one_red_flag": ("red flag", "screen", "safer", "trap", "scam"),
        "send_this_to_your_parents": ("family", "parents", "mom", "dad", "message", "send this"),
        "before_you_click": ("click", "tap", "link", "qr", "scan", "login"),
        "money_move_red_flags": ("invoice", "payment", "pay", "bank", "money", "marketplace"),
        "account_trap": ("login", "password", "account", "mfa", "reset", "sign in"),
    }
    keywords = series_keywords.get(series.series_id, ())
    return any(keyword in public_text for keyword in keywords)


def validate_content_series_registry(
    registry: tuple[ContentSeries, ...] = DEFAULT_CONTENT_SERIES,
) -> ValidationResult:
    errors: list[str] = []
    seen: set[str] = set()

    if not registry:
        errors.append("at least one content series is required")

    for series in registry:
        if _is_blank(series.series_id):
            errors.append("series_id is required")
        elif series.series_id in seen:
            errors.append(f"duplicate series_id: {series.series_id}")
        seen.add(series.series_id)

        for field_name, value in {
            "name": series.name,
            "promise": series.promise,
            "audience": series.audience,
            "follow_reason": series.follow_reason,
        }.items():
            if _is_blank(value):
                errors.append(f"series {series.series_id} {field_name} is required")

        for field_name, values in {
            "allowed_hook_types": series.allowed_hook_types,
            "format_rules": series.format_rules,
            "visual_grammar": series.visual_grammar,
            "no_go_patterns": series.no_go_patterns,
        }.items():
            if not values:
                errors.append(f"series {series.series_id} {field_name} is required")

        series_text = _series_text(series)
        for match in _forbidden_matches(series_text):
            errors.append(f"series {series.series_id} uses forbidden public topic: {match}")

    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def validate_candidate_series_fit(
    candidate: VideoCandidate,
    series_id: str,
    *,
    registry: tuple[ContentSeries, ...] = DEFAULT_CONTENT_SERIES,
) -> ValidationResult:
    errors: list[str] = []
    series = find_series(series_id, registry)
    if series is None:
        return ValidationResult(False, (f"unknown series: {series_id}",))

    public_text = _candidate_public_text(candidate)
    for match in _forbidden_matches(public_text):
        errors.append(f"candidate uses forbidden public topic: {match}")

    if not _candidate_matches_series(candidate, series):
        errors.append(f"candidate does not clearly match series: {series_id}")

    return ValidationResult(is_valid=not errors, errors=tuple(errors))
