#!/usr/bin/env python3
"""Reproduce SoftwareInspect's invented sales handoff SLA examples.

Run with Python 3.9+ and an IANA timezone database:
  python3 sales-handoff-sla.py > sales-handoff-sla.json

This is an educational calculation, not a CRM integration or production SLA
engine. Weekdays 09:00-17:00 Europe/Oslo; no holidays or calendar exceptions.
Inputs deliberately exercise the October 25, 2026 daylight-saving transition.
All records are invented. Clock origin remains handoff_ready_at on reassignment.
"""

import json
from datetime import datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo

ZONE = ZoneInfo("Europe/Oslo")
UTC = timezone.utc
DECISION_TARGET = 30
OUTREACH_TARGET = 60


def stamp(value):
    return datetime.fromisoformat(value).replace(tzinfo=ZONE)


def business_minutes(start, end):
    """Count overlap with each local weekday window, comparing instants in UTC."""
    assert end.astimezone(UTC) >= start.astimezone(UTC)
    day = start.astimezone(ZONE).date()
    total = 0
    while day <= end.astimezone(ZONE).date():
        if day.weekday() < 5:
            opening = datetime.combine(day, time(9), ZONE).astimezone(UTC)
            closing = datetime.combine(day, time(17), ZONE).astimezone(UTC)
            lower = max(start.astimezone(UTC), opening)
            upper = min(end.astimezone(UTC), closing)
            total += max(0, (upper - lower).total_seconds() / 60)
        day += timedelta(days=1)
    assert total == int(total)
    return int(total)


def build_inputs():
    standard = {
        "handoff_ready_at": "2026-10-19T09:00",
        "accepted_at": "2026-10-19T09:10",
        "first_sales_touch_at": "2026-10-19T09:35",
        "as_of": "2026-10-19T10:30",
    }
    changes = [
        ("H01", "Ordinary accepted handoff", {}),
        ("H02", "Friday handoff, Monday action across DST change", {
            "handoff_ready_at": "2026-10-23T16:50",
            "accepted_at": "2026-10-26T09:10",
            "first_sales_touch_at": "2026-10-26T09:35",
            "as_of": "2026-10-26T10:30",
        }),
        ("H03", "After-hours handoff", {
            "handoff_ready_at": "2026-10-19T17:20",
            "accepted_at": "2026-10-20T09:15",
            "first_sales_touch_at": "2026-10-20T09:50",
            "as_of": "2026-10-20T10:30",
        }),
        ("H04", "Accepted but no sales touch", {
            "accepted_at": "2026-10-19T09:05", "first_sales_touch_at": None,
        }),
        ("H05", "Valid timing rejection", {
            "accepted_at": None, "first_sales_touch_at": None,
            "rejected_at": "2026-10-19T09:20", "reason": "timing",
            "rejection_note": "Invented prospect asks to revisit in November",
            "review_on": "2026-11-02",
        }),
        ("H06", "Incomplete rejection remains pending", {
            "accepted_at": None, "first_sales_touch_at": None,
            "rejected_at": "2026-10-19T09:20", "reason": None,
            "rejection_note": None,
        }),
        ("H07", "Late acceptance and late sales touch", {
            "accepted_at": "2026-10-19T09:45",
            "first_sales_touch_at": "2026-10-19T10:10",
        }),
        ("H08", "Owner change does not restart either clock", {
            "owner_changed_at": "2026-10-19T09:25",
            "accepted_at": "2026-10-19T09:40",
            "first_sales_touch_at": "2026-10-19T09:55",
        }),
        ("H09", "Automatic receipt does not count as a sales touch", {
            "automatic_receipt_at": "2026-10-19T09:01",
            "first_sales_touch_at": None,
        }),
        ("H10", "Sales touch exactly at the target", {
            "first_sales_touch_at": "2026-10-19T10:00",
        }),
    ]
    return [dict(standard, id=key, case=case, **change)
            for key, case, change in changes]


