// AI draft for the compose window. Unlike the generic writing assistant this
// is grounded the same way reply drafts are: the recipient's contact record
// (custom fields, campaign membership), the full correspondence history with
// that address, and the org voice profile all feed the prompt. And instead of
// inventing a purpose when it has none, the model is told to ask: a response
// can be a single clarifying question the composer surfaces to the user.
package handler

import (
	"fmt"
	"net/http"
	"slices"
	"strings"

	"github.com/gin-gonic/gin"

	"github.com/warmbly/warmbly/internal/api/middleware"
	"github.com/warmbly/warmbly/internal/app/credits"
	"github.com/warmbly/warmbly/internal/app/unibox"
	"github.com/warmbly/warmbly/internal/errx"
	"github.com/warmbly/warmbly/internal/models"
	"github.com/warmbly/warmbly/internal/pkg/generation"
)

// composeQuestionPrefix marks a model response that is a clarifying question
// rather than a draft. Kept out of user-visible text by the handler.
const composeQuestionPrefix = "QUESTION:"

const composeQuestionRule = `

MISSING PURPOSE: if you genuinely cannot tell WHAT this email should accomplish (no instruction, no subject, and the history gives no hint), do not invent a pitch. Respond with a single line starting with "QUESTION: " followed by one short, specific question that would let you write the email (for example what the user wants to offer, ask, or follow up on). Ask only when essential; if the context is enough, write the email.`

