// handler.go exposes the chat module's HTTP endpoints.
// Routes follow the workspace/handler.go pattern: a single Handler holds all
// service dependencies, and Mount registers sub-routes with Auth middleware.
//
// Auth strategy:
//   - All routes require a valid session (Auth middleware, 401 on failure).
//   - Admin-only routes are wrapped by adminGuard, which checks IsAdmin and
//     returns 403 for non-admin callers.
//
// Error format: httpx.WriteError (Google API style {code, message, request_id}).
package chat

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"

	"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
	httpx "github.com/yan1h/agent-coding-workflow/internal/transport/http"
	"github.com/yan1h/agent-coding-workflow/internal/transport/http/middleware"
)

// AdminLookup resolves whether a user is an admin.
// Using a narrow interface (rather than importing the user package directly)
// mirrors the workspace handler pattern and makes the dependency testable.
type AdminLookup interface {
	IsAdmin(ctx context.Context, id uuid.UUID) (bool, error)
}

// Handler holds all chat service dependencies and implements chi.Router mounting.
type Handler struct {
	convSvc  ConversationService
	msgSvc   MessageService
	tplSvc   TemplateService
	epSvc    EndpointService
	usageSvc UsageService
	upload   *Uploader
	resolver middleware.SessionResolver
	users    AdminLookup
}

// NewHandler constructs a Handler.
func NewHandler(
	conv ConversationService,
	msg MessageService,
	tpl TemplateService,
	ep EndpointService,
	usage UsageService,
	upload *Uploader,
	resolver middleware.SessionResolver,
	users AdminLookup,
) *Handler {
	return &Handler{
		convSvc:  conv,
		msgSvc:   msg,
		tplSvc:   tpl,
		epSvc:    ep,
		usageSvc: usage,
		upload:   upload,
		resolver: resolver,
		users:    users,
	}
}

// Mount registers all chat routes under /api/v1 on r.
func (h *Handler) Mount(r chi.Router) {
	r.Route("/api/v1", func(r chi.Router) {
		r.Use(middleware.Auth(h.resolver, middleware.AuthOptions{}))

		// User-scoped conversation routes.
		r.Get("/conversations", h.listConversations)
		r.Post("/conversations", h.createConversation)
		r.Get("/conversations/{id}", h.getConversation)
		r.Patch("/conversations/{id}", h.updateConversationTitle)
		r.Delete("/conversations/{id}", h.deleteConversation)
		r.Post("/conversations/{id}/messages", h.sendMessage)
		r.Get("/conversations/{id}/stream", h.streamMessages) // T18
		r.Get("/conversations/{id}/messages", h.listMessages)

		// User-scoped message routes.
		r.Post("/messages/{id}/cancel", h.cancelMessage)
		r.Post("/messages/{id}/retry", h.retryMessage)

		// Upload endpoint (T18).
		r.Post("/uploads", h.upload.Handle)

		// User-scoped template routes.
		r.Get("/prompt-templates", h.listTemplates)
		r.Post("/prompt-templates", h.createTemplate)
		r.Patch("/prompt-templates/{id}", h.updateTemplate)
		r.Delete("/prompt-templates/{id}", h.deleteTemplate)

		// User usage.
		r.Get("/users/me/usage", h.userDailyUsage)

		// Public (non-admin) models list for chat composers.
		r.Get("/models", h.listModelsPublic)

		// Admin-only routes.
		r.Group(func(r chi.Router) {
			r.Use(h.adminGuard)
			r.Get("/admin/llm-endpoints", h.listEndpoints)
			r.Post("/admin/llm-endpoints", h.createEndpoint)
			r.Patch("/admin/llm-endpoints/{id}", h.updateEndpoint)
			r.Delete("/admin/llm-endpoints/{id}", h.deleteEndpoint)
			r.Post("/admin/llm-endpoints/{id}/test", h.testEndpoint)
			r.Get("/admin/llm-models", h.listModels)
			r.Post("/admin/llm-models", h.createModel)
			r.Patch("/admin/llm-models/{id}", h.updateModel)
			r.Delete("/admin/llm-models/{id}", h.deleteModel)
			r.Get("/admin/usage", h.adminUsage)
		})
	})
}

