// VoicerOnePBX Programmable Voice — Example 1: Auto Attendant (Swift, stdlib only)
//
// The classic phone menu: greet, collect one digit, branch to a department.
// See ../python/app.py for the fully-commented reference; this is the same flow, terser.
// Uses only system frameworks (Foundation, Network, CryptoKit) — no packages.
//
// Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 swift main.swift
// Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/ivr

import Foundation
import Network
import CryptoKit

let SECRET = ProcessInfo.processInfo.environment["VOICER_SECRET"] ?? ""
let BASE   = ProcessInfo.processInfo.environment["VOICER_BASE_URL"] ?? "http://localhost:8080"
let PORT   = UInt16(ProcessInfo.processInfo.environment["PORT"] ?? "8080") ?? 8080

typealias Verb = [String: Any]
typealias CCD = [Verb]

// Where each menu choice sends the caller. `route.to` values are IDs the PBX admin configured.
let department: [String: (phrase: String, transfer: Verb)] = [
    "1": ("Connecting you to sales.",     ["route": ["to": "sales"]]),
    "2": ("Connecting you to support.",   ["route": ["to": "support"]]),
    "0": ("Connecting you to reception.", ["route": ["to": "100"]]),
    "9": ("Connecting your call.",        ["forward": ["to": "+19055550134"]]),
]

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

func handleIVR(_ event: [String: Any]) -> CCD {
    [
        ["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."]],
        ["gather": ["numDigits": 1, "timeout": 6, "actionUrl": "\(BASE)/ivr/main"]],
    ]
}

func handleMain(_ event: [String: Any]) -> CCD {
    let reason = event["reason"] as? String ?? ""
    let digits = event["digits"] as? String ?? ""
    if reason == "hangup" { return [] }                          // caller gone
    guard reason != "timeout", let dep = department[digits] else {
        return [["say": ["text": "Sorry, I did not get that."]],
                ["redirect": ["url": "\(BASE)/ivr"]]]
    }
    return [["say": ["text": dep.phrase]], dep.transfer]
}

func route(path: String, event: [String: Any]) -> CCD? {
    switch path {
    case "/ivr":      return handleIVR(event)
    case "/ivr/main": return handleMain(event)
    default:          return nil
    }
}

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

/// Validate the PBX's HMAC signature: sha256=hex(HMAC-SHA256(secret, ts + "." + body)). See contract §7.
func verify(body: Data, headers: [String: String]) -> Bool {
    if SECRET.isEmpty { return true }                            // dev mode — skipped (warned at startup)
    let ts = headers["x-voicer-timestamp"] ?? ""
    let sent = (headers["x-voicer-signature"] ?? "").replacingOccurrences(of: "sha256=", with: "")
    var msg = Data("\(ts).".utf8); msg.append(body)
    let mac = HMAC<SHA256>.authenticationCode(for: msg, using: SymmetricKey(data: Data(SECRET.utf8)))
    let hexStr = mac.map { String(format: "%02x", $0) }.joined()
    // constant-time compare
    guard hexStr.utf8.count == sent.utf8.count else { return false }
    var diff: UInt8 = 0
    for (a, b) in zip(hexStr.utf8, sent.utf8) { diff |= a ^ b }
    return diff == 0
}

/// Verify → parse event → run the route → serialize the CCD. Returns (status, contentType, body).
func appHandler(path: String, headers: [String: String], body: Data) -> (Int, String, Data) {
    if !verify(body: body, headers: headers) {
        return (403, "application/json", Data(#"{"error":"bad signature"}"#.utf8))
    }
    let event = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] ?? [:]
    print("  → \(path)  event=\(event["event"] ?? "") digits=\(event["digits"] ?? "")")
    guard let ccd = route(path: path, event: event) else {
        return (404, "application/json", Data(#"{"error":"no such route"}"#.utf8))
    }
    let data = (try? JSONSerialization.data(withJSONObject: ccd)) ?? Data("[]".utf8)
    return (200, "application/json", data)
}

// A minimal HTTP/1.1 server over the Network framework. Reads one request, replies, closes. In production use
// Vapor/Hummingbird and keep only `verify` + the handlers above.
func startServer() {
    let listener = try! NWListener(using: .tcp, on: NWEndpoint.Port(rawValue: PORT)!)
    listener.newConnectionHandler = { conn in
        conn.start(queue: .global())
        var buf = Data()
        func read() {
            conn.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, done, err in
                if let data = data { buf.append(data) }
                if let hdrEnd = buf.range(of: Data("\r\n\r\n".utf8)) {
                    let head = String(data: buf.subdata(in: 0..<hdrEnd.lowerBound), encoding: .utf8) ?? ""
                    let lines = head.components(separatedBy: "\r\n")
                    let reqParts = lines.first?.components(separatedBy: " ") ?? []
                    let path = reqParts.count > 1 ? reqParts[1] : "/"
                    var headers = [String: String]()
                    for l in lines.dropFirst() {
                        if let c = l.firstIndex(of: ":") {
                            headers[l[..<c].trimmingCharacters(in: .whitespaces).lowercased()] =
                                l[l.index(after: c)...].trimmingCharacters(in: .whitespaces)
                        }
                    }
                    let clen = Int(headers["content-length"] ?? "0") ?? 0
                    let bodyStart = hdrEnd.upperBound
                    if buf.count - bodyStart >= clen {
                        let body = buf.subdata(in: bodyStart..<(bodyStart + clen))
                        let (code, ctype, respBody) = appHandler(path: path, headers: headers, body: body)
                        let status = ["200": "200 OK", "403": "403 Forbidden", "404": "404 Not Found"]["\(code)"] ?? "\(code)"
                        var out = Data("HTTP/1.1 \(status)\r\nContent-Type: \(ctype)\r\nContent-Length: \(respBody.count)\r\nConnection: close\r\n\r\n".utf8)
                        out.append(respBody)
                        conn.send(content: out, completion: .contentProcessed { _ in conn.cancel() })
                        return
                    }
                }
                if done || err != nil { conn.cancel(); return }
                read()
            }
        }
        read()
    }
    listener.start(queue: .global())
    print("Auto-attendant sample on :\(PORT)  (control URL → \(BASE)/ivr)")
    if SECRET.isEmpty { print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).") }
}

startServer()
dispatchMain()