// DraftCompose — POST /unibox/compose/draft
func (h *Handler) DraftCompose(c *gin.Context) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		errx.JSON(c, errx.New(errx.BadRequest, "no organization selected"))
		return
	}
	userID, err := middleware.GetUserUUID(c)
	if err != nil {
		errx.JSON(c, errx.New(errx.Unauthorized, "invalid user"))
		return
	}
	if h.AIProvider == nil {
		errx.JSON(c, errx.New(errx.ServiceUnavailable, "the AI assistant is not configured"))
		return
	}
	if allowed, xerr := h.FeatureGateService.CanUseUnibox(c.Request.Context(), *orgID); xerr != nil {
		errx.JSON(c, xerr)
		return
	} else if !allowed {
		errx.JSON(c, errx.New(errx.Forbidden, "The unified inbox requires an active trial or paid subscription."))
		return
	}

	var req struct {
		// To is the recipient address; empty is allowed (no grounding yet).
		To          string `json:"to"`
		Subject     string `json:"subject"`
		Instruction string `json:"instruction"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
		return
	}
	address := bareAddress(req.To)

	// Grounding: contact record + prior correspondence with the address.
	contactCtx := ""
	historyBlock := ""
	historyCount := 0
	if address != "" {
		contactCtx = h.contactContext(c, userID, *orgID, address)
		historyBlock, historyCount = h.composeHistoryContext(c, address)
	}

	paid, _ := h.FeatureGateService.IsPaidOrganization(c.Request.Context(), *orgID)
	model := h.AIProvider.ModelForTier(paid)
	voice := h.orgVoice(c.Request.Context(), *orgID, "")
	hasVoice := strings.TrimSpace(voice.ProductDescription) != "" ||
		strings.TrimSpace(voice.ICPNotes) != "" ||
		strings.TrimSpace(voice.VoiceProfile) != ""

	idemKey := strings.TrimSpace(c.GetHeader("Idempotency-Key"))
	local := h.AIProvider.IsLocal()
	reqCtx := c.Request.Context()
	{
		meta := models.CreditMeta{Context: models.CreditContext{Detail: "compose draft to " + address}}
		if actor, aerr := middleware.GetUserUUID(c); aerr == nil {
			meta.ActorID = actor
		}
		reqCtx = models.WithCreditMeta(reqCtx, meta)
	}

	var remaining int
	if local {
		if bal, berr := h.CreditService.GetBalance(reqCtx, *orgID); berr == nil {
			remaining = bal
		}
	} else {
		var cerr error
		remaining, cerr = h.CreditService.Consume(reqCtx, *orgID, credits.CostReplyDraft, "compose_draft", model, 0, idemKey)
		if cerr != nil {
			mapCreditError(c, cerr)
			return
		}
	}

	system := generation.BuildComposeRules(voice) + composeQuestionRule
	if h.SkillsService != nil {
		if pre := h.SkillsService.EnabledPreamble(c.Request.Context(), *orgID); pre != "" {
			system += "\n\n" + pre
		}
	}

	prompt := buildComposePrompt(address, contactCtx, historyBlock, req.Subject, req.Instruction)
	result, gerr := h.AIProvider.Complete(c.Request.Context(), generation.CompletionRequest{
		System: system,
		Prompt: prompt,
		Model:  model,
	})
	if gerr != nil {
		if !local {
			if bal, rerr := h.CreditService.Grant(reqCtx, *orgID, credits.CostReplyDraft, "compose_draft_refund"); rerr == nil {
				remaining = bal
			}
		}
		errx.JSON(c, errx.New(errx.ServiceUnavailable, "The email drafter is temporarily unavailable. Your credits were not charged."))
		return
	}

	charged := 0
	if !local {
		charged = credits.CostReplyDraft
		if extra, serr := h.CreditService.SettleUsage(reqCtx, *orgID, credits.CostReplyDraft, result.Model, result.TokensUsed, "compose_draft", settleKey(idemKey)); serr == nil && extra > 0 {
			remaining -= extra
			charged += extra
		}
	}

	resp := gin.H{
		"credits_remaining": remaining,
		"credits_charged":   charged,
		"tokens_used":       result.TokensUsed,
		"model":             result.Model,
		"grounding": gin.H{
			"contact":       contactCtx != "",
			"history":       historyCount,
			"voice_profile": hasVoice,
		},
	}

	text := strings.TrimSpace(result.Text)
	if q, ok := strings.CutPrefix(text, composeQuestionPrefix); ok {
		resp["question"] = strings.TrimSpace(strings.SplitN(q, "\n", 2)[0])
	} else {
		resp["text"] = text
	}
	c.JSON(http.StatusOK, resp)
}

// composeHistoryMessages bounds how much correspondence with one address is
// pulled into a compose draft. Grounding now carries real message text rather
// than preview lines, so fewer messages say considerably more.
const composeHistoryMessages = 8

// composeHistoryContext renders the most recent conversations with the address
// (both directions) as a compact transcript block, newest first.
func (h *Handler) composeHistoryContext(c *gin.Context, address string) (string, int) {
	oid := middleware.GetOrganizationID(c)
	if oid == nil {
		return "", 0
	}
	msgs, xerr := h.UniboxService.AddressGrounding(c.Request.Context(), *oid, address, composeHistoryMessages)
	if xerr != nil || len(msgs) == 0 {
		return "", 0
	}
	// The query returns newest first; the renderer reads a conversation
	// oldest first, so flip it before rendering.
	slices.Reverse(msgs)
	return unibox.RenderGrounding(msgs), len(msgs)
}

func buildComposePrompt(address, contactCtx, history, subject, instruction string) string {
	var b strings.Builder
	b.WriteString("You are writing a brand-new outbound email (not a reply).\n")
	if address != "" {
		fmt.Fprintf(&b, "\nRecipient: %s\n", address)
	} else {
		b.WriteString("\nRecipient: not chosen yet.\n")
	}
	if contactCtx != "" {
		b.WriteString(contactCtx)
		b.WriteString("\n")
	} else if address != "" {
		b.WriteString("No contact record for this address.\n")
	}
	if history != "" {
		b.WriteString("\nPrevious correspondence with them (newest first):\n\n")
		b.WriteString(history)
		b.WriteString("\n")
	} else if address != "" {
		b.WriteString("\nNo previous correspondence with this address; this is a first touch.\n")
	}
	if s := strings.TrimSpace(subject); s != "" {
		fmt.Fprintf(&b, "\nSubject line the user already wrote: %s\n", s)
	}
	b.WriteString("\nWrite the complete email body, ready to send. Return ONLY the body text: no subject line, no signature, no placeholders like [Name]; if a detail is unknown, write around it.")
	if inst := strings.TrimSpace(instruction); inst != "" {
		fmt.Fprintf(&b, "\nThe user wants: %s", inst)
	}
	return b.String()
}
