package handlers

// HTTP handlers for the Custom Views feature (t-paliad-144 Phase A1).
//
// Endpoints:
//   GET    /api/user-views                       — list saved views
//   POST   /api/user-views                       — create
//   GET    /api/user-views/{id}                  — fetch one
//   PATCH  /api/user-views/{id}                  — partial update
//   DELETE /api/user-views/{id}                  — delete
//   POST   /api/user-views/{id}/touch            — bump last_used_at
//
//   POST   /api/views/run                        — run an ad-hoc spec
//   POST   /api/views/{slug}/run                 — run a saved view by slug
//   GET    /api/views/system                     — list system view definitions
//
// All endpoints require authentication. Paliad's RLS scopes user_views
// rows to auth.uid(); the handler layer also AND-joins userID for
// defense-in-depth.

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"

	"github.com/google/uuid"

	"mgit.msbls.de/m/paliad/internal/services"
)

// requireUserViews returns true when the user-view + substrate services
// are wired. Calls writeJSON 503 + returns false otherwise.
func requireUserViews(w http.ResponseWriter) bool {
	if !requireDB(w) {
		return false
	}
	if dbSvc.userView == nil || dbSvc.event == nil {
		writeJSON(w, http.StatusServiceUnavailable, map[string]string{
			"error": "views not configured",
		})
		return false
	}
	return true
}

// ============================================================================
// /api/user-views — CRUD
// ============================================================================

// GET /api/user-views — list the caller's saved views.
func handleListUserViews(w http.ResponseWriter, r *http.Request) {
	if !requireUserViews(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	views, err := dbSvc.userView.ListForUser(r.Context(), uid)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, views)
}

// userViewCreatePayload mirrors services.CreateUserViewInput on the wire.
type userViewCreatePayload struct {
	Slug       string              `json:"slug"`
	Name       string              `json:"name"`
	Icon       *string             `json:"icon,omitempty"`
	FilterSpec services.FilterSpec `json:"filter_spec"`
	RenderSpec services.RenderSpec `json:"render_spec"`
	ShowCount  bool                `json:"show_count,omitempty"`
}

// POST /api/user-views — create.
func handleCreateUserView(w http.ResponseWriter, r *http.Request) {
	if !requireUserViews(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	var p userViewCreatePayload
	if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"})
		return
	}
	created, err := dbSvc.userView.Create(r.Context(), uid, services.CreateUserViewInput{
		Slug:       p.Slug,
		Name:       p.Name,
		Icon:       p.Icon,
		FilterSpec: p.FilterSpec,
		RenderSpec: p.RenderSpec,
		ShowCount:  p.ShowCount,
	})
	if err != nil {
		writeUserViewError(w, err)
		return
	}
	writeJSON(w, http.StatusCreated, created)
}

// GET /api/user-views/{id} — fetch one.
func handleGetUserView(w http.ResponseWriter, r *http.Request) {
	if !requireUserViews(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	view, err := dbSvc.userView.GetByID(r.Context(), uid, id)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if view == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
		return
	}
	writeJSON(w, http.StatusOK, view)
}

// userViewUpdatePayload accepts every field as optional. `null` icon
// clears the field (matching service-side semantic of *string{""} → clear).
type userViewUpdatePayload struct {
	Slug       *string              `json:"slug,omitempty"`
	Name       *string              `json:"name,omitempty"`
	Icon       *string              `json:"icon,omitempty"`
	FilterSpec *services.FilterSpec `json:"filter_spec,omitempty"`
	RenderSpec *services.RenderSpec `json:"render_spec,omitempty"`
	SortOrder  *int                 `json:"sort_order,omitempty"`
	ShowCount  *bool                `json:"show_count,omitempty"`
}

// PATCH /api/user-views/{id} — partial update.
func handleUpdateUserView(w http.ResponseWriter, r *http.Request) {
	if !requireUserViews(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	var p userViewUpdatePayload
	if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"})
		return
	}
	updated, err := dbSvc.userView.Update(r.Context(), uid, id, services.UpdateUserViewInput{
		Slug:       p.Slug,
		Name:       p.Name,
		Icon:       p.Icon,
		FilterSpec: p.FilterSpec,
		RenderSpec: p.RenderSpec,
		SortOrder:  p.SortOrder,
		ShowCount:  p.ShowCount,
	})
	if err != nil {
		writeUserViewError(w, err)
		return
	}
	if updated == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
		return
	}
	writeJSON(w, http.StatusOK, updated)
}

