from __future__ import annotations

import re
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any

from fastapi import HTTPException
from sqlmodel import Session, select

from app.models.core import (
    ActivityEvent,
    AnalyticsPostSnapshot,
    AppSetting,
    ContentScript,
    ExternalPost,
    Idea,
    PostDraft,
    ProductionQueueItem,
    Theme,
    VideoAsset,
    WebsiteCompanion,
    now_utc,
)

ACTIVE_NEXT_STATUSES = ["locked_next", "approved_next"]
ACTIVE_QUEUE_STATUSES = ["suggested", "approved_next", "locked_next", "concept_proposed", "concept_approved", "producing", "in_review", "needs_changes", "held"]
ARCHIVE_STATUSES = ["archived", "done", "archived_pre_real_run", "hidden_from_active_review"]

STOPWORDS = {
    "this", "that", "your", "you", "the", "and", "for", "with", "from", "will", "stop", "dont", "don", "didnt", "did",
    "account", "message", "warning", "scam", "fake", "short", "video", "test", "preview", "says", "locked", "lock",
}

TEMPLATE_ANGLES = [
    {
        "suffix": "the second payment trap",
        "hook": "The first fee is not the scam — the second payment request is.",
        "brief": "Show a fake small delivery/customs fee flow where the danger is the follow-up card-verification step, then give the safer move: open the official delivery app/site manually.",
        "format": "Fast phone-screen style red-flag explainer with one clear safer action.",
        "keywords": ["delivery", "fee", "customs", "payment", "card"],
    },
    {
        "suffix": "when support calls after the popup",
        "hook": "If a popup gives you a phone number, the phone call is part two of the scam.",
        "brief": "Open with a scary device/security popup, then show the dangerous shift to phone support and remote access pressure.",
        "format": "Dramatic popup-to-call escalation, high contrast UI, calm safety instruction.",
        "keywords": ["support", "popup", "phone", "remote", "call"],
    },
    {
        "suffix": "the changed invoice detail check",
        "hook": "One changed bank detail can redirect the whole invoice.",
        "brief": "Business-safety short: changed payment details in an invoice email, verify through an existing trusted channel before paying.",
        "format": "Office/email scenario with before-pay checklist.",
        "keywords": ["invoice", "bank", "payment", "business", "supplier"],
    },
    {
        "suffix": "the courier chatbot pressure trick",
        "hook": "A courier chatbot asking for a card is not customer support.",
        "brief": "Messenger/chatbot delivery flow that pushes a tiny fee, card update or identity check; safer move is app-first verification.",
        "format": "Chat interface beats, subtitle-heavy, simple red flag labels.",
        "keywords": ["delivery", "chatbot", "courier", "fee", "card"],
    },
    {
        "suffix": "new number, old pressure",
        "hook": "The new number is not the red flag — the rushed money request is.",
        "brief": "Family-message scam with a new number and urgent payment/gift-card ask. Show callback verification before acting.",
        "format": "Text-message story with emotional pressure cue and calm verification step.",
        "keywords": ["new", "number", "family", "payment", "gift"],
    },
    {
        "suffix": "creator collab login trap",
        "hook": "A brand deal that starts with a login link is not a brand deal.",
        "brief": "Creator-account safety: fake collaboration email or DM asks for login/permissions. Safer move: verify sender and never log in from the message link.",
        "format": "Creator dashboard/DM visual metaphor, premium clean look.",
        "keywords": ["creator", "brand", "login", "collab", "account"],
    },
    {
        "suffix": "refund link that steals the card",
        "hook": "A refund should not need your full card again.",
        "brief": "Fake refund/overpayment link asks for card details. Explain why legitimate refunds usually reverse to original payment method.",
        "format": "Payment-screen close-up with one strong rule.",
        "keywords": ["refund", "card", "payment", "link"],
    },
    {
        "suffix": "side hustle app upfront fee",
        "hook": "If the job starts with you paying them, pause.",
        "brief": "Side-hustle/app opportunity asks for activation, training, or verification fee. Safer move: research outside the app and never pay to unlock income.",
        "format": "App-store/job-offer style, punchy risk labels.",
        "keywords": ["job", "side", "hustle", "fee", "app"],
    },
    {
        "suffix": "QR code at the parking meter",
        "hook": "A sticker QR code can replace the real payment page.",
        "brief": "Public QR/payment red flag: parking/charging meter sticker QR sends to fake payment page. Safer move: use official app or typed URL.",
        "format": "Real-world object + phone screen split visual.",
        "keywords": ["qr", "parking", "payment", "sticker"],
    },
    {
        "suffix": "bank code read-aloud replay",
        "hook": "If someone asks for the code, the code is already the target.",
        "brief": "Follow-up to bank-code performer: show how attackers reframe a verification code as a security check; safer move: hang up and call back through official app.",
        "format": "Voice-call pressure scenario with code box visual.",
        "keywords": ["bank", "code", "call", "verification"],
    },
    {
        "suffix": "marketplace pickup payment switch",
        "hook": "The payment switch is where the marketplace protection disappears.",
        "brief": "Buyer/seller moves chat off-platform and changes payment method; show why protection is lost.",
        "format": "Marketplace chat + payment-method switch visual.",
        "keywords": ["marketplace", "payment", "seller", "buyer"],
    },
    {
        "suffix": "subscription renewal panic screen",
        "hook": "A renewal warning with a countdown wants speed, not truth.",
        "brief": "Fake subscription renewal popup/email pushes immediate payment update; safer move: open the service manually and check billing.",
        "format": "Countdown/panic screen defused by calm app-first step.",
        "keywords": ["subscription", "renewal", "countdown", "billing"],
    },
]

