"""TrueTraceShorts production registries and hard gates.

Side-effect-free guardrails for the format-family → topic-registry → asset-check
→ review-package workflow. This module deliberately does not render, upload,
write files, generate images, call TTS, or call platform APIs.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import StrEnum
import json
from pathlib import Path
import re
from typing import Any, Mapping, Sequence

from autoshorts.ideas.candidate import ValidationResult

ROOT = Path(__file__).resolve().parents[2]
DEFAULT_STRATEGY_DIR = ROOT / "data" / "strategy"


class ProductionStage(StrEnum):
    IDEA = "IDEA"
    FORMAT_SELECTED = "FORMAT_SELECTED"
    SCAM_TYPE_SELECTED = "SCAM_TYPE_SELECTED"
    EXISTING_ASSET_CHECKED = "EXISTING_ASSET_CHECKED"
    SCRIPT_SELECTED_OR_CREATED = "SCRIPT_SELECTED_OR_CREATED"
    SCRIPT_QA_PASSED = "SCRIPT_QA_PASSED"
    VISUAL_BRIEF_CREATED = "VISUAL_BRIEF_CREATED"
    PREMIUM_KEYFRAMES_GENERATED = "PREMIUM_KEYFRAMES_GENERATED"
    KEYFRAME_QA_PASSED = "KEYFRAME_QA_PASSED"
    ANIMATIC_PREVIEW_READY = "ANIMATIC_PREVIEW_READY"
    USER_REVIEW_READY = "USER_REVIEW_READY"
    APPROVED_FOR_FINAL_RENDER = "APPROVED_FOR_FINAL_RENDER"
    FINAL_RENDER_READY = "FINAL_RENDER_READY"
    MANUAL_UPLOAD_PACK_READY = "MANUAL_UPLOAD_PACK_READY"
    WEBSITE_COMPANION_READY = "WEBSITE_COMPANION_READY"
    PUBLISHED_OR_ARCHIVED = "PUBLISHED_OR_ARCHIVED"


STATE_ORDER: tuple[ProductionStage, ...] = tuple(ProductionStage)


class TopicStatus(StrEnum):
    IDEA = "idea"
    SCRIPTED = "scripted"
    KEYFRAMES_READY = "keyframes_ready"
    PREVIEW_READY = "preview_ready"
    USER_REVIEW = "user_review"
    APPROVED_FOR_FINAL = "approved_for_final"
    UPLOAD_DRY_RUN_VALID = "upload_dry_run_valid"
    PUBLISHED = "published"
    PAUSED = "paused"
    REJECTED = "rejected"
    ARCHIVED = "archived"


class VisualStyle(StrEnum):
    PREMIUM_AI_KEYFRAMES_ONLY = "premium_ai_keyframes_only"
    PREMIUM_AI_KEYFRAMES_ONLY_REQUIRED = "premium_ai_keyframes_only_required"


@dataclass(frozen=True)
class PremiumVisualPolicy:
    renderer_owned_diagrams: bool = False
    powerpoint_like_graphics: bool = False
    old_mockup_renderer: bool = False
    self_built_ui_final: bool = False
    non_premium_visuals: bool = False
    visual_can_fit_any_generic_ai_video: bool = False
    first_frame_problem_not_visible: bool = False
    no_premium_keyframes: bool = False
    internal_layout_proof_only: bool = False
    subtitles_only_renderer: bool = True
    premium_ai_keyframes: bool = True


def validate_premium_visual_policy(policy: PremiumVisualPolicy, *, release_candidate: bool = True) -> ValidationResult:
    errors: list[str] = []
    blockers = {
        "renderer_owned_diagrams": policy.renderer_owned_diagrams,
        "powerpoint_like_graphics": policy.powerpoint_like_graphics,
        "old_mockup_renderer": policy.old_mockup_renderer,
        "self_built_ui_final": policy.self_built_ui_final,
        "non_premium_visuals": policy.non_premium_visuals,
        "visual_can_fit_any_generic_ai_video": policy.visual_can_fit_any_generic_ai_video,
        "first_frame_problem_not_visible": policy.first_frame_problem_not_visible,
        "no_premium_keyframes": policy.no_premium_keyframes,
    }
    for name, enabled in blockers.items():
        if enabled:
            errors.append(f"PremiumVisualPolicy blocks release: {name}=true")
    if release_candidate and policy.internal_layout_proof_only:
        errors.append("internal layout proofs cannot be release candidates")
    if release_candidate and not policy.premium_ai_keyframes:
        errors.append("premium_ai_keyframes are required for release candidate")
    if release_candidate and not policy.subtitles_only_renderer:
        errors.append("renderer must only add motion/crop/audio/captions for release candidate")
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


@dataclass(frozen=True)
class ExistingAssetCheck:
    topic_id: str
    scam_type_id: str
    keywords: tuple[str, ...]
    script: str | None = None
    renderer: str | None = None
    keyframes: tuple[str, ...] = ()
    website_page: str | None = None
    blocked_legacy_assets: tuple[str, ...] = ()
    selected_source_of_truth: str | None = None
    checked: bool = False

    def report(self) -> str:
        return "\n".join(
            [
                "Existing assets found:",
                f"script: {self.script or 'none'}",
                f"renderer: {self.renderer or 'none'}",
                f"keyframes: {', '.join(self.keyframes) if self.keyframes else 'none'}",
                f"website page: {self.website_page or 'none'}",
                f"blocked legacy assets: {', '.join(self.blocked_legacy_assets) if self.blocked_legacy_assets else 'none'}",
                f"selected source of truth: {self.selected_source_of_truth or 'none'}",
            ]
        )


def validate_existing_asset_check(check: ExistingAssetCheck) -> ValidationResult:
    errors: list[str] = []
    if not check.checked:
        errors.append("EXISTING_ASSET_CHECK is required before production")
    if not check.topic_id.strip():
        errors.append("asset check topic_id is required")
    if not check.scam_type_id.strip():
        errors.append("asset check scam_type_id is required")
    if not check.selected_source_of_truth:
        errors.append("selected_source_of_truth is required")
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


@dataclass(frozen=True)
class PlannedKeyframe:
    index: int
    scene_description: str
    visible_problem: str
    safety_constraints: tuple[str, ...] = ()


@dataclass(frozen=True)
class ReviewPackageRequired:
    candidate_id: str
    format_family: str
    scam_type_id: str
    topic_id: str
    script_version: str
    script_hash: str
    existing_assets_checked: ExistingAssetCheck
    selected_script_path: str
    visual_policy: PremiumVisualPolicy
    blocked_legacy_assets: tuple[str, ...]
    planned_keyframes: tuple[PlannedKeyframe, ...]
    caption_style: str
    voice: str
    website_companion_slug: str
    cta: str
    expected_metric: str
    risks: tuple[str, ...]
    approval_command: str


@dataclass(frozen=True)
class WebsiteCompanionPackageRequired:
    slug: str
    title: str
    hook: str
    scam_type_id: str
    redFlag: str
    whyItWorks: str
    saferMove: str
    ifAlreadyClicked: tuple[str, ...]
    checklist: tuple[str, ...]
    checklistDetails: tuple[str, ...]
    videoUrl: str | None
    thumbnail: str | None
    toolRelevance: tuple[str, ...]
    affiliateCategory: str
    seoTitle: str
    metaDescription: str
    body: str


_NO_REAL_DATA_PATTERNS = (
    re.compile(r"\b(?:\+?\d[\d .()/-]{7,}\d)\b"),
    re.compile(r"\b[A-Z]{2}\d{2}[A-Z0-9]{10,30}\b"),
    re.compile(r"\b(?:\d[ -]*?){13,19}\b"),
    re.compile(r"\b(?!example\.com\b)(?:[a-z0-9-]+\.)+(?:com|net|org|ch|de|co|io|app)\b", re.I),
)

_FORBIDDEN_SCAMBAITING_TERMS = (
    "scambait",
    "bait the scammer",
    "contact the scammer",
    "humiliate the scammer",
    "callcenter infiltration",
    "rat malware",
    "malware",
    "hack back",
    "counterattack",
    "dox",
)


def validate_no_real_sensitive_data(text: str) -> ValidationResult:
    errors: list[str] = []
    for pattern in _NO_REAL_DATA_PATTERNS:
        if pattern.search(text or ""):
            errors.append("real names/domains/phone/bank/card-like data are blocked in scam examples")
            break
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def validate_scambaiting_policy(text: str) -> ValidationResult:
    lower = (text or "").casefold()
    errors = [f"scambaiting/hacking action content blocked: {term}" for term in _FORBIDDEN_SCAMBAITING_TERMS if term in lower]
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def validate_review_package_required(package: ReviewPackageRequired) -> ValidationResult:
    errors: list[str] = []
    for name in ("candidate_id", "format_family", "scam_type_id", "topic_id", "script_version", "script_hash", "selected_script_path", "caption_style", "voice", "website_companion_slug", "cta", "expected_metric", "approval_command"):
        if not str(getattr(package, name) or "").strip():
            errors.append(f"{name} is required")
    errors.extend(validate_existing_asset_check(package.existing_assets_checked).errors)
    errors.extend(validate_premium_visual_policy(package.visual_policy).errors)
    if not package.planned_keyframes:
        errors.append("planned_keyframes are required before render")
    if "active-word" not in package.caption_style and "active word" not in package.caption_style:
        errors.append("caption style must remain static active-word")
    if "box" in package.caption_style.casefold() and "no box" not in package.caption_style.casefold():
        errors.append("caption style must not use a box/frame")
    text = " ".join([package.cta, package.topic_id, package.scam_type_id, *(k.scene_description for k in package.planned_keyframes)])
    errors.extend(validate_no_real_sensitive_data(text).errors)
    errors.extend(validate_scambaiting_policy(text).errors)
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def validate_website_companion_package_required(package: WebsiteCompanionPackageRequired) -> ValidationResult:
    errors: list[str] = []
    required = ("slug", "title", "hook", "scam_type_id", "redFlag", "whyItWorks", "saferMove", "seoTitle", "metaDescription", "body")
    for name in required:
        if not str(getattr(package, name) or "").strip():
            errors.append(f"WebsiteCompanionPackage {name} is required")
    if not package.ifAlreadyClicked:
        errors.append("WebsiteCompanionPackage ifAlreadyClicked is required")
    if not package.checklist:
        errors.append("WebsiteCompanionPackage checklist is required")
    text = " ".join(str(getattr(package, name) or "") for name in required)
    errors.extend(validate_no_real_sensitive_data(text).errors)
    errors.extend(validate_scambaiting_policy(text).errors)
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def validate_stage_sequence(stages: Sequence[ProductionStage | str]) -> ValidationResult:
    errors: list[str] = []
    normalized = [ProductionStage(stage) for stage in stages]
    if not normalized:
        return ValidationResult(False, ("production state sequence is required",))
    expected_prefix = list(STATE_ORDER[: len(normalized)])
    if normalized != expected_prefix:
        errors.append("production state machine blocks skipped stages")
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def render_allowed(
    *,
    stages: Sequence[ProductionStage | str],
    review_package: ReviewPackageRequired | None,
    website_companion: WebsiteCompanionPackageRequired | None = None,
    final_or_upload_pack: bool = False,
) -> ValidationResult:
    errors: list[str] = []
    stage_result = validate_stage_sequence(stages)
    errors.extend(stage_result.errors)
    normalized = [ProductionStage(stage) for stage in stages]
    if ProductionStage.EXISTING_ASSET_CHECKED not in normalized:
        errors.append("no images or render before EXISTING_ASSET_CHECKED")
    if ProductionStage.KEYFRAME_QA_PASSED not in normalized:
        errors.append("no render before KEYFRAME_QA_PASSED")
    if review_package is None:
        errors.append("ReviewPackage is required before render")
    else:
        errors.extend(validate_review_package_required(review_package).errors)
    if final_or_upload_pack:
        if ProductionStage.USER_REVIEW_READY not in normalized or ProductionStage.APPROVED_FOR_FINAL_RENDER not in normalized:
            errors.append("no final without user approval")
        if website_companion is None:
            errors.append("WebsiteCompanionPackage is required before upload pack")
        else:
            errors.extend(validate_website_companion_package_required(website_companion).errors)
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def _load_json(path: Path) -> Mapping[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def load_format_families(path: Path | None = None) -> Mapping[str, Any]:
    return _load_json(path or DEFAULT_STRATEGY_DIR / "format_families.json")


def load_scam_type_atlas(path: Path | None = None) -> Mapping[str, Any]:
    return _load_json(path or DEFAULT_STRATEGY_DIR / "scam_type_atlas.json")


def load_topic_registry(path: Path | None = None) -> Mapping[str, Any]:
    return _load_json(path or DEFAULT_STRATEGY_DIR / "topic_registry.json")


def validate_format_family_registry(registry: Mapping[str, Any]) -> ValidationResult:
    errors: list[str] = []
    expected = {"RED_FLAG_SHORT", "ALREADY_CLICKED", "TRUE_SCAM_STORY", "SPOT_THE_TRAP_QUIZ", "SCAM_BREAKDOWN"}
    families = {entry.get("format_family") for entry in registry.get("families", [])}
    missing = expected - families
    if missing:
        errors.append(f"format family registry missing: {sorted(missing)}")
    mix = registry.get("mix_percent", {})
    if sum(int(v) for v in mix.values()) != 100:
        errors.append("format mix must total 100")
    if mix.get("RED_FLAG_SHORT") != 50 or mix.get("ALREADY_CLICKED") != 20:
        errors.append("format mix must match current production distribution")
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def validate_scam_type_atlas(atlas: Mapping[str, Any]) -> ValidationResult:
    errors: list[str] = []
    entries = atlas.get("entries", [])
    if len(entries) < 20:
        errors.append("scam type atlas must include at least the 20 start entries")
    ids = [entry.get("scam_type_id") for entry in entries]
    if len(ids) != len(set(ids)):
        errors.append("scam type atlas contains duplicate scam_type_id")
    for entry in entries:
        sid = entry.get("scam_type_id", "<missing>")
        if not entry.get("safer_move"):
            errors.append(f"{sid}: saferMove/safer_move is required")
        if not entry.get("if_already_happened"):
            errors.append(f"{sid}: ifAlreadyHappened/if_already_happened is required")
        if not entry.get("normal_screen_example"):
            errors.append(f"{sid}: normal_screen_example is required")
        text = " ".join(str(entry.get(k, "")) for k in ("short_definition", "normal_screen_example", "safer_move"))
        errors.extend(validate_scambaiting_policy(text).errors)
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def validate_topic_registry(registry: Mapping[str, Any], atlas: Mapping[str, Any] | None = None) -> ValidationResult:
    errors: list[str] = []
    topics = registry.get("topics", [])
    ids = [topic.get("topic_id") for topic in topics]
    if len(ids) != len(set(ids)):
        errors.append("topic registry contains duplicate topic_id")
    allowed_status = {status.value for status in TopicStatus}
    atlas_ids = {entry.get("scam_type_id") for entry in (atlas or {}).get("entries", [])}
    for topic in topics:
        tid = topic.get("topic_id", "<missing>")
        if topic.get("status") not in allowed_status:
            errors.append(f"{tid}: invalid status")
        if atlas_ids and topic.get("scam_type_id") not in atlas_ids:
            errors.append(f"{tid}: scam_type_id not found in atlas")
        if topic.get("status") == TopicStatus.REJECTED.value and topic.get("reuse_allowed") is not False:
            errors.append(f"{tid}: rejected topics must set reuse_allowed=false")
        if topic.get("status") != TopicStatus.REJECTED.value and not topic.get("approved_visual_style"):
            errors.append(f"{tid}: approved_visual_style is required")
    return ValidationResult(is_valid=not errors, errors=tuple(errors))


def topic_by_id(registry: Mapping[str, Any], topic_id: str) -> Mapping[str, Any] | None:
    for topic in registry.get("topics", []):
        if topic.get("topic_id") == topic_id:
            return topic
    return None


def block_unmanaged_duplicate_topic(registry: Mapping[str, Any], *, topic_id: str, scam_type_id: str, format_family: str) -> ValidationResult:
    topic = topic_by_id(registry, topic_id)
    if topic is not None:
        if topic.get("status") == TopicStatus.REJECTED.value:
            return ValidationResult(False, ("topic exists but is rejected; do not reuse as release candidate",))
        return ValidationResult(False, ("topic already exists; reuse registry source of truth instead of creating unmanaged duplicate",))
    for existing in registry.get("topics", []):
        if existing.get("scam_type_id") == scam_type_id and existing.get("format_family") == format_family and existing.get("status") != TopicStatus.REJECTED.value:
            return ValidationResult(False, ("similar topic exists; run Existing Asset Check and justify reuse/new variant",))
    return ValidationResult(True, ())