// adminGuard is middleware that enforces admin access for the wrapped routes.
func (h *Handler) adminGuard(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		uid, ok := middleware.UserIDFromContext(r.Context())
		if !ok {
			writeErr(w, r, errs.New(errs.CodeUnauthorized, "unauthorized"))
			return
		}
		isAdmin, err := h.users.IsAdmin(r.Context(), uid)
		if err != nil {
			writeErr(w, r, err)
			return
		}
		if !isAdmin {
			writeErr(w, r, errs.New(errs.CodeForbidden, "admin only"))
			return
		}
		next.ServeHTTP(w, r)
	})
}

// userID returns the authenticated user's ID from the request context.
// Returns CodeUnauthorized if the Auth middleware did not run or yielded no ID.
func (h *Handler) userID(r *http.Request) (uuid.UUID, error) {
	uid, ok := middleware.UserIDFromContext(r.Context())
	if !ok {
		return uuid.Nil, errs.New(errs.CodeUnauthorized, "unauthorized")
	}
	return uid, nil
}

// ===== Transport helpers =====

func writeErr(w http.ResponseWriter, r *http.Request, err error) {
	httpx.WriteError(w, middleware.RequestIDFromContext(r.Context()), err)
}

func writeJSON(w http.ResponseWriter, status int, body any) {
	httpx.WriteJSON(w, status, body)
}

func parseUUID(s string) (uuid.UUID, error) {
	id, err := uuid.Parse(s)
	if err != nil {
		return uuid.Nil, errs.Wrap(err, errs.CodeInvalidInput, "invalid uuid")
	}
	return id, nil
}

// ===== Conversation handlers =====

func (h *Handler) listConversations(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	q := r.URL.Query()
	f := ConversationFilter{
		TitleQuery: q.Get("q"),
	}
	if v := q.Get("limit"); v != "" {
		if n, e := strconv.Atoi(v); e == nil {
			f.Limit = n
		}
	}
	if v := q.Get("project_id"); v != "" {
		if id, e := parseUUID(v); e == nil {
			f.ProjectID = &id
		}
	}
	if v := q.Get("workspace_id"); v != "" {
		if id, e := parseUUID(v); e == nil {
			f.WorkspaceID = &id
		}
	}
	if v := q.Get("issue_id"); v != "" {
		if id, e := parseUUID(v); e == nil {
			f.IssueID = &id
		}
	}
	if v := q.Get("requirement_id"); v != "" {
		if id, e := parseUUID(v); e == nil {
			f.RequirementID = &id
		}
	}
	convs, err := h.convSvc.List(r.Context(), uid, f)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out := make([]ConversationDTO, 0, len(convs))
	for i := range convs {
		out = append(out, toConversationDTO(&convs[i]))
	}
	writeJSON(w, http.StatusOK, out)
}

func (h *Handler) createConversation(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req CreateConversationReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	conv, err := h.convSvc.Create(r.Context(), uid, CreateConversationInput{
		ModelID:       req.ModelID,
		TemplateID:    req.TemplateID,
		SystemPrompt:  req.SystemPrompt,
		ProjectID:     req.ProjectID,
		WorkspaceID:   req.WorkspaceID,
		IssueID:       req.IssueID,
		RequirementID: req.RequirementID,
	})
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusCreated, toConversationDTO(conv))
}

func (h *Handler) getConversation(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	limit := 50
	if v := r.URL.Query().Get("limit"); v != "" {
		if n, e := strconv.Atoi(v); e == nil && n > 0 {
			limit = n
		}
	}
	conv, msgs, err := h.convSvc.Get(r.Context(), uid, id, limit)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	msgDTOs := make([]MessageDTO, 0, len(msgs))
	for i := range msgs {
		msgDTOs = append(msgDTOs, toMessageDTO(&msgs[i]))
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"conversation": toConversationDTO(conv),
		"messages":     msgDTOs,
	})
}