FAMILY_HINTS = {
    "delivery": ["delivery", "courier", "fee", "customs", "parcel"],
    "support": ["support", "popup", "phone", "remote"],
    "account": ["account", "login", "lock", "mfa", "code"],
    "invoice": ["invoice", "bank", "business", "supplier"],
    "creator": ["creator", "brand", "collab", "login"],
    "marketplace": ["marketplace", "seller", "buyer"],
}


def normalize_text(value: str | None) -> str:
    return " ".join(re.sub(r"[^a-z0-9]+", " ", (value or "").lower()).split())


def token_set(value: str | None) -> set[str]:
    return {w for w in normalize_text(value).split() if len(w) > 2 and w not in STOPWORDS}


def similarity(a: str | None, b: str | None) -> float:
    left, right = token_set(a), token_set(b)
    if not left or not right:
        return 0.0
    return len(left & right) / len(left | right)


def expected_package_id(item: ProductionQueueItem) -> str:
    return item.expected_package_id or f"package_{item.id[:8]}"


def expected_manifest(item: ProductionQueueItem) -> str:
    return item.expected_package_manifest or f"storage/incoming/{expected_package_id(item)}/manifest.json"


def active_queue_query():
    return select(ProductionQueueItem).where(ProductionQueueItem.status.notin_(ARCHIVE_STATUSES)).order_by(ProductionQueueItem.position, ProductionQueueItem.priority.desc(), ProductionQueueItem.created_at)


def list_queue(session: Session, include_archived: bool = False) -> dict[str, Any]:
    stmt = select(ProductionQueueItem).order_by(ProductionQueueItem.position, ProductionQueueItem.priority.desc(), ProductionQueueItem.created_at)
    if not include_archived:
        stmt = stmt.where(ProductionQueueItem.status.notin_(ARCHIVE_STATUSES))
    items = session.exec(stmt).all()
    return {"items": [serialize_item(session, item) for item in items], "count": len(items), "active_count": len([i for i in items if i.status not in ARCHIVE_STATUSES])}


def _all_existing_titles(session: Session) -> list[dict[str, Any]]:
    rows: list[dict[str, Any]] = []
    for v in session.exec(select(VideoAsset)).all():
        status = getattr(v.status, "value", v.status)
        if status in {"archived", "hidden_from_active_review", "archived_pre_real_run", "rejected"}:
            continue
        rows.append({"kind": "video", "title": v.working_title or v.candidate_id, "family_id": v.topic_id, "package_id": v.package_id, "status": v.status, "created_at": v.created_at})
    for d in session.exec(select(PostDraft)).all():
        if d.video_asset_id:
            video = session.get(VideoAsset, d.video_asset_id)
            status = getattr(video.status, "value", video.status) if video else None
            if status in {"archived", "hidden_from_active_review", "archived_pre_real_run", "rejected"}:
                continue
        if d.title:
            rows.append({"kind": "youtube_draft", "title": d.title, "family_id": None, "package_id": None, "status": d.status, "created_at": d.created_at})
        if d.caption:
            rows.append({"kind": "caption", "title": d.caption, "family_id": None, "package_id": None, "status": d.status, "created_at": d.created_at})
    for s in session.exec(select(ContentScript)).all():
        rows.append({"kind": "script", "title": s.title, "family_id": s.family_id, "package_id": None, "status": s.status, "created_at": s.created_at})
    for p in session.exec(select(ExternalPost)).all():
        rows.append({"kind": "youtube_csv", "title": p.title, "family_id": p.topic_id, "package_id": None, "status": p.mapping_status, "created_at": p.created_at})
    for c in session.exec(select(WebsiteCompanion)).all():
        rows.append({"kind": "website_slug", "title": c.slug, "family_id": None, "package_id": None, "status": c.status, "created_at": c.created_at})
    return [r for r in rows if r.get("title")]


def duplicate_report(session: Session, title: str, hook: str | None = None, keywords: list[str] | None = None) -> dict[str, Any]:
    text = " ".join([title or "", hook or "", " ".join(keywords or [])])
    matches = []
    for row in _all_existing_titles(session):
        score = max(similarity(title, row["title"]), similarity(text, row["title"]))
        normalized = normalize_text(row["title"])
        if "account" in normalize_text(title) and ("locked" in normalized or "lock" in normalized):
            score = max(score, 0.72)
        if score >= 0.36:
            matches.append({"kind": row["kind"], "title": row["title"], "score": round(score, 2), "status": row.get("status"), "package_id": row.get("package_id")})
    matches = sorted(matches, key=lambda m: m["score"], reverse=True)[:4]
    if matches and matches[0]["score"] >= 0.7:
        risk = "high"
        recommendation = "Use only if the hook angle is significantly different."
    elif matches:
        risk = "medium"
        recommendation = "Variation is acceptable if the first sentence and safer action are clearly different."
    else:
        risk = "low"
        recommendation = "No close duplicate detected."
    return {"risk": risk, "similar_content": matches, "recommendation": recommendation}


