// VoicerOnePBX Programmable Voice — Example 3: Spoken Support Search (Go, 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.
//
// Run:  VOICER_SECRET=… VOICER_BASE_URL=http://<lan-ip>:8080 go run main.go
// Point the PBX endpoint's control URL at:  http://<lan-ip>:8080/support
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"
)

var (
	secret = os.Getenv("VOICER_SECRET")
	base   = envOr("VOICER_BASE_URL", "http://localhost:8080")
	port   = envOr("PORT", "8080")
)

type Verb map[string]any
type CCD []Verb

type Event struct {
	Event  string `json:"event"`
	CallID string `json:"callId"`
	Digits string `json:"digits"`
	Speech string `json:"speech"`
	Reason string `json:"reason"`
}

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

func searchKB(query string) (string, bool) {
	q := strings.ToLower(query)
	best, bestScore := "", 0
	for _, e := range kb {
		score := 0
		for _, k := range e.keywords {
			if strings.Contains(q, k) {
				score++
			}
		}
		if score > bestScore {
			best, bestScore = e.answer, score
		}
	}
	return best, bestScore > 0
}

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

func handleStart(e Event) CCD {
	return CCD{
		{"say": map[string]any{"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": map[string]any{"input": "speech", "speechTimeout": 8, "actionUrl": base + "/support/search"}},
	}
}

func handleSearch(e Event) CCD {
	if e.Reason == "hangup" {
		return CCD{}
	}
	query := strings.TrimSpace(e.Speech)
	if query == "" {
		return CCD{
			{"say": map[string]any{"text": "Sorry, I didn't catch that."}},
			{"redirect": map[string]any{"url": base + "/support"}},
		}
	}
	answer, ok := searchKB(query)
	if !ok {
		return CCD{
			{"say": map[string]any{"text": fmt.Sprintf("I heard: %s. I couldn't find an answer to that. "+
				"Let me connect you with someone who can help.", query)}},
			{"route": map[string]any{"to": "support"}},
		}
	}
	log.Printf("  🔎 query=%q → answered", query)
	return CCD{
		{"say": map[string]any{"text": "Here's what I found. " + answer}},
		{"say": map[string]any{"text": "To ask another question press 1, to speak to an agent press 2, " +
			"or you can hang up. Thanks for calling."}},
		{"gather": map[string]any{"numDigits": 1, "timeout": 6, "actionUrl": base + "/support/after"}},
	}
}

func handleAfter(e Event) CCD {
	if e.Reason == "hangup" {
		return CCD{}
	}
	switch e.Digits {
	case "1":
		return CCD{{"redirect": map[string]any{"url": base + "/support"}}}
	case "2":
		return CCD{{"say": map[string]any{"text": "Connecting you to an agent."}}, {"route": map[string]any{"to": "support"}}}
	default:
		return CCD{{"say": map[string]any{"text": "Thanks for calling Acme support. Goodbye."}}, {"hangup": map[string]any{}}}
	}
}

var routes = map[string]func(Event) CCD{
	"/support":        handleStart,
	"/support/search": handleSearch,
	"/support/after":  handleAfter,
}

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

func verify(body []byte, h http.Header) bool {
	if secret == "" {
		return true
	}
	ts := h.Get("X-Voicer-Timestamp")
	sent := strings.TrimPrefix(h.Get("X-Voicer-Signature"), "sha256=")
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(ts + "."))
	mac.Write(body)
	return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(sent))
}

func handle(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	if !verify(body, r.Header) {
		http.Error(w, `{"error":"bad signature"}`, http.StatusForbidden)
		return
	}
	fn, ok := routes[r.URL.Path]
	if !ok {
		http.Error(w, `{"error":"no such route"}`, http.StatusNotFound)
		return
	}
	var e Event
	_ = json.Unmarshal(body, &e)
	log.Printf("  → %s  event=%s speech=%q digits=%q", r.URL.Path, e.Event, e.Speech, e.Digits)
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(fn(e))
}

func main() {
	for p := range routes {
		http.HandleFunc(p, handle)
	}
	fmt.Printf("Support-search sample on :%s  (control URL → %s/support)\n", port, base)
	if secret == "" {
		fmt.Println("  ⚠️  VOICER_SECRET is empty — signature verification is DISABLED (dev only).")
	}
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

func envOr(k, def string) string {
	if v := os.Getenv(k); v != "" {
		return v
	}
	return def
}
