#!/usr/bin/env python3
"""
VoicerOnePBX Programmable Voice — Example 2: Phone Survey
========================================================

A multi-step flow that keeps **state across callbacks**. The whole point of this example is the pattern for
carrying data from one step to the next: the platform is stateless between requests, so YOU hold the state,
keyed by the stable `callId` the PBX echoes on every callback.

Flow (a 3-question satisfaction survey ending in a recorded comment):

    POST /survey            (call.inbound.pre_route)   → Q1: rate 1–5
    POST /survey/q2         (gather.completed)         store Q1 → Q2: recommend us? 1=yes 2=no
    POST /survey/q3         (gather.completed)         store Q2 → "leave a comment after the tone"
    POST /survey/done       (record.completed)         store transcript → thank + hang up, print summary

`record` is the same request→response shape as `gather`: it POSTs record.completed to its actionUrl and you
return the next CCD.

Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 python3 app.py
Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/survey
"""

from __future__ import annotations

import hashlib
import hmac
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SECRET = os.environ.get("VOICER_SECRET", "")
BASE   = os.environ.get("VOICER_BASE_URL", "http://localhost:8080")
PORT   = int(os.environ.get("PORT", "8080"))

# Per-call state, keyed by callId. In production this is a row in your database, not a process-memory dict —
# but the KEY is the same: the callId the PBX gives you on every callback for this call.
RESPONSES: dict[str, dict] = {}


# ── The handlers — this is the app ───────────────────────────────────────────

def handle_start(event: dict) -> list:
    """Inbound call: open a fresh record for this callId and ask question 1."""
    RESPONSES[event["callId"]] = {"from": event.get("from"), "rating": None, "recommend": None, "comment": None}
    return [
        {"say": {"text": "Thanks for taking our two-question survey. "
                         "On a scale of 1 to 5, how satisfied were you with your call today? "
                         "Press a number from 1 to 5."}},
        {"gather": {"numDigits": 1, "timeout": 8, "actionUrl": f"{BASE}/survey/q2"}},
    ]


def handle_q2(event: dict) -> list:
    """Answer to Q1 arrived. Store it, then ask Q2."""
    rec = RESPONSES.get(event["callId"])
    if rec is None or event.get("reason") == "hangup":
        return []                                            # caller gone or unknown call — nothing to do
    if event.get("digits"):
        rec["rating"] = event["digits"]                       # ← state carried forward by callId
    return [
        {"say": {"text": "Thank you. Would you recommend us to a friend? Press 1 for yes, 2 for no."}},
        {"gather": {"numDigits": 1, "timeout": 8, "actionUrl": f"{BASE}/survey/q3"}},
    ]


def handle_q3(event: dict) -> list:
    """Answer to Q2 arrived. Store it, then invite a recorded comment."""
    rec = RESPONSES.get(event["callId"])
    if rec is None or event.get("reason") == "hangup":
        return []
    rec["recommend"] = {"1": "yes", "2": "no"}.get(event.get("digits", ""), "unknown")
    return [
        {"say": {"text": "Last thing: leave any comments after the tone, then press pound. "
                         "Or press pound now to finish."}},
        # `record` posts record.completed to actionUrl. transcribe=true gives an on-device transcript;
        # playBeep=true plays the tone the prompt promised.
        {"record": {"maxSeconds": 30, "finishOnKey": "#", "transcribe": True, "playBeep": True,
                    "actionUrl": f"{BASE}/survey/done"}},
    ]


def handle_done(event: dict) -> list:
    """The recording finished. Store the transcript, finalize, and thank the caller."""
    rec = RESPONSES.pop(event["callId"], None)
    if rec is not None:
        rec["comment"] = event.get("transcript", "")          # empty if the caller skipped or said nothing
        # "Persist" the completed survey. Replace this print with a DB insert.
        print("  ✅ SURVEY COMPLETE:", json.dumps(rec))
    # A document with no gather/record/redirect ends the call after it finishes speaking; hangup is explicit.
    return [{"say": {"text": "Thank you for your feedback. Goodbye."}}, {"hangup": {}}]


ROUTES = {
    "/survey":    handle_start,
    "/survey/q2": handle_q2,
    "/survey/q3": handle_q3,
    "/survey/done": handle_done,
}


# ── HTTP plumbing (generic; identical across the three examples) ──────────────

def verify(raw_body: bytes, headers) -> bool:
    if not SECRET:
        return True
    ts = headers.get("X-Voicer-Timestamp", "")
    sent = headers.get("X-Voicer-Signature", "").replace("sha256=", "")
    expected = hmac.new(SECRET.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sent)


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    def log_message(self, *_):
        pass

    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        raw = self.rfile.read(length)
        if not verify(raw, self.headers):
            self._send(403, b'{"error":"bad signature"}', "application/json")
            return
        handler = ROUTES.get(self.path.split("?")[0])
        if handler is None:
            self._send(404, b'{"error":"no such route"}', "application/json")
            return
        event = json.loads(raw or b"{}")
        print(f"  → {self.path}  event={event.get('event')} "
              f"digits={event.get('digits','')!r} callId={event.get('callId','')[:8]}")
        ccd = handler(event)
        self._send(200, json.dumps(ccd).encode(), "application/json")

    def _send(self, code: int, body: bytes, ctype: str):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


def main():
    print(f"Survey sample on :{PORT}  (control URL → {BASE}/survey)")
    if not SECRET:
        print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).")
    ThreadingHTTPServer(("0.0.0.0", PORT), Handler).serve_forever()


if __name__ == "__main__":
    main()
