// Dashboard AI agent endpoints. Sessions and their message runs are per-user;
// message and approval runs stream over SSE (text deltas, tool step events,
// approval_required, done with credits_remaining). The run executes in the
// request context, so a client that aborts the fetch cancels the run (the stop
// mechanism). Tools execute AS the invoking member with their org permission
// bits enforced by the registry.
package handler

import (
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"

	"github.com/warmbly/warmbly/internal/api/middleware"
	"github.com/warmbly/warmbly/internal/app/aiagent"
	"github.com/warmbly/warmbly/internal/app/aitools"
	"github.com/warmbly/warmbly/internal/errx"
	"github.com/warmbly/warmbly/internal/utils/paging"
)

// jwtInvocation builds a tool invocation for a JWT (dashboard) caller: it runs
// as the member with their org permission bits, never an API key.
func (h *Handler) jwtInvocation(c *gin.Context) (aitools.Invocation, *errx.Error) {
	orgID := middleware.GetOrganizationID(c)
	if orgID == nil {
		return aitools.Invocation{}, errx.New(errx.BadRequest, "no organization selected")
	}
	userID, err := middleware.GetUserUUID(c)
	if err != nil {
		return aitools.Invocation{}, errx.New(errx.Unauthorized, "invalid user")
	}
	member, xerr := h.OrganizationService.GetMembership(c.Request.Context(), *orgID, userID)
	if xerr != nil || member == nil {
		return aitools.Invocation{}, errx.New(errx.Forbidden, "not a member of this organization")
	}
	return aitools.Invocation{
		OrgID:     *orgID,
		UserID:    userID,
		OrgPerms:  member.Permissions,
		IsAPIKey:  false,
		IP:        c.ClientIP(),
		UserAgent: c.Request.UserAgent(),
	}, nil
}

// CreateAgentSession — POST /ai/sessions
func (h *Handler) CreateAgentSession(c *gin.Context) {
	if h.AIAgentService == nil {
		errx.JSON(c, errx.New(errx.ServiceUnavailable, "the AI assistant is not configured"))
		return
	}
	inv, xerr := h.jwtInvocation(c)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	var req struct {
		Page     string `json:"page"`
		Resource string `json:"resource"`
	}
	_ = c.ShouldBindJSON(&req)

	sess, serr := h.AIAgentService.CreateSession(c.Request.Context(), inv.OrgID, inv.UserID, req.Page, req.Resource)
	if serr != nil {
		errx.JSON(c, serr)
		return
	}
	c.JSON(http.StatusOK, sess)
}

// ListAgentSessions — GET /ai/sessions (cursor paginated, newest first)
func (h *Handler) ListAgentSessions(c *gin.Context) {
	if h.AIAgentService == nil {
		errx.JSON(c, errx.New(errx.ServiceUnavailable, "the AI assistant is not configured"))
		return
	}
	inv, xerr := h.jwtInvocation(c)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	limit, xerr := parseCreditLimit(c.Query("limit"), 25, 100)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	beforeAt, beforeID, xerr := paging.DecodeTimeCursor(c.Query("cursor"))
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	sessions, err := h.AIAgentService.ListSessions(c.Request.Context(), inv.OrgID, inv.UserID, limit+1, beforeAt, beforeID)
	if err != nil {
		errx.JSON(c, errx.New(errx.Internal, "failed to list sessions"))
		return
	}
	var nextCursor *string
	if len(sessions) > limit {
		last := sessions[limit-1]
		nextCursor = paging.EncodeTime(last.CreatedAt, last.ID)
		sessions = sessions[:limit]
	}
	c.JSON(http.StatusOK, gin.H{
		"data":       sessions,
		"pagination": gin.H{"next_cursor": nextCursor, "has_more": nextCursor != nil},
	})
}

// AgentSessionMessages — GET /ai/sessions/:id/messages returns a session's
// hydrated transcript (+ any pending approval) so a reopened tab rehydrates.
func (h *Handler) AgentSessionMessages(c *gin.Context) {
	if h.AIAgentService == nil {
		errx.JSON(c, errx.New(errx.ServiceUnavailable, "the AI assistant is not configured"))
		return
	}
	inv, xerr := h.jwtInvocation(c)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	sessionID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid session id"))
		return
	}
	sess, gerr := h.AIAgentService.GetSession(c.Request.Context(), inv.OrgID, inv.UserID, sessionID)
	if gerr != nil || sess == nil {
		errx.JSON(c, errx.New(errx.NotFound, "session not found"))
		return
	}
	turns, terr := h.AIAgentService.Transcript(c.Request.Context(), inv.OrgID, inv.UserID, sessionID)
	if terr != nil {
		errx.JSON(c, terr)
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"title":      sess.Title,
		"turns":      turns,
		"pending":    sess.Context.Pending,
		"free_model": sess.Context.FreeModel,
	})
}

