// VoicerOnePBX Programmable Voice — Example 3: Spoken Support Search (Swift, stdlib only)
//
// The caller speaks a question; the backend searches a knowledge base and reads the answer back.
// 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/support

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]

// A toy knowledge base. Swap for a DB full-text query, a vector store, or an LLM call — text in, answer out.
let kb: [(keywords: [String], answer: String)] = [
    (["hours", "open", "close", "time"],
     "We are open Monday to Friday, 9 A M to 6 P M Eastern, and closed on weekends."),
    (["password", "reset", "login", "sign in"],
     "To reset your password, visit acme dot com slash reset and follow the emailed link."),
    (["refund", "return", "money back"],
     "Refunds are processed within 5 business days to your original payment method."),
    (["shipping", "delivery", "track", "order"],
     "Standard shipping takes 3 to 5 business days. You will get a tracking link by email once it ships."),
    (["cancel", "subscription", "unsubscribe"],
     "You can cancel any time from Account, then Billing, then Cancel Subscription."),
]

func searchKB(_ query: String) -> String? {
    let q = query.lowercased()
    var best: String?; var bestScore = 0
    for entry in kb {
        let score = entry.keywords.reduce(0) { $0 + (q.contains($1) ? 1 : 0) }
        if score > bestScore { best = entry.answer; bestScore = score }
    }
    return best
}

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

func handleStart(_ e: [String: Any]) -> CCD {
    [
        ["say": ["text": "Welcome to Acme support. After the tone, ask your question in a few words. "
                       + "For example, what are your hours, or how do I reset my password."]],
        // input:"speech" → the PBX records + transcribes on-device, returning the text in `speech`.
        ["gather": ["input": "speech", "speechTimeout": 8, "actionUrl": "\(BASE)/support/search"]],
    ]
}

func handleSearch(_ e: [String: Any]) -> CCD {
    if (e["reason"] as? String) == "hangup" { return [] }
    let query = (e["speech"] as? String ?? "").trimmingCharacters(in: .whitespaces)
    if query.isEmpty {
        return [["say": ["text": "Sorry, I didn't catch that."]],
                ["redirect": ["url": "\(BASE)/support"]]]
    }
    guard let answer = searchKB(query) else {
        return [["say": ["text": "I heard: \(query). I couldn't find an answer to that. "
                               + "Let me connect you with someone who can help."]],
                ["route": ["to": "support"]]]
    }
    print("  🔎 query=\"\(query)\" → answered")
    return [
        ["say": ["text": "Here's what I found. \(answer)"]],
        ["say": ["text": "To ask another question press 1, to speak to an agent press 2, "
                       + "or you can hang up. Thanks for calling."]],
        ["gather": ["numDigits": 1, "timeout": 6, "actionUrl": "\(BASE)/support/after"]],
    ]
}

func handleAfter(_ e: [String: Any]) -> CCD {
    if (e["reason"] as? String) == "hangup" { return [] }
    switch e["digits"] as? String ?? "" {
    case "1": return [["redirect": ["url": "\(BASE)/support"]]]
    case "2": return [["say": ["text": "Connecting you to an agent."]], ["route": ["to": "support"]]]
    default:  return [["say": ["text": "Thanks for calling Acme support. Goodbye."]], ["hangup": [:]]]
    }
}

func route(path: String, event: [String: Any]) -> CCD? {
    switch path {
    case "/support":        return handleStart(event)
    case "/support/search": return handleSearch(event)
    case "/support/after":  return handleAfter(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"] ?? "") speech=\(event["speech"] ?? "") 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("Support-search sample on :\(PORT)  (control URL → \(BASE)/support)")
    if SECRET.isEmpty { print("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).") }
}

startServer()
dispatchMain()