def _family_stats(session: Session) -> dict[str, dict[str, Any]]:
    families = {f.id: {"family": f, "views": 0, "retention": None, "subscribers": 0, "videos": 0, "best": None} for f in session.exec(select(Theme)).all()}
    for v in session.exec(select(VideoAsset)).all():
        if v.topic_id in families:
            families[v.topic_id]["videos"] += 1
    snapshots = session.exec(select(AnalyticsPostSnapshot).order_by(AnalyticsPostSnapshot.snapshot_at.desc())).all()
    seen = set()
    latest = []
    for s in snapshots:
        key = (s.provider, s.account_id, s.external_post_id)
        if key in seen or s.is_demo:
            continue
        seen.add(key); latest.append(s)
    posts = {p.external_post_id: p for p in session.exec(select(ExternalPost)).all()}
    retention_values: dict[str, list[float]] = defaultdict(list)
    for snap in latest:
        post = posts.get(snap.external_post_id)
        family_id = post.topic_id if post else None
        if not family_id and snap.video_asset_id:
            video = session.get(VideoAsset, snap.video_asset_id)
            family_id = video.topic_id if video else None
        if family_id in families:
            families[family_id]["views"] += snap.views or 0
            families[family_id]["subscribers"] += snap.subscribers_delta or 0
            if snap.retention_proxy_pct is not None:
                retention_values[family_id].append(snap.retention_proxy_pct)
            if not families[family_id]["best"] or (snap.views or 0) > (families[family_id]["best"].views or 0):
                families[family_id]["best"] = snap
    for fid, values in retention_values.items():
        families[fid]["retention"] = round(sum(values) / len(values), 1) if values else None
    return families


def _family_for_angle(session: Session, angle: dict[str, Any], families: list[Theme], index: int) -> Theme | None:
    if not families:
        return None
    text = " ".join(angle["keywords"] + [angle["suffix"]])
    scored = []
    for family in families:
        ft = normalize_text(" ".join([family.title or "", family.description or "", family.audience_pain or "", family.promise or ""]))
        score = sum(1 for kw in angle["keywords"] if kw in ft)
        for _, kws in FAMILY_HINTS.items():
            if any(k in text for k in kws) and any(k in ft for k in kws):
                score += 1
        scored.append((score, family.priority or 0, family))
    best = max(scored, key=lambda row: (row[0], row[1]))
    return best[2] if best[0] > 0 else families[index % len(families)]


def build_recommendation_candidates(session: Session, limit: int = 16) -> list[dict[str, Any]]:
    stats = _family_stats(session)
    families = [s["family"] for s in sorted(stats.values(), key=lambda x: (x["views"], x["family"].priority), reverse=True)]
    if not families:
        families = session.exec(select(Theme).order_by(Theme.priority.desc())).all()
    recent = session.exec(select(VideoAsset).order_by(VideoAsset.created_at.desc())).all()[:8]
    recent_family_counts = Counter(v.topic_id for v in recent if v.topic_id)
    candidates: list[dict[str, Any]] = []
    used_titles = set()
    for idx, angle in enumerate(TEMPLATE_ANGLES):
        family = _family_for_angle(session, angle, families, idx)
        family_title = family.title if family else "Everyday Red Flags"
        base = family_title.replace("Scams", "").replace("Warnings", "").strip() or "Everyday red flag"
        title = f"{base}: {angle['suffix']}"
        if normalize_text(title) in used_titles:
            continue
        used_titles.add(normalize_text(title))
        family_stat = stats.get(family.id, {}) if family else {}
        duplicate = duplicate_report(session, title, angle["hook"], angle["keywords"])
        rotation_penalty = recent_family_counts.get(family.id if family else None, 0)
        priority = 100 + int(family_stat.get("views") or 0) + int(family.priority if family else 0) - rotation_penalty * 18
        if duplicate["risk"] == "high":
            priority -= 55
        elif duplicate["risk"] == "medium":
            priority -= 20
        rotation = "Rotate away soon" if rotation_penalty >= 2 else "Good rotation fit" if rotation_penalty == 0 else "Recently used once; angle must differ"
        reason = [
            f"Family performance: {family_stat.get('views', 0)} linked views" if family else "General red-flag coverage gap.",
            f"Hook angle: {angle['hook']}",
            duplicate["recommendation"],
            f"Rotation: {rotation}.",
        ]
        best = family_stat.get("best")
        if best:
            reason.insert(1, f"Best related performer: {best.title} with {best.views or 0} views.")
        candidates.append({
            "title": title,
            "family_id": family.id if family else None,
            "family": family_title,
            "hook": angle["hook"],
            "brief": angle["brief"],
            "reason": reason,
            "priority": priority,
            "style": angle["format"],
            "format": angle["format"],
            "rotation_hint": rotation,
            "duplicate_risk": duplicate["risk"],
            "duplicate_report": duplicate,
            "performance_reference": {"views": family_stat.get("views", 0), "retention": family_stat.get("retention"), "subscribers": family_stat.get("subscribers", 0), "best_video": best.title if best else None},
            "source": "recommendation",
            "status": "suggested",
            "keywords": angle["keywords"],
        })
    ideas = session.exec(select(Idea).where(Idea.status.in_(["idea", "needs_research", "planned", "approved"])).order_by(Idea.priority.desc())).all()
    for idea in ideas[:8]:
        family = session.get(Theme, idea.topic_id) if idea.topic_id else None
        title = idea.title
        if normalize_text(title) in used_titles:
            continue
        duplicate = duplicate_report(session, title, idea.short_explanation, idea.keywords or [])
        family_stat = stats.get(idea.topic_id, {}) if idea.topic_id else {}
        priority = int(idea.priority or 50) + int(family_stat.get("views") or 0) - (50 if duplicate["risk"] == "high" else 0)
        candidates.append({
            "title": title,
            "family_id": idea.topic_id,
            "family": family.title if family else idea.pillar,
            "hook": idea.short_explanation or title,
            "brief": idea.short_explanation or title,
            "reason": ["Manual/library idea is available.", duplicate["recommendation"], f"Performance reference: {family_stat.get('views', 0)} linked family views."],
            "priority": priority,
            "style": "Use the family’s proven short-form red-flag format.",
            "rotation_hint": "Manual/library idea; check recent-family balance before locking.",
            "duplicate_risk": duplicate["risk"],
            "duplicate_report": duplicate,
            "performance_reference": {"views": family_stat.get("views", 0), "retention": family_stat.get("retention"), "subscribers": family_stat.get("subscribers", 0)},
            "source": "recommendation",
            "status": "suggested",
        })
    return sorted(candidates, key=lambda c: (c["priority"], c["duplicate_risk"] == "low"), reverse=True)[:limit]