// DELETE /api/user-views/{id} — delete.
func handleDeleteUserView(w http.ResponseWriter, r *http.Request) {
	if !requireUserViews(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	deleted, err := dbSvc.userView.Delete(r.Context(), uid, id)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if !deleted {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// POST /api/user-views/{id}/touch — bump last_used_at. Fire-and-forget
// from the page handler (Q10 most-recently-used landing).
func handleTouchUserView(w http.ResponseWriter, r *http.Request) {
	if !requireUserViews(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	id, err := uuid.Parse(r.PathValue("id"))
	if err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
		return
	}
	if err := dbSvc.userView.Touch(r.Context(), uid, id); err != nil {
		writeServiceError(w, err)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

// ============================================================================
// /api/views — substrate execution
// ============================================================================

// runRequest wraps the optional spec override for /api/views/{slug}/run.
// When body is empty / fields are zero-valued, the saved spec is used as-is.
type runRequest struct {
	Filter *services.FilterSpec `json:"filter,omitempty"`
	Render *services.RenderSpec `json:"render,omitempty"` // currently informational; substrate ignores
}

// POST /api/views/run — execute an ad-hoc FilterSpec without persisting.
//
// Used by the editor's live-preview (Q27) and by the inbox/agenda
// system pages internally (Phase B will route them here; Phase A1
// leaves the wiring as a no-op for those pages).
func handleRunAdhocView(w http.ResponseWriter, r *http.Request) {
	if !requireUserViews(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	var p runRequest
	if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"})
		return
	}
	if p.Filter == nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "filter is required"})
		return
	}
	if err := p.Filter.Validate(); err != nil {
		writeServiceError(w, err)
		return
	}
	res, err := dbSvc.event.RunSpec(r.Context(), uid, *p.Filter, dbSvc.approval)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, res)
}

// POST /api/views/{slug}/run — run a saved view (or a system view by slug).
//
// Optional body: { filter: <override> } overrides the saved spec for
// this run only (transient — doesn't mutate the stored row). Used for
// query-param overrides in the URL contract (Q16).
func handleRunSavedView(w http.ResponseWriter, r *http.Request) {
	if !requireUserViews(w) {
		return
	}
	uid, ok := requireUser(w, r)
	if !ok {
		return
	}
	slug := r.PathValue("slug")
	if slug == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "slug is required"})
		return
	}

	// System view first — code-resident; doesn't need DB read.
	if sys := lookupSystemView(slug); sys != nil {
		spec := sys.Filter
		if err := maybeOverrideSpec(&spec, r.Body); err != nil {
			writeServiceError(w, err)
			return
		}
		res, err := dbSvc.event.RunSpec(r.Context(), uid, spec, dbSvc.approval)
		if err != nil {
			writeServiceError(w, err)
			return
		}
		writeJSON(w, http.StatusOK, res)
		return
	}

	// User view.
	view, err := dbSvc.userView.GetBySlug(r.Context(), uid, slug)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if view == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "view not found"})
		return
	}
	spec, err := services.UnmarshalFilterSpec(view.FilterSpec)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	if err := maybeOverrideSpec(&spec, r.Body); err != nil {
		writeServiceError(w, err)
		return
	}
	res, err := dbSvc.event.RunSpec(r.Context(), uid, spec, dbSvc.approval)
	if err != nil {
		writeServiceError(w, err)
		return
	}
	writeJSON(w, http.StatusOK, res)
}

// GET /api/views/system — list system view definitions. Used by the
// editor to seed "start from a system view as a template".
func handleListSystemViews(w http.ResponseWriter, r *http.Request) {
	if _, ok := requireUser(w, r); !ok {
		return
	}
	writeJSON(w, http.StatusOK, services.AllSystemViews())
}

// ============================================================================
// helpers
// ============================================================================

// lookupSystemView returns a SystemView whose slug matches, or nil.
func lookupSystemView(slug string) *services.SystemView {
	for _, sv := range services.AllSystemViews() {
		if sv.Slug == slug {
			view := sv
			return &view
		}
	}
	return nil
}

// maybeOverrideSpec replaces `spec` with body.filter when the request
// body parses as a runRequest with a non-nil Filter. Empty body / no
// override → no-op. The override is validated.
func maybeOverrideSpec(spec *services.FilterSpec, body io.Reader) error {
	var p runRequest
	dec := json.NewDecoder(body)
	if err := dec.Decode(&p); err != nil {
		// Empty body is fine — no override.
		return nil
	}
	if p.Filter == nil {
		return nil
	}
	if err := p.Filter.Validate(); err != nil {
		return err
	}
	*spec = *p.Filter
	return nil
}

// writeUserViewError adds slug-taken handling on top of writeServiceError.
func writeUserViewError(w http.ResponseWriter, err error) {
	if err == nil {
		return
	}
	if errors.Is(err, services.ErrUserViewSlugTaken) {
		writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
		return
	}
	writeServiceError(w, err)
}
