"""
Stage 3 — Phone Screen Scheduler Agent
========================================
WHAT THIS DOES
  For each candidate a recruiter advanced to the phone screen, this agent:
    1. finds time slots that work for BOTH the candidate and every required interviewer
       (across time zones, around their busy calendars, with buffers and notice rules),
    2. sends a self-serve booking link showing only those valid slots,
    3. on booking, writes the event to everyone's calendar and confirms it, and
    4. sends reminders to the candidate and the interviewer(s).
  It handles logistics ONLY. It never screens, judges, or decides.

WHAT KEEPS IT SAFE (same posture as the Stage 2 agent)
  - DRY_RUN = True by default: prints what it WOULD do, sends/books nothing.
  - Texts only go to candidates who consented (consent_to_contact = TRUE).
  - Never offers a slot outside an interviewer's working hours or over a busy block.
  - If no time works, it escalates to the recruiter instead of forcing it.
  - Every action is written to an audit log.

You only edit the CONFIG section. The engine below it can be left alone.
"""

import csv, json, sys, secrets
from datetime import datetime, timedelta, timezone, time
try:
    from zoneinfo import ZoneInfo
except ImportError:
    sys.exit("This script needs Python 3.9+ (zoneinfo).")

# =====================================================================
#  CONFIG  —  EDIT THIS SECTION
# =====================================================================
DRY_RUN = True                 # True = print only (safe). False = really send & book.
SIMULATE_BOOKING = True        # Demo only: auto-pick the first slot to show the booking flow.
                               # In real life the candidate books from the link themselves.

CANDIDATES_CSV  = "stage3_candidates_sample.csv"
INTERVIEWERS_JSON = "stage3_interviewers_sample.json"
AUDIT_LOG = "stage3_scheduler_audit_log.csv"

DURATION_MIN        = 30       # length of the phone screen
BOOKING_WINDOW_DAYS = 7        # how far ahead to offer slots
MIN_NOTICE_HOURS    = 24       # earliest a slot can be from now
BUFFER_MIN          = 10       # padding before/after each booking
SLOT_GRANULARITY_MIN= 30       # offer slots on :00 / :30, etc.
MAX_SLOTS_TO_OFFER  = 6        # how many options to show on the link

COMPANY_NAME    = "Acme Talent"
FROM_NAME       = "Arun from Acme Recruiting"
SCHEDULING_BASE = "https://book.acme.example/s"   # self-serve booking page base URL
VIDEO_PLATFORM  = "Zoom"

# =====================================================================
#  PROVIDER HOOKS  —  plug your real services in here (examples commented)
# =====================================================================
def get_busy(iv_key, iv, now):
    """Return list of (start_utc, end_utc) busy blocks for an interviewer.
    DRY_RUN builds them from the sample JSON. Live: call your calendar's free/busy API."""
    if DRY_RUN:
        tz = ZoneInfo(iv["tz"])
        today_local = now.astimezone(tz).date()
        blocks = []
        for b in iv.get("busy", []):
            d = today_local + timedelta(days=b["day_offset"])
            sh, sm = map(int, b["start"].split(":"))
            eh, em = map(int, b["end"].split(":"))
            bs = datetime.combine(d, time(sh, sm), tzinfo=tz).astimezone(timezone.utc)
            be = datetime.combine(d, time(eh, em), tzinfo=tz).astimezone(timezone.utc)
            blocks.append((bs, be))
        return blocks
    # --- Example (Google Calendar freebusy) ---
    # service.freebusy().query(body={"timeMin":..., "timeMax":..., "items":[{"id": iv["email"]}]})...
    raise NotImplementedError("Wire your calendar free/busy API into get_busy().")

def create_calendar_event(attendee_email, start_utc, end_utc, summary, join_link):
    if DRY_RUN:
        print(f"      [DRY-RUN CALENDAR] hold for {attendee_email} @ {start_utc:%a %b %d %H:%M}Z  ({summary})")
        return True
    # --- Example: service.events().insert(calendarId=attendee_email, body={...}) ---
    raise NotImplementedError("Wire your calendar write API into create_calendar_event().")

def send_email(to_email, subject, body):
    if DRY_RUN:
        print(f"      [DRY-RUN EMAIL] to {to_email}  |  {subject}")
        return True
    raise NotImplementedError("Add your email provider in send_email().")

