# Docs: /guides/send-newsletters-python
import argparse
import json
import os
import re
from collections.abc import Mapping
from datetime import datetime, timedelta, timezone
from mailkite import MailKite


class MockMailKite(MailKite):
    """Local request seam; no sockets, account creation, or delivery."""
    def __init__(self):
        super().__init__(accessToken="mock-session")
        self.contacts, self.members, self.calls = {}, set(), []
        self.suppressed = {"blocked@example.com"}

    def request(self, method: str, path: str, body: dict[str, object] | None = None) -> dict[str, object] | list[dict[str, object]]:
        """Emulate only the server routes used by this demo, without network I/O."""
        self.calls.append((method, path, body))
        if path == "/api/contacts/suppressions" and method == "POST":
            email = body["email"].lower()
            self.suppressed.add(email)
            if email in self.contacts:
                self.contacts[email].update(status="unsubscribed", subscribed=0)
            return {"suppressed": True, "email": email}
        if path == "/api/contacts/suppressions":
            return {"suppressions": [{"email": e} for e in self.suppressed]}
        if method == "POST" and path == "/api/contacts":
            self.contacts.setdefault(body["email"], dict(body, id=f"ctr_{len(self.contacts)}", status="active", subscribed=0))
            return dict(self.contacts[body["email"]])
        if method == "PATCH" and path.startswith("/api/contacts/"):
            contact = next(c for c in self.contacts.values() if path.endswith(c["id"]))
            contact["subscribed"] = int(body["subscribed"])
            return dict(contact)
        if path == "/api/lists":
            return {"id": "lst_mock", "name": body["name"]}
        if path == "/api/lists/lst_mock/contacts" and method == "POST":
            self.members.update(body["contactIds"])
            return {"added": len(body["contactIds"])}
        if path == "/api/lists/lst_mock/contacts":
            return [c for c in self.contacts.values() if c["id"] in self.members]
        if path == "/api/broadcasts":
            self.broadcast = dict(body, id="bct_mock", status="draft", scheduled_at=None, sent_count=0, fail_count=0)
            return dict(self.broadcast)
        if path == "/api/broadcasts/bct_mock/send":
            assert self.broadcast["status"] == "draft"
            count = sum(c["id"] in self.members and c["subscribed"] and c["status"] == "active" and c["email"] not in self.suppressed for c in self.contacts.values())
            if not count:
                raise ValueError("precheck_failed: no subscribed contacts in the audience")
            when = body.get("scheduledAt")
            self.broadcast["status"] = "scheduled" if when else "sending"
            self.broadcast["scheduled_at"] = int(datetime.fromisoformat(when).timestamp() * 1000) if when else None
            return {"id": "bct_mock", "status": self.broadcast["status"], "audienceCount": count}
        if path == "/api/broadcasts/bct_mock/cancel":
            assert self.broadcast["status"] == "scheduled"
            self.broadcast.update(status="draft", scheduled_at=None)
            return {"ok": True, "status": "draft"}
        if path == "/api/broadcasts/bct_mock":
            return dict(self.broadcast)
        raise ValueError(f"Unexpected mock call: {method} {path}")


SUBSCRIBERS = [
    {"email": "ada@example.com", "name": "Ada", "consent": True},
    {"email": "ADA@example.com", "name": "Ada", "consent": True},
    {"email": "opted-out@example.com", "name": "Lin", "consent": False},
    {"email": "blocked@example.com", "name": "Bo", "consent": True},
]