def ensure_recommendation_queue(session: Session, minimum: int = 12, replace_existing_suggestions: bool = False) -> dict[str, Any]:
    if replace_existing_suggestions:
        for item in session.exec(select(ProductionQueueItem).where(ProductionQueueItem.source == "recommendation", ProductionQueueItem.status == "suggested")).all():
            item.status = "archived"
            item.updated_at = now_utc()
            session.add(item)
        session.commit()
    active_suggestions = session.exec(select(ProductionQueueItem).where(ProductionQueueItem.source == "recommendation", ProductionQueueItem.status == "suggested")).all()
    if len(active_suggestions) >= minimum:
        return {"created": 0, "items": [serialize_item(session, i) for i in active_suggestions]}
    existing_norm = {normalize_text(i.title) for i in session.exec(select(ProductionQueueItem).where(ProductionQueueItem.status.notin_(ARCHIVE_STATUSES))).all()}
    max_pos = max([i.position for i in session.exec(select(ProductionQueueItem)).all()] or [0])
    created = []
    for cand in build_recommendation_candidates(session, limit=20):
        if len(active_suggestions) + len(created) >= minimum:
            break
        if normalize_text(cand["title"]) in existing_norm:
            continue
        item = ProductionQueueItem(
            family_id=cand.get("family_id"),
            title=cand["title"],
            brief=f"Hook: {cand['hook']}\n\nBrief: {cand['brief']}\n\nFormat: {cand.get('format') or cand.get('style') or 'Fast red-flag explainer'}",
            script_constraints="First sentence must land in <1s. Keep it concrete, no generic cyber jargon. End with one safer action. No final Wan render without explicit approval.",
            visual_constraints=f"{cand['style']} Avoid real scam URLs, phone numbers, bank details, private data, or unreviewed logos.",
            voice_constraints="Use approved Gianna premium voice for real render. Calm, confident, fast but readable.",
            reason="\n".join(cand["reason"]),
            priority=int(cand["priority"]),
            position=max_pos + len(created) + 1,
            status="suggested",
            source="recommendation",
        )
        session.add(item); session.commit(); session.refresh(item)
        created.append(item)
        existing_norm.add(normalize_text(item.title))
    if created:
        session.add(ActivityEvent(entity_type="production_queue", entity_id="bulk", event_type="production_recommendations_seeded", label="Production recommendations seeded", payload_json={"created": len(created), "minimum": minimum}))
        session.commit()
    items = session.exec(select(ProductionQueueItem).where(ProductionQueueItem.source == "recommendation", ProductionQueueItem.status == "suggested").order_by(ProductionQueueItem.position)).all()
    return {"created": len(created), "items": [serialize_item(session, i) for i in items]}