def send_sms(to_phone, body):
    if DRY_RUN:
        print(f"      [DRY-RUN TEXT]  to {to_phone}")
        return True
    raise NotImplementedError("Add your SMS provider in send_sms().")

def generate_video_link():
    return f"https://{VIDEO_PLATFORM.lower()}.example/j/{secrets.token_hex(4)}"

def generate_booking_link(candidate, slots_utc):
    token = secrets.token_urlsafe(6)
    return f"{SCHEDULING_BASE}/{token}"

# =====================================================================
#  ENGINE  —  you can leave this alone
# =====================================================================
def is_true(v): return str(v).strip().lower() in ("true", "yes", "y", "1")

def overlaps(a_s, a_e, b_s, b_e): return a_s < b_e and b_s < a_e

def fmt_local(dt_utc, tzname):
    return dt_utc.astimezone(ZoneInfo(tzname)).strftime("%a %b %d, %I:%M %p %Z")

def find_mutual_slots(candidate, interviewers, now):
    """The matching brain: return slots (UTC) valid for the candidate AND all interviewers."""
    duration = timedelta(minutes=DURATION_MIN)
    buffer = timedelta(minutes=BUFFER_MIN)
    earliest = now + timedelta(hours=MIN_NOTICE_HOURS)
    window_end = now + timedelta(days=BOOKING_WINDOW_DAYS)

    busy = {k: get_busy(k, iv, now) for k, iv in interviewers.items()}

    # build a UTC slot grid on the granularity boundary
    step = timedelta(minutes=SLOT_GRANULARITY_MIN)
    grid = now.replace(minute=(now.minute // SLOT_GRANULARITY_MIN) * SLOT_GRANULARITY_MIN,
                       second=0, microsecond=0) + step
    slots = []
    cur = grid
    while cur <= window_end:
        start, end = cur, cur + duration
        if start >= earliest:
            ok = True
            for k, iv in interviewers.items():
                tz = ZoneInfo(iv["tz"])
                ls, le = start.astimezone(tz), end.astimezone(tz)
                ws = time(*map(int, iv["work_start"].split(":")))
                we = time(*map(int, iv["work_end"].split(":")))
                if ls.weekday() >= 5:                      ok = False; break   # weekend
                if ls.date() != le.date():                 ok = False; break   # crosses midnight
                if not (ls.time() >= ws and le.time() <= we): ok = False; break # working hours
                if any(overlaps(start - buffer, end + buffer, bs, be) for bs, be in busy[k]):
                    ok = False; break                                          # busy
            if ok:
                slots.append(start)
        cur += step
    return slots

def reminder_plan(slot_utc, candidate, interviewers):
    plan = []
    plan.append(("candidate", "on booking", "now", "Confirmation + join link"))
    plan.append(("candidate", "24h before", fmt_local(slot_utc - timedelta(hours=24), candidate["timezone"]), "Reminder + reschedule link"))
    plan.append(("candidate", "1h before", fmt_local(slot_utc - timedelta(hours=1), candidate["timezone"]), "Starting soon + join link"))
    for iv in interviewers.values():
        plan.append((iv["name"], "morning of", fmt_local(slot_utc.replace(hour=15, minute=0), iv["tz"]).split(",")[0] + " AM digest", "Daily phone-screen digest"))
        plan.append((iv["name"], "15m before", fmt_local(slot_utc - timedelta(minutes=15), iv["tz"]), "Reminder + candidate one-pager"))
    return plan

def process(cand, reqs, all_ivs, now, audit):
    name = f'{cand["first_name"]} {cand["last_name"]}'.strip()
    print(f"\n• {name}  [{cand['candidate_id']}]  →  {cand['job_title']}  ({cand['job_req_id']})")

    req = cand["job_req_id"]
    iv_keys = reqs.get(req, [])
    if not iv_keys:
        print("   SKIPPED — no interviewers configured for this req.")
        audit.append([cand["candidate_id"], "skipped", "no interviewers for req"]); return
    interviewers = {k: all_ivs[k] for k in iv_keys}
    print(f"   Interviewers: {', '.join(all_ivs[k]['name'] for k in iv_keys)}")

    slots = find_mutual_slots(cand, interviewers, now)

    if not slots:
        print(f"   NO MUTUAL AVAILABILITY in the next {BOOKING_WINDOW_DAYS} days.")
        print(f"   ESCALATING to recruiter ({cand['recruiter_owner']}) to add availability.")
        audit.append([cand["candidate_id"], "escalated", "no mutual availability"]); return

    shown = slots[:MAX_SLOTS_TO_OFFER]
    link = generate_booking_link(cand, shown)
    print(f"   Found {len(slots)} mutual slot(s). Offering first {len(shown)} via booking link:")
    for s in shown:
        print(f"      - {fmt_local(s, cand['timezone'])}   (candidate's time zone)")

    # Booking invite to the candidate
    send_email(cand["email"], f"Let's schedule your phone screen for {cand['job_title']}",
               f"Hi {cand['first_name']}, pick a time: {link}")
    if cand["phone"] and is_true(cand["consent_to_contact"]):
        send_sms(cand["phone"], f"Hi {cand['first_name']}, book your {COMPANY_NAME} phone screen: {link}")
    elif cand["phone"]:
        print("   TEXT skipped — candidate hasn't consented to texts. Email only.")
    audit.append([cand["candidate_id"], "invited", f"{len(shown)} slots offered"])

    if not SIMULATE_BOOKING:
        return

    # --- Demo: simulate the candidate picking the first slot, to show the post-booking flow ---
    chosen = shown[0]
    end = chosen + timedelta(minutes=DURATION_MIN)
    join = generate_video_link()
    print(f"   ↳ (demo) candidate books: {fmt_local(chosen, cand['timezone'])}")
    create_calendar_event(cand["email"], chosen, end, f"Phone screen — {cand['job_title']}", join)
    for k in iv_keys:
        create_calendar_event(all_ivs[k]["email"], chosen, end,
                              f"Phone screen — {name} ({cand['job_title']})", join)
    send_email(cand["email"], f"You're booked — {cand['job_title']} phone screen",
               f"Confirmed for {fmt_local(chosen, cand['timezone'])}. Join: {join}")
    print("   Reminder schedule:")
    for who, when, at, what in reminder_plan(chosen, cand, interviewers):
        print(f"      - {who:<12} {when:<11} {at:<28} {what}")
    audit.append([cand["candidate_id"], "booked", fmt_local(chosen, cand['timezone'])])

def main():
    now = datetime.now(timezone.utc)
    try:
        with open(CANDIDATES_CSV, newline="", encoding="utf-8") as f:
            cands = list(csv.DictReader(f))
        with open(INTERVIEWERS_JSON, encoding="utf-8") as f:
            data = json.load(f)
    except FileNotFoundError as e:
        sys.exit(f"Missing input file: {e.filename}")
    all_ivs, reqs = data["interviewers"], data["reqs"]

    mode = "DRY-RUN (nothing sent or booked)" if DRY_RUN else "LIVE"
    print(f"Stage 3 Phone Screen Scheduler — {mode}")
    print(f"Now: {now:%Y-%m-%d %H:%M}Z  |  window: {BOOKING_WINDOW_DAYS} days  |  duration: {DURATION_MIN} min")

    audit = []
    for c in cands:
        if c.get("pipeline_stage", "").strip().lower() != "phone screen" or c.get("status", "").strip().lower() != "active":
            print(f"\n• {c.get('first_name','?')} — skipped (not an active phone-screen candidate)")
            continue
        process(c, reqs, all_ivs, now, audit)

    stamp = now.isoformat(timespec="seconds")
    new = True
    try: open(AUDIT_LOG, "x").close()
    except FileExistsError: new = False
    with open(AUDIT_LOG, "a", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        if new: w.writerow(["run_at_utc", "candidate_id", "action", "detail"])
        for r in audit: w.writerow([stamp] + r)

    booked = sum(1 for r in audit if r[1] == "booked")
    esc = sum(1 for r in audit if r[1] == "escalated")
    print(f"\nDone. Invited/booked {sum(1 for r in audit if r[1] in ('invited','booked'))}, escalated {esc}. Log: {AUDIT_LOG}")

if __name__ == "__main__":
    main()