func (h *Handler) updateConversationTitle(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req UpdateConversationTitleReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	if err := h.convSvc.UpdateTitle(r.Context(), uid, id, req.Title); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) deleteConversation(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	if err := h.convSvc.SoftDelete(r.Context(), uid, id); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ===== Message handlers =====

func (h *Handler) sendMessage(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	convID, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req SendMessageReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	userMsgID, assistantMsgID, err := h.msgSvc.Send(r.Context(), uid, convID, req.Content, req.AttachmentIDs)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, SendMessageResp{
		UserMessageID:      userMsgID,
		AssistantMessageID: assistantMsgID,
		StreamURL:          fmt.Sprintf("/api/v1/conversations/%s/stream", convID),
	})
}

func (h *Handler) listMessages(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	convID, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	q := r.URL.Query()
	var beforeID *int64
	if v := q.Get("before_id"); v != "" {
		n, e := strconv.ParseInt(v, 10, 64)
		if e == nil {
			beforeID = &n
		}
	}
	limit := 50
	if v := q.Get("limit"); v != "" {
		if n, e := strconv.Atoi(v); e == nil && n > 0 {
			limit = n
		}
	}
	msgs, err := h.msgSvc.ListMessages(r.Context(), uid, convID, beforeID, limit)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out := make([]MessageDTO, 0, len(msgs))
	for i := range msgs {
		out = append(out, toMessageDTO(&msgs[i]))
	}
	writeJSON(w, http.StatusOK, out)
}

func (h *Handler) cancelMessage(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	msgID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
	if err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "invalid message id"))
		return
	}
	if err := h.msgSvc.Cancel(r.Context(), uid, msgID); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) retryMessage(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	msgID, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
	if err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "invalid message id"))
		return
	}
	newID, convID, err := h.msgSvc.Retry(r.Context(), uid, msgID)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"assistant_message_id": newID,
		"stream_url":           fmt.Sprintf("/api/v1/conversations/%s/stream", convID),
	})
}

// ===== Template handlers =====

func (h *Handler) listTemplates(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	tpls, err := h.tplSvc.List(r.Context(), uid)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out := make([]TemplateDTO, 0, len(tpls))
	for i := range tpls {
		out = append(out, toTemplateDTO(&tpls[i]))
	}
	writeJSON(w, http.StatusOK, out)
}

func (h *Handler) createTemplate(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req CreateTemplateReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	scope := TemplateScope(req.Scope)
	// Creating a system-scoped template requires admin privileges.
	if scope == TemplateScopeSystem {
		isAdmin, e := h.users.IsAdmin(r.Context(), uid)
		if e != nil {
			writeErr(w, r, e)
			return
		}
		if !isAdmin {
			writeErr(w, r, errs.New(errs.CodeForbidden, "system scope requires admin"))
			return
		}
	}
	tpl, err := h.tplSvc.Create(r.Context(), uid, req.Name, req.Content, scope)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusCreated, toTemplateDTO(tpl))
}