def serialize_item(session: Session, item: ProductionQueueItem) -> dict[str, Any]:
    family = session.get(Theme, item.family_id) if item.family_id else None
    idea = session.get(Idea, item.idea_id) if item.idea_id else None
    script = session.get(ContentScript, item.content_script_id) if item.content_script_id else None
    video = session.get(VideoAsset, item.video_asset_id) if item.video_asset_id else None
    brief = item.brief or ""
    hook = None
    for line in brief.splitlines():
        if line.lower().startswith("hook:"):
            hook = line.split(":", 1)[1].strip()
            break
    dupe = duplicate_report(session, item.title, hook)
    rotation = _rotation_hint_for_item(session, item)
    data = {
        "id": item.id,
        "family_id": item.family_id,
        "idea_id": item.idea_id,
        "content_script_id": item.content_script_id,
        "title": item.title,
        "brief": item.brief,
        "hook": hook,
        "script_constraints": item.script_constraints,
        "visual_constraints": item.visual_constraints,
        "voice_constraints": item.voice_constraints,
        "reason": item.reason,
        "reason_lines": [line for line in (item.reason or "").split("\n") if line],
        "priority": item.priority,
        "position": item.position,
        "status": item.status,
        "source": item.source,
        "expected_package_id": expected_package_id(item),
        "expected_package_manifest": expected_manifest(item),
        "actual_package_id": item.actual_package_id,
        "video_asset_id": item.video_asset_id,
        "attached_package_path": item.attached_package_path,
        "error": item.error,
        "family": family.title if family else None,
        "idea": idea.title if idea else None,
        "script": script.title if script else None,
        "video_title": video.working_title if video else None,
        "duplicate_risk": dupe["risk"],
        "similar_content": dupe["similar_content"],
        "duplicate_recommendation": dupe["recommendation"],
        "rotation_hint": rotation,
        "format_hint": _format_hint(item),
        "performance_reference": _performance_reference_for_item(session, item),
        "created_at": item.created_at.isoformat() if item.created_at else None,
        "updated_at": item.updated_at.isoformat() if item.updated_at else None,
    }
    return data


def _format_hint(item: ProductionQueueItem) -> str:
    text = item.visual_constraints or item.brief or ""
    return text.split(".")[0][:180] if text else "Fast red-flag explainer with one concrete safer action"


def _performance_reference_for_item(session: Session, item: ProductionQueueItem) -> dict[str, Any]:
    stats = _family_stats(session).get(item.family_id or "", {})
    best = stats.get("best")
    return {"views": stats.get("views", 0), "retention": stats.get("retention"), "subscribers": stats.get("subscribers", 0), "best_video": best.title if best else None}


def _rotation_hint_for_item(session: Session, item: ProductionQueueItem) -> str:
    recent = session.exec(select(VideoAsset).order_by(VideoAsset.created_at.desc())).all()[:5]
    count = len([v for v in recent if v.topic_id and v.topic_id == item.family_id])
    if count >= 3:
        return "High repetition risk: this family appeared often recently. Lock only if hook angle is clearly different."
    if count:
        return "Recently used family; keep variation strong."
    return "Good rotation fit: not overused in the last active videos."


def create_queue_item(session: Session, payload: dict[str, Any]) -> dict[str, Any]:
    max_pos = max([i.position for i in session.exec(select(ProductionQueueItem)).all()] or [0])
    item = ProductionQueueItem(
        family_id=payload.get("family_id"),
        idea_id=payload.get("idea_id"),
        content_script_id=payload.get("content_script_id"),
        title=payload.get("title") or payload.get("idea") or "Untitled production item",
        brief=payload.get("brief"),
        script_constraints=payload.get("script_constraints"),
        visual_constraints=payload.get("visual_constraints"),
        voice_constraints=payload.get("voice_constraints"),
        reason=payload.get("reason") if isinstance(payload.get("reason"), str) else "\n".join(payload.get("reason") or []),
        priority=int(payload.get("priority", 0)),
        position=int(payload.get("position", max_pos + 1)),
        status=payload.get("status") or "suggested",
        source=payload.get("source") or "manual",
        expected_package_id=payload.get("expected_package_id"),
        expected_package_manifest=payload.get("expected_package_manifest"),
    )
    if not item.expected_package_id:
        item.expected_package_id = expected_package_id(item)
    if not item.expected_package_manifest:
        item.expected_package_manifest = expected_manifest(item)
    session.add(item); session.commit(); session.refresh(item)
    session.add(ActivityEvent(entity_type="production_queue", entity_id=item.id, event_type="production_queue_item_created", label="Production queue item created", payload_json={"title": item.title, "source": item.source}))
    session.commit()
    return serialize_item(session, item)


