# Nightly batch: upsert today's contacts, build one campaign and start it. Python 3.9+ with requests.
#   ROWS_FILE=rows.json AGENT_ID=agent_... python nightly-batch.py
# Every request runs, so the job looks for tonight's campaign by name first: re-running it the same
# night continues where it stopped instead of creating a second campaign.
import json
import os
import sys
import time
from datetime import datetime, timezone

import requests

base = os.environ.get("RELATIONSAI_API_BASE", "https://voice-api.relationsai.com/v1")
# Defaults to the test-mode Sample agent; set AGENT_ID to use your own agent.
agent_id = os.environ.get("AGENT_ID", "agent_1ZltiWzxrXmMprGD6q63")
batch = os.environ.get("BATCH_ID") or datetime.now(timezone.utc).strftime("%Y-%m-%d")
name = f"Nightly renewals {batch}"
# Rows from your warehouse query, with consent exactly as your records hold it (true, false or absent).
# Without ROWS_FILE the job uses three test-mode numbers, so it needs a test key.
if not os.environ.get("ROWS_FILE") and not os.environ.get("RELATIONSAI_API_KEY", "").startswith("rai_test_"):
    sys.exit("Set ROWS_FILE, or use a test key (rai_test_...) to run with the built-in test-mode rows.")
if os.environ.get("ROWS_FILE"):
    with open(os.environ["ROWS_FILE"]) as f:
        rows = json.load(f)
else:
    rows = [
        {"phone": "+12025550100", "name": "Asha Rao"},
        {"phone": "+12025550101", "name": "Ravi Iyer"},
        {"phone": "+12025550102", "name": "Meera Shah"},
    ]

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['RELATIONSAI_API_KEY']}"
next_slot = 0.0


def api(method, path, body=None):
    # Stay well inside the write limit (120 per minute per key): at most 1 write every 0.6 s.
    global next_slot
    for attempt in range(1, 6):
        time.sleep(max(0.0, next_slot - time.monotonic()))
        next_slot = time.monotonic() + 0.6
        r = session.request(method, f"{base}{path}", json=body, timeout=30)
        data = r.json()
        if r.ok:
            return data
        # Only 429 and 503 say nothing was done, so only they are sent again here. Anything else stops the
        # job; re-running it the same night looks up what already exists and continues.
        retry_after = float(r.headers.get("Retry-After", 2**attempt))
        if r.status_code not in (429, 503) or attempt == 5 or retry_after > 120:
            err = data["error"]
            sys.exit(f"{path}: {err['code']}: {err['message']} (request {err['request_id']})")
        time.sleep(retry_after)


# An upsert is safe to repeat: it updates the same contact.
contact_ids = []
for row in rows:
    body = {"phone_number": row["phone"], "name": row["name"]}
    if isinstance(row.get("consent"), bool):
        body["consent_granted"] = row["consent"]
    contact = api("POST", "/contacts/upsert", body)
    if not contact["do_not_call"]:
        contact_ids.append(contact["id"])  # the start would leave them out anyway

# Tonight's campaign may exist already (an earlier run that stopped halfway).
campaign = next((c for c in api("GET", "/campaigns?limit=100")["data"] if c["name"] == name), None)
if campaign and campaign["status"] != "draft":
    print(f"{campaign['id']} is already {campaign['status']}: nothing to do")
    sys.exit(0)
if campaign is None:
    # A create takes up to 1000 contact ids; the rest are added below.
    campaign = api("POST", "/campaigns", {"name": name, "agent_id": agent_id, "concurrency_limit": 5, "contact_ids": contact_ids[:1000]})
# Add every chunk while it is a draft: contacts already in the campaign are skipped, so repeating is safe.
for i in range(0, len(contact_ids), 1000):
    api("POST", f"/campaigns/{campaign['id']}/recipients", {"contact_ids": contact_ids[i : i + 1000]})

started = api("POST", f"/campaigns/{campaign['id']}/start")
print(f"{started['id']} {started['status']}: {started['recipient_count']} of {len(rows)} rows will be called")
