
    size = sample_size or c.preview_sample_size or 5
    size = max(1, min(size, 20))

    if not c.recipient_list_id:
        raise HTTPException(status_code=400, detail="Campaign has no recipient list")
    rl = await db.get(RecipientList, c.recipient_list_id)
    if not rl or rl.user_id != user.id:
        raise HTTPException(status_code=400, detail="Recipient list not found")

    recipient_rows = (await db.execute(
        select(Recipient).where(Recipient.list_id == rl.id, Recipient.is_valid == True)  # noqa: E712
        .order_by(Recipient.id).limit(size))).scalars().all()
    if not recipient_rows:
        raise HTTPException(status_code=400, detail="Recipient list has no valid recipients")

    # materialize plan rows WITHOUT persisting CampaignRecipient entities
    result = await materialize_campaign(db, c, recipient_rows, preview=True,
                                          preview_size=size)
    plan = result.get("plan", [])

    # render actual message content per sample
    engine = VariableEngine(c.variable_config or {})
    out = []
    for item in plan:
        rec = next((r for r in recipient_rows if r.id == item["recipient_id"]), None)
        if not rec:
            continue
        tpl = await db.get(EmailTemplate, item["template_id"]) if item.get("template_id") else None
        subj = await db.get(SubjectLine, item["subject_id"]) if item.get("subject_id") else None
        g = GeneratedValues()
        g.invoice_number = item.get("invoice_number", "")
        g.amount = item.get("amount")
        g.shipping = item.get("shipping")
        g.invoice_date = item.get("invoice_date")
        g.due_date = item.get("due_date")
        phone = item.get("phone", "")
        unsub = f"{cfg.UNSUBSCRIBE_BASE or cfg.BASE_URL}/unsubscribe/{rec.unsub_token}" if c.unsubscribe_enabled else ""
        extra = {"unsubscribe_link": unsub, "company_phone": phone}

        body_html = ""
        subject = ""
        if tpl:
            res = engine.substitute(tpl.body_html, rec, g,
                                    custom_fields=rec.custom_fields or {}, extra=extra)
            body_html = res.text
            subject = subj.subject if subj else (tpl.subject_hint or tpl.name)
        if subj:
            res_s = engine.substitute(subj.subject, rec, g,
                                      custom_fields=rec.custom_fields or {}, extra=extra)
            subject = res_s.text
        out.append({
            "recipient": {"id": rec.id, "email": rec.email,
                          "first_name": rec.first_name, "last_name": rec.last_name},
            "from_account": item.get("account_email", ""),
            "subject": subject,
            "body_html": sanitize_preview(body_html),
            "raw_variables": {"invoice_number": g.invoice_number,
                              "amount": g.amount, "shipping": g.shipping,
                              "phone": phone},
            "warnings": item.get("warnings", []),
        })
    return {"campaign_id": c.id, "sample_size": len(out), "messages": out,
            "plan_summary": result.get("summary", {})}


# ---------------------------------------------------------------------------
# Control: start / pause / resume / cancel
# ---------------------------------------------------------------------------

@router.post("/{cid}/start")
