#!/usr/bin/env python3
"""
VoicerOnePBX Programmable Voice — Example 1: Auto Attendant
===========================================================

The classic phone menu: greet the caller, collect a single digit, and branch to a department. This is the
"hello world" of the platform — one prompt, one gather, a switch statement.

The control flow, in the platform's request→response loop:

    caller dials the DID
        │
        ▼  POST /ivr        (event = call.inbound.pre_route)
    reply: [ say greeting, gather 1 digit → /ivr/main ]
        │
        ▼  POST /ivr/main   (event = gather.completed, digits = "2")
    reply: [ say "...", route/forward/redirect ]   ← ends or loops the call

Everything below the "HTTP plumbing" divider is generic scaffolding you can lift unchanged into the other
examples; the interesting part is `handle_ivr` and `handle_main`.

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/ivr
"""

from __future__ import annotations

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

# ── Configuration (env) ──────────────────────────────────────────────────────
SECRET = os.environ.get("VOICER_SECRET", "")                       # HMAC shared secret; must match the PBX endpoint
BASE   = os.environ.get("VOICER_BASE_URL", "http://localhost:8080")  # how the PBX reaches THIS app (for actionUrls)
PORT   = int(os.environ.get("PORT", "8080"))

# Where each menu choice sends the caller. `route.to` values are IDs the PBX admin configured — an extension,
# a queue, a ring group, or a voicemail box. Ask the admin for the real ones; these are illustrative.
DEPARTMENTS = {
    "1": ("Connecting you to sales.",   {"route": {"to": "sales"}}),      # a queue named "sales"
    "2": ("Connecting you to support.", {"route": {"to": "support"}}),    # a queue named "support"
    "0": ("Connecting you to reception.", {"route": {"to": "100"}}),      # extension 100
    "9": ("Connecting your call.",      {"forward": {"to": "+19055550134"}}),  # an external number, via a trunk
}


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

def handle_ivr(event: dict) -> list:
    """Inbound call arrived (event = call.inbound.pre_route). Greet and open the menu."""
    # `event` has callId, from, to, callerName, trunkId — branch on `to` here if one app serves several DIDs.
    return [
        {"say": {"text": "Thank you for calling Acme. "
                         "For sales press 1, for support press 2, "
                         "for reception press 0, or press 9 to reach our after-hours line."}},
        # A gather ENDS this document. The PBX plays the prompt, collects one digit (or times out after 6 s),
        # then POSTs gather.completed to actionUrl. actionUrl must be absolute and reachable by the PBX.
        {"gather": {"numDigits": 1, "timeout": 6, "actionUrl": f"{BASE}/ivr/main"}},
    ]


def handle_main(event: dict) -> list:
    """A menu digit came back (event = gather.completed). Branch to the chosen department."""
    reason = event.get("reason")
    digits = event.get("digits", "")

    # The caller hung up mid-gather: the call is already gone, so acknowledge with an empty document.
    if reason == "hangup":
        return []

    # No input in time, or a key we don't recognise: re-greet by JUMPING back to /ivr with `redirect`
    # (fetches that CCD with no further input — a clean state-machine loop).
    if reason == "timeout" or digits not in DEPARTMENTS:
        return [
            {"say": {"text": "Sorry, I did not get that."}},
            {"redirect": {"url": f"{BASE}/ivr"}},
        ]

    # A valid choice: say the confirmation and hand the call to the department. `route`/`forward` end the flow.
    phrase, transfer = DEPARTMENTS[digits]
    return [{"say": {"text": phrase}}, transfer]


# Map request path → handler. Add a route here for each actionUrl you hand out.
ROUTES = {
    "/ivr":      handle_ivr,
    "/ivr/main": handle_main,
}


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

def verify(raw_body: bytes, headers) -> bool:
    """Validate the PBX's HMAC signature. See the contract §7.

    signature = HMAC-SHA256(key=SECRET, msg=timestamp + "." + rawBody), hex, sent as "sha256=<hex>".
    """
    if not SECRET:
        return True  # dev mode — verification skipped (warned at startup)
    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()
    # Optionally also reject a stale `ts` here (e.g. abs(now - ts) > 300) to blunt replay.
    return hmac.compare_digest(expected, sent)


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

    def log_message(self, *_):  # quiet the default access log; we print our own line
        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")
            print(f"  ✗ {self.path}  REJECTED (bad HMAC signature)")
            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)                                  # ← run the app logic
        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"Auto-attendant sample on :{PORT}  (control URL → {BASE}/ivr)")
    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()
