package api

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

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5"

	"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
	"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

// Valid `kind` values. Mirrors the CHECK constraint in migration 0032
// — keep both lists in sync if you add a kind here, otherwise inserts
// pass the handler whitelist and trip the DB constraint instead.
var validPlaybackErrorKinds = map[string]struct{}{
	"zero_duration": {},
	"load_failed":   {},
	"stalled":       {},
}

// Valid `resolution` values. Hidden / deleted / requested are stamped
// automatically when the admin clicks the matching action from the
// inbox; fixed / ignored are operator-driven (no action taken on the
// track itself). Mirrors the CHECK constraint in migration 0032.
var validPlaybackErrorResolutions = map[string]struct{}{
	"hidden":    {},
	"deleted":   {},
	"requested": {},
	"fixed":     {},
	"ignored":   {},
}

// reportPlaybackErrorRequest is the body of POST /api/playback-errors.
// TrackID is a UUID string; Kind is one of [validPlaybackErrorKinds];
// Detail is optional free-text from the client (e.g. ExoPlayer error
// message); ClientID identifies the device/browser so support can
// correlate reports across surfaces.
type reportPlaybackErrorRequest struct {
	TrackID  string  `json:"track_id"`
	Kind     string  `json:"kind"`
	Detail   *string `json:"detail,omitempty"`
	ClientID string  `json:"client_id"`
}

// adminPlaybackErrorView is one row in the admin inbox response. The
// track / album / artist fields are joined so the SPA can render
// without a second round-trip per row.
type adminPlaybackErrorView struct {
	ID            string  `json:"id"`
	TrackID       string  `json:"track_id"`
	UserID        string  `json:"user_id"`
	Username      string  `json:"username"`
	ClientID      string  `json:"client_id"`
	Kind          string  `json:"kind"`
	Detail        *string `json:"detail,omitempty"`
	OccurredAt    string  `json:"occurred_at"`
	ResolvedAt    *string `json:"resolved_at,omitempty"`
	Resolution    *string `json:"resolution,omitempty"`
	TrackTitle    string  `json:"track_title"`
	TrackFilePath string  `json:"track_file_path"`
	ArtistName    string  `json:"artist_name"`
	AlbumTitle    string  `json:"album_title"`
	AlbumID       string  `json:"album_id"`
}

// resolvePlaybackErrorRequest is the body of POST
// /api/admin/playback-errors/{id}/resolve. The resolution string is
// validated against [validPlaybackErrorResolutions].
type resolvePlaybackErrorRequest struct {
	Resolution string `json:"resolution"`
}

// Pagination defaults for the admin list endpoint. 50 fits the
// admin inbox table on a laptop without scrolling; max 200 prevents
// a misbehaving client from asking for the whole table at once.
const (
	defaultPlaybackErrorPageSize = 50
	maxPlaybackErrorPageSize     = 200
)

// handleReportPlaybackError implements POST /api/playback-errors.
// Any signed-in user can report; the handler validates kind + track
// existence and inserts a row for admin review.
func (h *handlers) handleReportPlaybackError(w http.ResponseWriter, r *http.Request) {
	user, ok := requireUser(w, r)
	if !ok {
		return
	}
	var req reportPlaybackErrorRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, apierror.BadRequest("bad_request", "invalid JSON body"))
		return
	}
	trackID, ok := parseUUID(req.TrackID)
	if !ok {
		writeErr(w, apierror.BadRequest("bad_request", "invalid track_id"))
		return
	}
	if _, ok := validPlaybackErrorKinds[req.Kind]; !ok {
		writeErr(w, apierror.BadRequest("bad_request", "invalid kind"))
		return
	}
	if req.ClientID == "" {
		writeErr(w, apierror.BadRequest("bad_request", "client_id required"))
		return
	}
	if _, err := dbq.New(h.pool).GetTrackByID(r.Context(), trackID); err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			writeErr(w, &apierror.Error{Status: 404, Code: "not_found", Message: "track not found"})
			return
		}
		h.logger.Error("api: playback_error: lookup track", "err", err)
		writeErr(w, apierror.Internal(err))
		return
	}
	row, err := dbq.New(h.pool).InsertPlaybackError(r.Context(), dbq.InsertPlaybackErrorParams{
		TrackID:  trackID,
		UserID:   user.ID,
		ClientID: req.ClientID,
		Kind:     req.Kind,
		Detail:   req.Detail,
	})
	if err != nil {
		h.logger.Error("api: playback_error: insert", "err", err)
		writeErr(w, apierror.Internal(err))
		return
	}
	writeJSON(w, http.StatusCreated, map[string]string{"id": uuidToString(row.ID)})
}

