package api

import (
	"context"
	"errors"
	"net/http"
	"net/url"
	"strconv"
	"strings"

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

// Widest plausible bounds for an open-ended year filter. A missing year_from
// means "from the beginning" rather than "from year zero of the query", and
// likewise for year_to, so the caller can filter on one edge only.
const (
	minBrowseYear = 0
	maxBrowseYear = 9999
)

var (
	errBadYear           = errors.New("year_from and year_to must be integers")
	errInvertedYearRange = errors.New("year_from must not be greater than year_to")
)

// yearFilter carries a parsed, validated inclusive year range. active is false
// when the request asked for no year filtering at all — distinct from a range
// that happens to cover everything, because the two take different code paths.
type yearFilter struct {
	from   int32
	to     int32
	active bool
}

// handleListLibraryAlbums implements GET /api/library/albums. Mirrors
// /api/artists?sort=alpha but for albums. The new wrapping-grid page on
// the SPA infinite-scrolls against this endpoint via TanStack
// createInfiniteQuery.
//
// Optional filters (#367): `genre` and `year_from`/`year_to`.
//
// Genre arrives as a QUERY parameter rather than a path segment on purpose.
// Raw ID3 genres routinely contain a slash — "Rock/Pop" is a real tag, and
// the one the task itself cites — which cannot survive a path segment: Go
// normalises %2F and the router would split the value into two segments.
func (h *handlers) handleListLibraryAlbums(w http.ResponseWriter, r *http.Request) {
	limit, offset, err := parsePaging(r.URL.Query())
	if err != nil {
		writeErr(w, apierror.BadRequest("bad_request", err.Error()))
		return
	}
	genre := strings.TrimSpace(r.URL.Query().Get("genre"))
	years, err := parseYearFilter(r.URL.Query())
	if err != nil {
		writeErr(w, apierror.BadRequest("bad_request", err.Error()))
		return
	}
	if genre != "" && years.active {
		// Refused rather than silently honouring one: the UI browses these as
		// separate axes (a genres page, a year filter on the albums page), so
		// the combination can only arrive from a caller that has misunderstood
		// the contract — and quietly dropping half a filter would report a
		// narrower result set than it actually returned.
		writeErr(w, apierror.BadRequest("unsupported_filter_combination",
			"genre and year filters cannot be combined"))
		return
	}

	q := dbq.New(h.pool)
	var (
		items []AlbumRef
		total int64
	)
	switch {
	case genre != "":
		items, total, err = albumsByGenre(r.Context(), q, genre, limit, offset)
	case years.active:
		items, total, err = albumsByYear(r.Context(), q, years, limit, offset)
	default:
		items, total, err = albumsAlpha(r.Context(), q, limit, offset)
	}
	if err != nil {
		h.logger.Error("api: list library albums", "err", err, "genre", genre, "years", years.active)
		writeErr(w, apierror.InternalMsg("lookup failed", err))
		return
	}
	writeJSON(w, http.StatusOK, Page[AlbumRef]{
		Items: items, Total: int(total), Limit: limit, Offset: offset,
	})
}

func albumsAlpha(
	ctx context.Context, q *dbq.Queries, limit, offset int,
) ([]AlbumRef, int64, error) {
	rows, err := q.ListAlbumsAlphaWithArtist(ctx, dbq.ListAlbumsAlphaWithArtistParams{
		Limit: int32(limit), Offset: int32(offset),
	})
	if err != nil {
		return nil, 0, err
	}
	total, err := q.CountAlbums(ctx)
	if err != nil {
		return nil, 0, err
	}
	items := make([]AlbumRef, 0, len(rows))
	for _, row := range rows {
		items = append(items, albumRefFrom(row.Album, row.ArtistName, 0, 0))
	}
	return items, total, nil
}

func albumsByGenre(
	ctx context.Context, q *dbq.Queries, genre string, limit, offset int,
) ([]AlbumRef, int64, error) {
	rows, err := q.ListAlbumsByGenreWithArtist(ctx, dbq.ListAlbumsByGenreWithArtistParams{
		Genre: genre, Lim: int32(limit), Off: int32(offset),
	})
	if err != nil {
		return nil, 0, err
	}
	total, err := q.CountAlbumsByGenre(ctx, genre)
	if err != nil {
		return nil, 0, err
	}
	items := make([]AlbumRef, 0, len(rows))
	for _, row := range rows {
		items = append(items, albumRefFrom(row.Album, row.ArtistName, 0, 0))
	}
	return items, total, nil
}

func albumsByYear(
	ctx context.Context, q *dbq.Queries, years yearFilter, limit, offset int,
) ([]AlbumRef, int64, error) {
	rows, err := q.ListAlbumsByYearRangeWithArtist(ctx,
		dbq.ListAlbumsByYearRangeWithArtistParams{
			YearFrom: years.from, YearTo: years.to,
			Lim: int32(limit), Off: int32(offset),
		})
	if err != nil {
		return nil, 0, err
	}
	total, err := q.CountAlbumsByYearRange(ctx, dbq.CountAlbumsByYearRangeParams{
		YearFrom: years.from, YearTo: years.to,
	})
	if err != nil {
		return nil, 0, err
	}
	items := make([]AlbumRef, 0, len(rows))
	for _, row := range rows {
		items = append(items, albumRefFrom(row.Album, row.ArtistName, 0, 0))
	}
	return items, total, nil
}

// parseYearFilter reads year_from / year_to. Either may be omitted, which
// leaves that edge open — filtering "everything before 1990" shouldn't
// require inventing a lower bound.
func parseYearFilter(raw url.Values) (yearFilter, error) {
	fromRaw := strings.TrimSpace(raw.Get("year_from"))
	toRaw := strings.TrimSpace(raw.Get("year_to"))
	if fromRaw == "" && toRaw == "" {
		return yearFilter{}, nil
	}
	f := yearFilter{from: minBrowseYear, to: maxBrowseYear, active: true}
	if fromRaw != "" {
		n, err := strconv.Atoi(fromRaw)
		if err != nil {
			return yearFilter{}, errBadYear
		}
		f.from = int32(n)
	}
	if toRaw != "" {
		n, err := strconv.Atoi(toRaw)
		if err != nil {
			return yearFilter{}, errBadYear
		}
		f.to = int32(n)
	}
	if f.from > f.to {
		// Rejected rather than swapped: silently reordering would return
		// results for a range the caller didn't ask for, and an inverted
		// range is far more likely a bug than an intent.
		return yearFilter{}, errInvertedYearRange
	}
	return f, nil
}