func (h *Handler) updateTemplate(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req UpdateTemplateReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	isAdmin, err := h.users.IsAdmin(r.Context(), uid)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	if err := h.tplSvc.Update(r.Context(), uid, isAdmin, id, req.Name, req.Content); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) deleteTemplate(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	isAdmin, err := h.users.IsAdmin(r.Context(), uid)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	if err := h.tplSvc.Delete(r.Context(), uid, isAdmin, id); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ===== Usage handlers =====

func (h *Handler) userDailyUsage(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	q := r.URL.Query()
	to := time.Now().UTC()
	from := to.AddDate(0, 0, -30)
	if v := q.Get("from"); v != "" {
		if t, e := time.Parse(time.RFC3339, v); e == nil {
			from = t
		}
	}
	if v := q.Get("to"); v != "" {
		if t, e := time.Parse(time.RFC3339, v); e == nil {
			to = t
		}
	}
	rows, err := h.usageSvc.UserDaily(r.Context(), uid, from, to)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out := make([]DayUsageDTO, 0, len(rows))
	for _, row := range rows {
		out = append(out, DayUsageDTO{
			Day:              row.Day,
			PromptTokens:     row.PromptTokens,
			CompletionTokens: row.CompletionTokens,
			ThinkingTokens:   row.ThinkingTokens,
			CostUSD:          row.CostUSD,
		})
	}
	writeJSON(w, http.StatusOK, out)
}

// ===== Admin: Endpoint handlers =====

func (h *Handler) listEndpoints(w http.ResponseWriter, r *http.Request) {
	eps, err := h.epSvc.ListEndpoints(r.Context())
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out := make([]EndpointDTO, 0, len(eps))
	for i := range eps {
		out = append(out, toEndpointDTO(&eps[i]))
	}
	writeJSON(w, http.StatusOK, out)
}

func (h *Handler) createEndpoint(w http.ResponseWriter, r *http.Request) {
	uid, err := h.userID(r)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req CreateEndpointReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	ep, err := h.epSvc.CreateEndpoint(r.Context(), uid, CreateEndpointInput{
		Provider:    LLMProvider(req.Provider),
		DisplayName: req.DisplayName,
		BaseURL:     req.BaseURL,
		APIKey:      req.APIKey,
	})
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusCreated, toEndpointDTO(ep))
}