// handleListAdminPlaybackErrors implements GET
// /api/admin/playback-errors?resolved=false&offset=0&limit=50.
func (h *handlers) handleListAdminPlaybackErrors(w http.ResponseWriter, r *http.Request) {
	resolved := r.URL.Query().Get("resolved") == "true"
	limit := parsePlaybackErrorLimit(r.URL.Query().Get("limit"))
	offset := parsePlaybackErrorOffset(r.URL.Query().Get("offset"))
	rows, err := dbq.New(h.pool).ListAdminPlaybackErrors(r.Context(), dbq.ListAdminPlaybackErrorsParams{
		ResolvedFilter: resolved,
		Off:            int32(offset),
		Lim:            int32(limit),
	})
	if err != nil {
		h.logger.Error("admin: list playback_errors", "err", err)
		writeAdminJSONErr(w, http.StatusInternalServerError, "server_error")
		return
	}
	out := make([]adminPlaybackErrorView, 0, len(rows))
	for _, row := range rows {
		view := adminPlaybackErrorView{
			ID:            uuidToString(row.ID),
			TrackID:       uuidToString(row.TrackID),
			UserID:        uuidToString(row.UserID),
			Username:      row.Username,
			ClientID:      row.ClientID,
			Kind:          row.Kind,
			Detail:        row.Detail,
			OccurredAt:    formatTimestamp(row.OccurredAt),
			Resolution:    row.Resolution,
			TrackTitle:    row.TrackTitle,
			TrackFilePath: row.TrackFilePath,
			ArtistName:    row.ArtistName,
			AlbumTitle:    row.AlbumTitle,
			AlbumID:       uuidToString(row.AlbumID),
		}
		if row.ResolvedAt.Valid {
			ts := formatTimestamp(row.ResolvedAt)
			view.ResolvedAt = &ts
		}
		out = append(out, view)
	}
	writeJSON(w, http.StatusOK, out)
}

// handleResolvePlaybackError implements POST
// /api/admin/playback-errors/{id}/resolve.
func (h *handlers) handleResolvePlaybackError(w http.ResponseWriter, r *http.Request) {
	admin, ok := auth.UserFromContext(r.Context())
	if !ok {
		writeAdminJSONErr(w, http.StatusUnauthorized, "unauthorized")
		return
	}
	id, ok := parseUUID(chi.URLParam(r, "id"))
	if !ok {
		writeAdminJSONErr(w, http.StatusBadRequest, "invalid_id")
		return
	}
	var req resolvePlaybackErrorRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeAdminJSONErr(w, http.StatusBadRequest, "bad_request")
		return
	}
	if _, ok := validPlaybackErrorResolutions[req.Resolution]; !ok {
		writeAdminJSONErr(w, http.StatusBadRequest, "invalid_resolution")
		return
	}
	resolution := req.Resolution
	row, err := dbq.New(h.pool).ResolvePlaybackError(r.Context(), dbq.ResolvePlaybackErrorParams{
		ID:         id,
		ResolvedBy: admin.ID,
		Resolution: &resolution,
	})
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			writeAdminJSONErr(w, http.StatusNotFound, "not_found")
			return
		}
		h.logger.Error("admin: resolve playback_error", "err", err)
		writeAdminJSONErr(w, http.StatusInternalServerError, "server_error")
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"id": uuidToString(row.ID)})
}

func parsePlaybackErrorLimit(raw string) int {
	if raw == "" {
		return defaultPlaybackErrorPageSize
	}
	n, err := strconv.Atoi(raw)
	if err != nil || n <= 0 {
		return defaultPlaybackErrorPageSize
	}
	if n > maxPlaybackErrorPageSize {
		return maxPlaybackErrorPageSize
	}
	return n
}

func parsePlaybackErrorOffset(raw string) int {
	if raw == "" {
		return 0
	}
	n, err := strconv.Atoi(raw)
	if err != nil || n < 0 {
		return 0
	}
	return n
}
