package main

import (
	_ "embed"
	"errors"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"github.com/kami/maven/internal/ipc"
	"github.com/kami/maven/internal/router"
	"github.com/kami/maven/internal/webauthn"
)

//go:embed chat.html
var chatPageHTML string

// chatTmpl — plain text conversation interface. No JS: form POSTs to /api/chat
// and the handler redirects back to /chat with the response.
var chatTmpl = parsePage("chat", chatPageHTML, nil)

// chatMsg — one message in the conversation history.
type chatMsg struct {
	Role string // "user" | "assistant"
	Text string
	// Source — the query source that claimed the turn, shown as a badge beside
	// the reply. Empty for a turn no source claimed (V-539).
	Source string
	// TraceID anchors the correction gesture (V-630). Non-zero ⇒ the turn was
	// persisted and can be corrected in one click. 0 ⇒ no correction is offered,
	// which is honest: a box with no database has no turn to correct.
	TraceID int64
	// Corrected — the owner already corrected this turn, so the page says thank
	// you instead of offering the buttons again.
	Corrected string
}

// correctionTargets — the seven public intents, in the order the buttons are
// shown. Read from internal/router rather than typed out, so a new intent cannot
// exist without a way to correct a turn into it.
var correctionTargets = []router.Intent{
	router.IntentFact, router.IntentNote, router.IntentReminder,
	router.IntentQuery, router.IntentAct, router.IntentChat, router.IntentSystem,
}

// handleChatPage renders the chat conversation page.
func handleChatPage(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI) {
	if !requireCore(w, core, "chat") {
		return
	}
	msgs := []chatMsg{}
	// Read user message + reply from query params (set by /api/chat redirect).
	if q := r.URL.Query().Get("q"); q != "" {
		msgs = append(msgs, chatMsg{Role: "user", Text: q})
	}
	if reply := r.URL.Query().Get("r"); reply != "" {
		id, _ := strconv.ParseInt(r.URL.Query().Get("t"), 10, 64)
		msgs = append(msgs, chatMsg{
			Role: "assistant", Text: reply, Source: r.URL.Query().Get("s"),
			TraceID: id, Corrected: r.URL.Query().Get("c"),
		})
	}
	// UserText rides beside the messages so the correction form can hand the
	// conversation back on the redirect: this page has no session and no JS, so
	// what is on screen is what the query params carry.
	renderPage(w, chatTmpl, struct {
		Error    string
		Messages []chatMsg
		Targets  []router.Intent
		UserText string
	}{Messages: msgs, Targets: correctionTargets, UserText: r.URL.Query().Get("q")})
}

// handleChatAPI processes a chat message POST and redirects back to /chat.
//
// State-changing, and the widest surface on this server: the text reaches the
// router, the LLM, and through mavend's applyAction the whole action path
// including `act` — so it is gated on the same step-up as POST /tools and
// POST /api/revert (Vikunja #317). With WebAuthn unconfigured the gate is
// fail-open exactly like the others (see stepUpOK); with -require-stepup it
// denies, which is the point of that flag.
func handleChatAPI(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, session *webauthn.PasskeySession, requireStepUp bool) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	if !requireCore(w, core, "chat") {
		return
	}
	if !stepUpGate(w, session, requireStepUp) {
		return
	}
	text := strings.TrimSpace(r.FormValue("text"))
	if text == "" {
		http.Redirect(w, r, "/chat", http.StatusSeeOther)
		return
	}
	// One conversation id for the whole web chat, and a different one from
	// telegram or the mic. A parked question belongs to the reach that was
	// asked; before this, a clarify nobody answered on the web ate the next
	// utterance spoken at the mic (Vikunja #466). This server has no
	// per-browser session, so every browser tab is the same conversation —
	// which is right for a single-owner box.
	reply, err := core.Chat(r.Context(), "web", text)
	if err != nil {
		log.Printf("chat api: %v", err)
		http.Redirect(w, r, "/chat", http.StatusSeeOther)
		return
	}
	// The claiming query source rides back on the redirect so the page can show
	// it. Empty for a turn no source claimed, which is most of them.
	dest := "/chat?q=" + url.QueryEscape(text) + "&r=" + url.QueryEscape(reply.Reply)
	if reply.Source != "" {
		dest += "&s=" + url.QueryEscape(reply.Source)
	}
	// The trace id rides along so the reply can carry a correction gesture
	// (V-630). Absent when nothing persisted, and the page then offers none.
	if reply.TraceID != 0 {
		dest += "&t=" + strconv.FormatInt(reply.TraceID, 10)
	}
	http.Redirect(w, r, dest, http.StatusSeeOther)
}

// handleCorrectAPI records that the last turn was routed wrongly (V-630).
//
// A correction is the only supervised signal this box gets, and everything else
// in the trace accumulates on its own. So the gesture has to cost nothing: one
// POST from the reply he is already looking at, carrying the trace id and
// optionally the intent it should have been. An unstated target is accepted,
// because a turn marked wrong with no target is still a usable negative.
//
// Step-up gated like POST /api/chat, and that costs the gesture nothing: he
// tapped to send the turn he is now correcting, so the session is already up.
// It is gated because trace ids are sequential integers and this writes the one
// table the routing heads (V-546) will be fitted on. A caller who can guess an
// id could otherwise mislabel turns he never corrected.
func handleCorrectAPI(w http.ResponseWriter, r *http.Request, core ipc.CoreAPI, session *webauthn.PasskeySession, requireStepUp bool) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}
	if !requireCore(w, core, "correct") {
		return
	}
	if !stepUpGate(w, session, requireStepUp) {
		return
	}
	id, err := strconv.ParseInt(strings.TrimSpace(r.FormValue("trace_id")), 10, 64)
	if err != nil || id <= 0 {
		http.Error(w, "trace_id required", http.StatusBadRequest)
		return
	}
	shouldBe := strings.TrimSpace(r.FormValue("should_be"))
	// Only one of the seven, or nothing. Free text here would put an unroutable
	// label in the one table V-632 fits prototypes from.
	if shouldBe != "" && !isCorrectionTarget(shouldBe) {
		http.Error(w, "should_be must be one of the seven intents", http.StatusBadRequest)
		return
	}
	if err := core.CorrectTurn(r.Context(), id, shouldBe); err != nil {
		log.Printf("correct turn %d: %v", id, err)
		// A turn past the retention bound is gone, and saying so is different
		// from saying the write broke.
		if errors.Is(err, ipc.ErrNoSuchTrace) {
			http.Error(w, "that turn is no longer stored", http.StatusNotFound)
			return
		}
		http.Error(w, "correction failed", http.StatusBadGateway)
		return
	}
	stamp := shouldBe
	if stamp == "" {
		stamp = "wrong"
	}
	// Back to the conversation he was in, with the turn still on screen. The
	// query params carry it, so the correction is preserved by re-sending them.
	dest := "/chat?q=" + url.QueryEscape(r.FormValue("q")) +
		"&r=" + url.QueryEscape(r.FormValue("rep")) + "&c=" + url.QueryEscape(stamp)
	if s := r.FormValue("s"); s != "" {
		dest += "&s=" + url.QueryEscape(s)
	}
	http.Redirect(w, r, dest, http.StatusSeeOther)
}

// isCorrectionTarget — one of the seven, and nothing else.
func isCorrectionTarget(s string) bool {
	for _, t := range correctionTargets {
		if string(t) == s {
			return true
		}
	}
	return false
}