func (h *Handler) updateEndpoint(w http.ResponseWriter, r *http.Request) {
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req UpdateEndpointReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	if err := h.epSvc.UpdateEndpoint(r.Context(), id, UpdateEndpointInput{
		DisplayName: req.DisplayName,
		BaseURL:     req.BaseURL,
		APIKey:      req.APIKey,
	}); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) deleteEndpoint(w http.ResponseWriter, r *http.Request) {
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	if err := h.epSvc.DeleteEndpoint(r.Context(), id); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) testEndpoint(w http.ResponseWriter, r *http.Request) {
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	if err := h.epSvc.TestEndpoint(r.Context(), id); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ===== Admin: Model handlers =====

func (h *Handler) listModels(w http.ResponseWriter, r *http.Request) {
	onlyEnabled := r.URL.Query().Get("only_enabled") == "true"
	models, err := h.epSvc.ListModels(r.Context(), onlyEnabled)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out := make([]ModelDTO, 0, len(models))
	for i := range models {
		out = append(out, toModelDTO(&models[i]))
	}
	writeJSON(w, http.StatusOK, out)
}

// listModelsPublic mirrors listModels but is reachable by any authenticated
// user (non-admin), so chat composers can list available models.
func (h *Handler) listModelsPublic(w http.ResponseWriter, r *http.Request) {
	onlyEnabled := r.URL.Query().Get("only_enabled") == "true"
	models, err := h.epSvc.ListModels(r.Context(), onlyEnabled)
	if err != nil {
		writeErr(w, r, err)
		return
	}
	out := make([]ModelDTO, 0, len(models))
	for i := range models {
		out = append(out, toModelDTO(&models[i]))
	}
	writeJSON(w, http.StatusOK, out)
}

func (h *Handler) createModel(w http.ResponseWriter, r *http.Request) {
	var req CreateModelReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	m, err := h.epSvc.CreateModel(r.Context(), CreateModelInput{
		EndpointID:       req.EndpointID,
		ModelID:          req.ModelID,
		DisplayName:      req.DisplayName,
		Capabilities:     req.Capabilities,
		ContextWindow:    req.ContextWindow,
		MaxOutputTokens:  req.MaxOutputTokens,
		PromptPrice:      req.PromptPricePerMillionUSD,
		CompletionPrice:  req.CompletionPricePerMillionUSD,
		ThinkingPrice:    req.ThinkingPricePerMillionUSD,
		IsTitleGenerator: req.IsTitleGenerator,
		Enabled:          req.Enabled,
		SortOrder:        req.SortOrder,
	})
	if err != nil {
		writeErr(w, r, err)
		return
	}
	writeJSON(w, http.StatusCreated, toModelDTO(m))
}

func (h *Handler) updateModel(w http.ResponseWriter, r *http.Request) {
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	var req UpdateModelReq
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, r, errs.Wrap(err, errs.CodeInvalidInput, "decode body"))
		return
	}
	if err := h.epSvc.UpdateModel(r.Context(), id, UpdateModelInput{
		DisplayName:      req.DisplayName,
		Capabilities:     req.Capabilities,
		ContextWindow:    req.ContextWindow,
		MaxOutputTokens:  req.MaxOutputTokens,
		PromptPrice:      req.PromptPricePerMillionUSD,
		CompletionPrice:  req.CompletionPricePerMillionUSD,
		ThinkingPrice:    req.ThinkingPricePerMillionUSD,
		IsTitleGenerator: req.IsTitleGenerator,
		Enabled:          req.Enabled,
		SortOrder:        req.SortOrder,
	}); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) deleteModel(w http.ResponseWriter, r *http.Request) {
	id, err := parseUUID(chi.URLParam(r, "id"))
	if err != nil {
		writeErr(w, r, err)
		return
	}
	if err := h.epSvc.DeleteModel(r.Context(), id); err != nil {
		writeErr(w, r, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ===== Admin: Usage handler =====

func (h *Handler) adminUsage(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	to := time.Now().UTC()
	from := to.AddDate(0, 0, -30)
	if v := q.Get("from"); v != "" {
		if t, e := time.Parse(time.RFC3339, v); e == nil {
			from = t
		}
	}
	if v := q.Get("to"); v != "" {
		if t, e := time.Parse(time.RFC3339, v); e == nil {
			to = t
		}
	}

	groupBy := q.Get("group_by")
	if groupBy == "" {
		groupBy = "day"
	}

	items := make([]AdminUsageItem, 0)
	switch groupBy {
	case "user":
		rows, err := h.usageSvc.SummarizeByUser(r.Context(), from, to)
		if err != nil {
			writeErr(w, r, err)
			return
		}
		for _, row := range rows {
			items = append(items, AdminUsageItem{
				Key:              row.UserID.String(),
				PromptTokens:     row.PromptTokens,
				CompletionTokens: row.CompletionTokens,
				ThinkingTokens:   row.ThinkingTokens,
				CostUSD:          row.CostUSD,
			})
		}
	case "model":
		rows, err := h.usageSvc.SummarizeByModel(r.Context(), from, to)
		if err != nil {
			writeErr(w, r, err)
			return
		}
		for _, row := range rows {
			items = append(items, AdminUsageItem{
				Key:              row.ModelID.String(),
				PromptTokens:     row.PromptTokens,
				CompletionTokens: row.CompletionTokens,
				ThinkingTokens:   row.ThinkingTokens,
				CostUSD:          row.CostUSD,
			})
		}
	case "day":
		rows, err := h.usageSvc.SummarizeByDay(r.Context(), from, to)
		if err != nil {
			writeErr(w, r, err)
			return
		}
		for _, row := range rows {
			items = append(items, AdminUsageItem{
				Key:              row.Day.Format(time.RFC3339),
				PromptTokens:     row.PromptTokens,
				CompletionTokens: row.CompletionTokens,
				ThinkingTokens:   row.ThinkingTokens,
				CostUSD:          row.CostUSD,
			})
		}
	default:
		writeErr(w, r, errs.New(errs.CodeInvalidInput, "invalid group_by"))
		return
	}

	writeJSON(w, http.StatusOK, AdminUsageResp{Items: items})
}