def update_queue_item(session: Session, item_id: str, payload: dict[str, Any]) -> dict[str, Any]:
    item = session.get(ProductionQueueItem, item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Production queue item not found")
    for key in ["status", "priority", "position", "brief", "script_constraints", "visual_constraints", "voice_constraints", "reason", "expected_package_id", "expected_package_manifest", "actual_package_id", "video_asset_id", "attached_package_path", "error"]:
        if key in payload:
            setattr(item, key, payload[key])
    item.updated_at = now_utc()
    session.add(item); session.commit(); session.refresh(item)
    return serialize_item(session, item)


def add_recommendation(session: Session, recommendation: dict[str, Any]) -> dict[str, Any]:
    return create_queue_item(session, {
        "family_id": recommendation.get("family_id"),
        "title": recommendation.get("idea") or f"Next {recommendation.get('family') or 'video'} variant",
        "brief": recommendation.get("idea") or recommendation.get("recommended_action"),
        "reason": recommendation.get("reason") or [],
        "priority": 50,
        "status": "suggested",
        "source": "recommendation",
        "script_constraints": "Fast hook, short setup, concrete safer action. No final Wan render without approval.",
        "visual_constraints": "Preview/test render allowed. Avoid unreviewed logos, real scam URLs, phone numbers or payment details.",
        "voice_constraints": "Use approved Gianna premium voice only for real renders; test package may use silent preview.",
    })


def lock_next(session: Session, item_id: str) -> dict[str, Any]:
    rows = session.exec(select(ProductionQueueItem).where(ProductionQueueItem.status == "locked_next")).all()
    for row in rows:
        if row.id != item_id:
            row.status = "approved_next"
            row.updated_at = now_utc()
            session.add(row)
    session.commit()
    return update_queue_item(session, item_id, {"status": "locked_next", "position": 0})


def unlock_next(session: Session, item_id: str | None = None) -> dict[str, Any]:
    rows = session.exec(select(ProductionQueueItem).where(ProductionQueueItem.status.in_(["locked_next", "concept_proposed", "concept_approved", "producing"]))).all()
    changed = []
    for row in rows:
        if item_id is None or row.id == item_id:
            row.status = "approved_next"
            row.position = max(row.position, 1)
            row.updated_at = now_utc()
            session.add(row); changed.append(row.id)
    session.commit()
    return {"ok": True, "unlocked": changed, "next": next_for_agent(session)}


def replace_locked_with(session: Session, item_id: str) -> dict[str, Any]:
    unlock_next(session)
    return lock_next(session, item_id)


def move_item(session: Session, item_id: str, direction: str) -> dict[str, Any]:
    items = session.exec(active_queue_query()).all()
    idx = next((i for i, item in enumerate(items) if item.id == item_id), None)
    if idx is None:
        raise HTTPException(status_code=404, detail="Production queue item not found")
    swap_idx = idx - 1 if direction == "up" else idx + 1
    if swap_idx < 0 or swap_idx >= len(items):
        return serialize_item(session, items[idx])
    items[idx].position, items[swap_idx].position = items[swap_idx].position, items[idx].position
    items[idx].updated_at = now_utc(); items[swap_idx].updated_at = now_utc()
    session.add(items[idx]); session.add(items[swap_idx]); session.commit(); session.refresh(items[idx])
    return serialize_item(session, items[idx])


def concept_payload(session: Session, item: ProductionQueueItem) -> dict[str, Any]:
    data = serialize_item(session, item)
    title = item.title
    hook = data.get("hook") or (item.brief or title).split("\n")[0]
    family = data.get("family") or "Everyday Red Flags"
    caption = f"{hook}\n\nSafer move: if a refund page asks for full card details, close it and verify inside the official app or typed website."
    hashtags = ["#ScamAlert", "#OnlineSafety", "#DigitalSafety", "#EverydayRedFlags"]
    script_text = "\n".join([
        "A refund should not need your full card again.",
        "This page says refund, but asks for the card number, expiry date and CVC.",
        "That is the red flag: real refunds usually go back to the payment method you already used.",
        "If a refund link needs a new card, stop.",
        "Close the link. Open the delivery app or website yourself, and check the refund there.",
        "One screen. One red flag. Never give a card to receive a refund.",
    ])
    storyboard = [
        {"time": "0.0-1.0s", "beat": "Instant visual hook", "visual": "Phone close-up: fake refund page already asking for full card details; no real logo or URL.", "voice": "A refund should not need your full card again."},
        {"time": "1.0-3.5s", "beat": "Name the trap", "visual": "Camera pushes into the card form fields: card number, expiry, CVC.", "voice": "This page says refund, but asks for the card number, expiry date and CVC."},
        {"time": "3.5-6.5s", "beat": "Explain the mechanism", "visual": "Clean contrast between 'refund' label and the dangerous 'enter card' fields.", "voice": "That is the red flag: real refunds usually go back to the payment method you already used."},
        {"time": "6.5-9.0s", "beat": "Rule", "visual": "The link is closed; official app icon / typed address path appears as safer route.", "voice": "If a refund link needs a new card, stop."},
        {"time": "9.0-13.0s", "beat": "Safer move + takeaway", "visual": "Official-app-first verification; calm final frame with the phone safely away from the link.", "voice": "Close the link. Open the delivery app or website yourself, and check the refund there."},
    ]
    return {
        "queue_item_id": item.id,
        "title": title,
        "content_family": family,
        "approval_mode": "script_concept_only",
        "no_preview_video_required": True,
        "hook": hook,
        "hook_rationale": "The hook is strong because it creates instant self-recognition and contradiction: viewers expect a refund to send money back, not ask for a full card again. The first frame can show the exact danger without needing sound, so the red flag lands inside the first second.",
        "script_idea": item.brief or f"Open with: {hook}. Show the red flag, then one safer move.",
        "script_text": script_text,
        "storyboard": storyboard,
        "visual_hook_plan": "First frame must show one concrete screen: a refund page asking for card number, expiry and CVC. No PowerPoint cards, no generic warning icons, no real brands, no real URLs. Premium AI image should feel like a cinematic phone close-up; renderer-owned subtitles are the only text layer beyond the controlled fake form text.",
        "director_plan": "Use 3-5 premium AI/keyframe visuals: phone close-up, card-field danger push-in, contrast beat, close-link beat, official-app-first safer move. Keep movement controlled: slow push, cut on sentence beats, no shake, no diagram overlays. Final render only after Director QA verifies silent first-frame recognition, readable subtitles and no PowerPoint/mockup feel.",
        "visual_concept": item.visual_constraints or data.get("format_hint"),
        "voice_tone": item.voice_constraints or "Gianna premium voice, calm and direct, fast but readable.",
        "tiktok_caption": caption,
        "youtube_title": title[:95],
        "youtube_description": f"A short practical red-flag guide: {title}.\n\nIf a refund page asks for full card details, stop and verify through the official app or typed website.\n\nThis video is educational and uses synthetic/AI-generated media.",
        "hashtags": hashtags,
        "why_this_video": data.get("reason_lines") or [data.get("duplicate_recommendation"), data.get("rotation_hint")],
        "duplicate_risk": data.get("duplicate_risk"),
        "similar_content": data.get("similar_content"),
        "confirm_command": f"APPROVE_PRODUCTION {item.id}",
    }


def propose_concept(session: Session, item_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    item = session.get(ProductionQueueItem, item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Production queue item not found")
    concept = payload.get("concept") if payload else None
    final_concept = concept or concept_payload(session, item)
    if item.status not in {"concept_approved", "producing", "package_ready", "in_review"}:
        item.status = "concept_proposed"
    item.error = None
    item.updated_at = now_utc()
    session.add(item)
    session.add(ActivityEvent(entity_type="production_queue", entity_id=item.id, event_type="production_concept_proposed", label="Production concept proposed", payload_json=final_concept))
    session.commit(); session.refresh(item)
    return {"item": serialize_item(session, item), "concept": final_concept}


def approve_generation(session: Session, item_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    item = session.get(ProductionQueueItem, item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Production queue item not found")
    item.status = "concept_approved"
    item.updated_at = now_utc()
    session.add(item)
    session.add(ActivityEvent(entity_type="production_queue", entity_id=item.id, event_type="production_concept_approved", label="Production concept approved", payload_json=payload or {}))
    session.commit(); session.refresh(item)
    return serialize_item(session, item)


def reject_concept(session: Session, item_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    item = session.get(ProductionQueueItem, item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Production queue item not found")
    item.status = "needs_changes"
    item.error = (payload or {}).get("reason") or "Concept rejected by creator"
    item.updated_at = now_utc()
    session.add(item)
    session.add(ActivityEvent(entity_type="production_queue", entity_id=item.id, event_type="production_concept_rejected", label="Production concept rejected", payload_json=payload or {}))
    session.commit(); session.refresh(item)
    return serialize_item(session, item)


def default_quality_profile() -> dict[str, Any]:
    return {
        "version": "autoshorts-quality-v1.1",
        "name": "AutoShorts Director Quality v1.1",
        "visual_policy": "Premium text-free AI styleframes are mandatory for real review videos. Deterministic/Pillow/layout mockups are blueprints only, never final review visuals. No PowerPoint/template slides, no large top titles, no persistent top banners, no unnecessary lower-third dimming, no large text boards, no AI-generated text, no real brands, real URLs, phone numbers, card or bank data. Styleframes remain text-free; renderer-owned captions/labels must be listed in quality_report.renderer_owned_text.",
        "voice_policy": "Use Gianna premium clone for real renders. Generate 2-3 takes, normalize audio, transcribe with Whisper/forced alignment, compare against script, require WER <= 0.03, no wrong critical keywords, no hook/safer-move omissions, no clipped syllables/clicks/peaks/glitches.",
        "hook_policy": "First 0.5-1.0s must show a concrete red-flag visual and direct hook. Pattern interrupt in first 1-2s, visual change every 2-3s, clear safer move at the end. hook_score >= 8 required.",
        "metadata_policy": "TikTok caption, hashtags, YouTube private title/description/tags, synthetic-media disclosure and safety wording must be present before review.",
        "safety_policy": "No real scam URLs, phone numbers, bank/card/IBAN data, private data, operational scam instructions, unreviewed logos, public uploads, TikTok automation, or website push from production.",
        "render_policy": "Director visual plan -> 3-5 premium text-free styleframes -> styleframe QA -> review candidate render. Captions/labels are renderer-owned. Wan/I2V final renders require separate explicit approval.",
        "gates_required": ["visual_gate", "voice_gate", "hook_gate", "metadata_gate", "safety_gate", "package_integrity_gate"],
        "visual_checks_required": ["premium_ai_styleframes_used", "styleframes_text_free", "no_large_top_title", "no_unnecessary_lower_third_dim", "no_powerpoint_layout", "renderer_owned_text_listed"],
        "visual_fail_if_false": ["no_large_top_title", "no_unnecessary_lower_third_dim", "no_powerpoint_layout"],
    }


def get_quality_profile(session: Session) -> dict[str, Any]:
    setting = session.get(AppSetting, "production_quality_profile")
    if not setting:
        setting = AppSetting(key="production_quality_profile", value_json=default_quality_profile())
        session.add(setting); session.commit(); session.refresh(setting)
    profile = setting.value_json or {}
    merged = default_quality_profile()
    merged.update(profile)
    return merged


def director_skill_payload() -> dict[str, Any]:
    return {"name": "AutoShorts Director Quality v1", "doc": "docs/director-production-skill.md", "required": True}


def next_for_agent(session: Session) -> dict[str, Any]:
    item = session.exec(select(ProductionQueueItem).where(ProductionQueueItem.status == "locked_next").order_by(ProductionQueueItem.position, ProductionQueueItem.priority.desc())).first()
    if not item:
        item = session.exec(select(ProductionQueueItem).where(ProductionQueueItem.status.in_(["concept_proposed", "concept_approved", "producing"])).order_by(ProductionQueueItem.position, ProductionQueueItem.priority.desc())).first()
    if not item:
        item = session.exec(select(ProductionQueueItem).where(ProductionQueueItem.status == "approved_next").order_by(ProductionQueueItem.position, ProductionQueueItem.priority.desc())).first()
    if not item:
        return {"queue_item_id": None, "message": "No locked/approved production item"}
    family = session.get(Theme, item.family_id) if item.family_id else None
    idea = session.get(Idea, item.idea_id) if item.idea_id else None
    script = session.get(ContentScript, item.content_script_id) if item.content_script_id else None
    return {
        "queue_item_id": item.id,
        "family": family.title if family else None,
        "family_id": item.family_id,
        "idea": idea.title if idea else None,
        "idea_id": item.idea_id,
        "content_script_id": item.content_script_id,
        "title": item.title,
        "brief": item.brief,
        "hook": serialize_item(session, item).get("hook"),
        "script_constraints": item.script_constraints or (script.script_text if script else None),
        "visual_constraints": item.visual_constraints,
        "voice_constraints": item.voice_constraints,
        "recommended_reason": [line for line in (item.reason or "").split("\n") if line],
        "duplicate_report": duplicate_report(session, item.title),
        "rotation_hint": _rotation_hint_for_item(session, item),
        "expected_package_id": expected_package_id(item),
        "expected_package_manifest": expected_manifest(item),
        "requires_concept_confirmation": True,
        "next_required_action": "propose_concept_then_wait_for_APPROVE_PRODUCTION",
        "concept_endpoint": f"/api/agent/production/{item.id}/concept-proposed",
        "approve_endpoint": f"/api/agent/production/{item.id}/approve-generation",
        "reject_endpoint": f"/api/agent/production/{item.id}/reject-concept",
        "confirm_command": f"APPROVE_PRODUCTION {item.id}",
        "production_rules": {
            "no_final_wan_render_without_approval": True,
            "test_preview_allowed": False,
            "renderer_owned_text": True,
            "no_unreviewed_upload": True,
            "concept_confirmation_required": True,
            "quality_report_required": True,
            "ready_for_review_requires_quality_passed": True,
        },
        "quality_profile": get_quality_profile(session),
        "director_skill": director_skill_payload(),
    }


def mark_started(session: Session, item_id: str) -> dict[str, Any]:
    item = session.get(ProductionQueueItem, item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Production queue item not found")
    if item.status not in {"concept_approved", "in_production", "producing"}:
        raise HTTPException(status_code=409, detail="Concept must be approved before production starts")
    session.add(ActivityEvent(entity_type="production_queue", entity_id=item_id, event_type="production_started", label="Production started", payload_json={}))
    session.commit()
    return update_queue_item(session, item_id, {"status": "producing"})


def attach_package(session: Session, item_id: str, payload: dict[str, Any]) -> dict[str, Any]:
    item = session.get(ProductionQueueItem, item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Production queue item not found")
    manifest_path = payload.get("manifest_path") or payload.get("package_path")
    package_path = payload.get("package_path") or (str(Path(manifest_path).parent) if manifest_path else None)
    updates = {
        "status": "package_ready",
        "attached_package_path": package_path,
        "expected_package_manifest": manifest_path or item.expected_package_manifest,
        "actual_package_id": payload.get("package_id") or item.actual_package_id,
        "video_asset_id": payload.get("video_asset_id") or item.video_asset_id,
    }
    session.add(ActivityEvent(entity_type="production_queue", entity_id=item_id, event_type="production_package_attached", label="Production package attached", payload_json={"package_path": package_path, "manifest_path": manifest_path, "video_asset_id": updates["video_asset_id"]}))
    session.commit()
    return update_queue_item(session, item_id, updates)


def mark_ready_for_review(session: Session, item_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
    payload = payload or {}
    video_id = payload.get("video_asset_id")
    if video_id:
        video = session.get(VideoAsset, video_id)
        if video and str(video.status) not in {"ready_for_review", "VideoStatus.ready_for_review"} and getattr(video.status, "value", video.status) != "ready_for_review":
            raise HTTPException(status_code=409, detail="Video must pass quality gates before ready_for_review")
    session.add(ActivityEvent(entity_type="production_queue", entity_id=item_id, event_type="production_ready_for_review", label="Production package ready for review", payload_json=payload))
    session.commit()
    return update_queue_item(session, item_id, {"status": "in_review", **({"video_asset_id": payload.get("video_asset_id")} if payload.get("video_asset_id") else {})})