def evaluate(row):
    ready, cutoff = stamp(row["handoff_ready_at"]), stamp(row["as_of"])
    accepted = row.get("accepted_at")
    rejected = (row.get("rejected_at") and row.get("reason")
                and row.get("rejection_note")
                and (row["reason"] != "timing" or row.get("review_on")))
    assert not (accepted and rejected)
    decision_at = accepted or (row["rejected_at"] if rejected else None)
    decision_age = business_minutes(ready, stamp(decision_at) if decision_at else cutoff)
    decision_status = ("met" if decision_age <= DECISION_TARGET else "late") if decision_at else (
        "pending_overdue" if decision_age > DECISION_TARGET else "pending")
    touch_at = row.get("first_sales_touch_at") if accepted else None
    touch_age = business_minutes(ready, stamp(touch_at) if touch_at else cutoff) if accepted else None
    touch_status = ("met" if touch_age <= OUTREACH_TARGET else "late") if touch_at else (
        "pending_overdue" if touch_age > OUTREACH_TARGET else "pending") if accepted else "not_applicable"
    elapsed = int((stamp(touch_at).astimezone(UTC) - ready.astimezone(UTC)).total_seconds() / 60) if touch_at else None
    return {
        "id": row["id"], "disposition": "accepted" if accepted else "rejected" if rejected else "pending",
        "decision_minutes": decision_age, "decision_status": decision_status,
        "sales_touch_minutes": touch_age, "sales_touch_status": touch_status,
        "elapsed_minutes_to_sales_touch": elapsed,
    }


def run():
    inputs = build_inputs()
    results = [evaluate(row) for row in inputs]
    expected = [
        (10, "met", 35, "met"), (20, "met", 45, "met"),
        (15, "met", 50, "met"), (5, "met", 90, "pending_overdue"),
        (20, "met", None, "not_applicable"),
        (90, "pending_overdue", None, "not_applicable"),
        (45, "late", 70, "late"), (40, "late", 55, "met"),
        (10, "met", 90, "pending_overdue"), (10, "met", 60, "met"),
    ]
    for row, wanted in zip(results, expected):
        assert tuple(row[k] for k in ("decision_minutes", "decision_status", "sales_touch_minutes", "sales_touch_status")) == wanted
    assert results[1]["elapsed_minutes_to_sales_touch"] == 3945
    summary = {
        "handoffs": len(results),
        "accepted": sum(r["disposition"] == "accepted" for r in results),
        "rejected": sum(r["disposition"] == "rejected" for r in results),
        "pending_decision": sum(r["disposition"] == "pending" for r in results),
        "decision_met": sum(r["decision_status"] == "met" for r in results),
        "decision_late": sum(r["decision_status"] == "late" for r in results),
        "decision_pending_overdue": sum(r["decision_status"] == "pending_overdue" for r in results),
        "sales_touch_met": sum(r["sales_touch_status"] == "met" for r in results),
        "sales_touch_late": sum(r["sales_touch_status"] == "late" for r in results),
        "sales_touch_pending_overdue": sum(r["sales_touch_status"] == "pending_overdue" for r in results),
    }
    assert summary == dict(handoffs=10, accepted=8, rejected=1, pending_decision=1,
                           decision_met=7, decision_late=2, decision_pending_overdue=1,
                           sales_touch_met=5, sales_touch_late=1, sales_touch_pending_overdue=2)
    return {
        "method": {"data": "invented educational handoffs, not customer observations",
                   "executed_on": "2026-10-17", "timezone": "Europe/Oslo",
                   "coverage": "Monday-Friday 09:00-17:00; no holidays",
                   "decision_target_minutes": DECISION_TARGET,
                   "sales_touch_target_minutes": OUTREACH_TARGET,
                   "clock_origin": "handoff_ready_at; owner changes do not reset",
                   "target_boundary": "at or before target counts as met",
                   "limitations": "No live CRM, holiday calendar, outcome benchmark, event ingestion, persistence or concurrency test"},
        "inputs": inputs, "results": results, "summary": summary,
    }


if __name__ == "__main__":
    print(json.dumps(run(), indent=2))