def campaign(mk: MailKite, rows: list[dict[str, str | bool]], config: Mapping[str, str], action: str = "draft") -> dict[str, object]:
    """Import eligible test subscribers and create, optionally schedule, a broadcast."""
    if action not in {"draft", "schedule-cancel", "send"}:
        raise ValueError("Unknown action")
    revoked = {r["email"].strip().lower() for r in rows if not r["consent"]}
    excluded = revoked | {s["email"].lower() for s in mk.listSuppressions()["suppressions"]}
    unique = {r["email"].strip().lower(): r for r in rows}
    audience_list = mk.createList({"name": "Guide controlled test audience"})
    ids = []
    for email, row in unique.items():
        if not row["consent"] or email in excluded:
            continue
        # No named contact CRUD method in shared spec; this is the SDK escape hatch.
        contact = mk.request("POST", "/api/contacts", {"email": email, "name": row.get("name")})
        if contact["status"] != "active":
            continue
        mk.request("PATCH", f'/api/contacts/{contact["id"]}', {"subscribed": True})
        ids.append(contact["id"])
    if not ids:
        raise ValueError("No eligible test subscribers; empty list remains for inspection")
    mk.addListContacts(audience_list["id"], {"contactIds": ids})
    audience = mk.listListContacts(audience_list["id"])
    draft = mk.createBroadcast({
        "name": "Guide newsletter preview", "from": config["from"], "replyTo": config["replyTo"],
        "subject": "Product notes for {{first_name}}",
        "text": "Hello {{first_name}},\nHere are this week's product notes. Reply with questions.",
        "html": "<p>Hello {{first_name}},</p><p>Here are this week's product notes. Reply with questions.</p>",
        "audience": {"type": "list", "id": audience_list["id"]}, "footerAddress": config["footerAddress"],
    })
    accepted = None
    if action == "schedule-cancel":
        when = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat()
        accepted = mk.sendBroadcast(draft["id"], {"scheduledAt": when})
        assert mk.getBroadcast(draft["id"])["status"] == "scheduled"
        mk.request("POST", f'/api/broadcasts/{draft["id"]}/cancel', {})
    elif action == "send":
        accepted = mk.sendBroadcast(draft["id"], {})
    return {"listId": audience_list["id"], "audience": [c["email"] for c in audience], "accepted": accepted, "broadcast": mk.getBroadcast(draft["id"])}


def live_config(env: Mapping[str, str]) -> tuple[list[dict[str, str | bool]], dict[str, str]]:
    """Validate explicit live configuration and constrain it to one controlled inbox."""
    for key in ("MAILKITE_MANAGEMENT_TOKEN", "MAILKITE_TEST_DOMAIN", "MAILKITE_TEST_RECIPIENT", "MAILKITE_FROM", "MAILKITE_REPLY_TO", "MAILKITE_POSTAL_ADDRESS"):
        if not env.get(key):
            raise ValueError(f"Missing {key}")
    if env["MAILKITE_MANAGEMENT_TOKEN"].startswith("mk_live_"):
        raise ValueError("Use a separate session/OAuth management token")
    domain = env["MAILKITE_TEST_DOMAIN"].lower()
    email = env["MAILKITE_TEST_RECIPIENT"].lower()
    if not re.fullmatch(r"test-[a-z0-9-]+@" + re.escape(domain), email) or re.search(r"(^|\.)example\.(com|net|org)$", domain):
        raise ValueError("Use one test-* inbox on your controlled domain, not example.com")
    return [{"email": email, "name": "Tester", "consent": True}], {"from": env["MAILKITE_FROM"], "replyTo": env["MAILKITE_REPLY_TO"], "footerAddress": env["MAILKITE_POSTAL_ADDRESS"]}


def management_token(env: Mapping[str, str]) -> str:
    """Use an existing token or log into a verified account without printing secrets."""
    if env.get("MAILKITE_MANAGEMENT_TOKEN"):
        return env["MAILKITE_MANAGEMENT_TOKEN"]
    if not env.get("MAILKITE_ACCOUNT_EMAIL") or not env.get("MAILKITE_ACCOUNT_PASSWORD"):
        raise ValueError("Provide management token or existing account email/password")
    login = MailKite("").request("POST", "/api/auth/login", {
        "email": env["MAILKITE_ACCOUNT_EMAIL"], "password": env["MAILKITE_ACCOUNT_PASSWORD"],
    })
    if not login.get("token") or not login.get("user", {}).get("emailVerified"):
        raise ValueError("Login requires a verified existing account")
    return login["token"]


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--live", action="store_true")
    actions = parser.add_mutually_exclusive_group()
    actions.add_argument("--send", action="store_true")
    actions.add_argument("--schedule-cancel", action="store_true")
    args = parser.parse_args()
    if args.send and not args.live:
        parser.error("--send requires --live")
    env = dict(os.environ, MAILKITE_MANAGEMENT_TOKEN=management_token(os.environ)) if args.live else os.environ
    rows, config = live_config(env) if args.live else (SUBSCRIBERS, {"from": "notes@example.com", "replyTo": "reply@example.com", "footerAddress": "MOCK postal address"})
    client = MailKite(accessToken=env["MAILKITE_MANAGEMENT_TOKEN"]) if args.live else MockMailKite()
    action = "send" if args.send else "schedule-cancel" if args.schedule_cancel else "draft"
    print(json.dumps(dict(mode="LIVE" if args.live else "MOCK, no emails sent", **campaign(client, rows, config, action)), indent=2))
