// VoicerOnePBX Programmable Voice — Example 2: Phone Survey (Swift, stdlib only)
//
// A multi-step flow that keeps state across callbacks, keyed by the stable callId.
// 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/survey

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]

// Per-call state, keyed by callId. In production this is a DB row; the KEY is what matters. Lock-guarded
// because connections are handled concurrently on a global queue.
final class Store {
    private let lock = NSLock()
    private var records: [String: [String: String]] = [:]
    func start(_ id: String, from: String) { lock.lock(); records[id] = ["from": from]; lock.unlock() }
    func set(_ id: String, _ key: String, _ value: String) {
        lock.lock(); if records[id] != nil { records[id]![key] = value }; lock.unlock()
    }
    func exists(_ id: String) -> Bool { lock.lock(); defer { lock.unlock() }; return records[id] != nil }
    func take(_ id: String) -> [String: String]? { lock.lock(); defer { lock.unlock() }; return records.removeValue(forKey: id) }
}
let store = Store()

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

func handleStart(_ e: [String: Any]) -> CCD {
    store.start(e["callId"] as? String ?? "", from: e["from"] as? String ?? "")
    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": "\(BASE)/survey/q2"]],
    ]
}

func handleQ2(_ e: [String: Any]) -> CCD {
    let id = e["callId"] as? String ?? ""
    guard store.exists(id), (e["reason"] as? String) != "hangup" else { return [] }
    if let d = e["digits"] as? String, !d.isEmpty { store.set(id, "rating", d) }   // ← 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": "\(BASE)/survey/q3"]],
    ]
}

func handleQ3(_ e: [String: Any]) -> CCD {
    let id = e["callId"] as? String ?? ""
    guard store.exists(id), (e["reason"] as? String) != "hangup" else { return [] }
    store.set(id, "recommend", ["1": "yes", "2": "no"][e["digits"] as? String ?? ""] ?? "unknown")
    return [
        ["say": ["text": "Last thing: leave any comments after the tone, then press pound. "
                       + "Or press pound now to finish."]],
        ["record": ["maxSeconds": 30, "finishOnKey": "#", "transcribe": true, "playBeep": true,
                    "actionUrl": "\(BASE)/survey/done"]],
    ]
}

func handleDone(_ e: [String: Any]) -> CCD {
    if var rec = store.take(e["callId"] as? String ?? "") {
        rec["comment"] = e["transcript"] as? String ?? ""
        print("  ✅ SURVEY COMPLETE: \(rec)")                    // replace with a DB insert
    }
    return [["say": ["text": "Thank you for your feedback. Goodbye."]], ["hangup": [:]]]
}

func route(path: String, event: [String: Any]) -> CCD? {
    switch path {
    case "/survey":      return handleStart(event)
    case "/survey/q2":   return handleQ2(event)
    case "/survey/q3":   return handleQ3(event)
    case "/survey/done": return handleDone(event)
    default:             return nil
    }
}

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

func verify(body: Data, headers: [String: String]) -> Bool {
    if SECRET.isEmpty { return true }
    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()
    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
}

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)
}

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("Survey sample on :\(PORT)  (control URL → \(BASE)/survey)")
    if SECRET.isEmpty { print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).") }
}

startServer()
dispatchMain()