// DeleteAgentSession — DELETE /ai/sessions/:id removes a conversation and its
// transcript. Sessions are private to the member, so no extra permission gate.
func (h *Handler) DeleteAgentSession(c *gin.Context) {
	if h.AIAgentService == nil {
		errx.JSON(c, errx.New(errx.ServiceUnavailable, "the AI assistant is not configured"))
		return
	}
	inv, xerr := h.jwtInvocation(c)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	sessionID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid session id"))
		return
	}
	if derr := h.AIAgentService.DeleteSession(c.Request.Context(), inv.OrgID, inv.UserID, sessionID); derr != nil {
		errx.JSON(c, derr)
		return
	}
	c.JSON(http.StatusOK, gin.H{"message": "session deleted"})
}

// ClearAgentSessions — DELETE /ai/sessions removes the member's entire
// conversation history in this workspace.
func (h *Handler) ClearAgentSessions(c *gin.Context) {
	if h.AIAgentService == nil {
		errx.JSON(c, errx.New(errx.ServiceUnavailable, "the AI assistant is not configured"))
		return
	}
	inv, xerr := h.jwtInvocation(c)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	n, derr := h.AIAgentService.ClearSessions(c.Request.Context(), inv.OrgID, inv.UserID)
	if derr != nil {
		errx.JSON(c, derr)
		return
	}
	c.JSON(http.StatusOK, gin.H{"message": "history cleared", "deleted": n})
}

// AgentMessage — POST /ai/sessions/:id/messages (SSE)
func (h *Handler) AgentMessage(c *gin.Context) {
	if h.AIAgentService == nil {
		errx.JSON(c, errx.New(errx.ServiceUnavailable, "the AI assistant is not configured"))
		return
	}
	inv, xerr := h.jwtInvocation(c)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	sessionID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid session id"))
		return
	}
	var req struct {
		MessageID string `json:"message_id"`
		Text      string `json:"text"`
		Page      string `json:"page"`
		Resource  string `json:"resource"`
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
		return
	}
	if req.MessageID == "" {
		req.MessageID = uuid.NewString()
	}

	emit := sseEmitter(c)
	if serr := h.AIAgentService.RunMessage(c.Request.Context(), inv, sessionID, req.MessageID, req.Text, req.Page, req.Resource, emit); serr != nil {
		emit(aiagent.StreamEvent{Type: "error", Code: string(codeIdentifier(serr)), Message: serr.Message})
	}
}

// AgentApprove — POST /ai/sessions/:id/approve (SSE) resumes a paused run.
func (h *Handler) AgentApprove(c *gin.Context) {
	if h.AIAgentService == nil {
		errx.JSON(c, errx.New(errx.ServiceUnavailable, "the AI assistant is not configured"))
		return
	}
	inv, xerr := h.jwtInvocation(c)
	if xerr != nil {
		errx.JSON(c, xerr)
		return
	}
	sessionID, err := uuid.Parse(c.Param("id"))
	if err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid session id"))
		return
	}
	var req struct {
		Decision string `json:"decision"` // approve | deny | always_allow
	}
	if err := c.ShouldBindJSON(&req); err != nil {
		errx.JSON(c, errx.New(errx.BadRequest, "invalid request body"))
		return
	}
	switch req.Decision {
	case "approve", "deny", "always_allow":
	default:
		errx.JSON(c, errx.New(errx.BadRequest, "decision must be approve, deny, or always_allow"))
		return
	}

	emit := sseEmitter(c)
	if serr := h.AIAgentService.Resume(c.Request.Context(), inv, sessionID, req.Decision, emit); serr != nil {
		emit(aiagent.StreamEvent{Type: "error", Code: string(codeIdentifier(serr)), Message: serr.Message})
	}
}

// sseEmitter prepares the response for Server-Sent Events and returns a
// flush-per-event emitter. Safe to call once per request.
func sseEmitter(c *gin.Context) func(aiagent.StreamEvent) {
	h := c.Writer.Header()
	h.Set("Content-Type", "text/event-stream")
	h.Set("Cache-Control", "no-cache")
	h.Set("Connection", "keep-alive")
	h.Set("X-Accel-Buffering", "no") // disable proxy buffering so deltas flush
	c.Writer.WriteHeader(http.StatusOK)
	flusher, _ := c.Writer.(http.Flusher)
	if flusher != nil {
		flusher.Flush()
	}
	return func(ev aiagent.StreamEvent) {
		b, err := json.Marshal(ev)
		if err != nil {
			return
		}
		_, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", b)
		if flusher != nil {
			flusher.Flush()
		}
	}
}

// codeIdentifier gives a short machine code for an errx to surface in the SSE
// error event.
func codeIdentifier(e *errx.Error) string {
	switch e.Code {
	case errx.NotFound:
		return "not_found"
	case errx.BadRequest:
		return "bad_request"
	case errx.Forbidden:
		return "forbidden"
	case errx.ServiceUnavailable:
		return "service_unavailable"
	default:
		return "error"
	}
}
