package filter

import (
	"time"

	. "github.com/Masterminds/squirrel"
	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/persistence"
)

type Options = model.QueryOptions

var defaultFilters = Eq{"missing": false}

func addDefaultFilters(options Options) Options {
	if options.Filters == nil {
		options.Filters = defaultFilters
	} else {
		options.Filters = And{defaultFilters, options.Filters}
	}
	return options
}

func AlbumsByNewest() Options {
	return addDefaultFilters(addDefaultFilters(Options{Sort: "recently_added", Order: "desc"}))
}

func AlbumsByRecent() Options {
	return addDefaultFilters(Options{Sort: "playDate", Order: "desc", Filters: Gt{"play_date": time.Time{}}})
}

func AlbumsByFrequent() Options {
	return addDefaultFilters(Options{Sort: "playCount", Order: "desc", Filters: Gt{"play_count": 0}})
}

func AlbumsByRandom() Options {
	return addDefaultFilters(Options{Sort: "random"})
}

func AlbumsByName() Options {
	return addDefaultFilters(Options{Sort: "name"})
}

func AlbumsByArtist() Options {
	return addDefaultFilters(Options{Sort: "artist"})
}

func AlbumsByArtistID(artistId string) Options {
	filters := []Sqlizer{
		persistence.Exists("json_tree(participants, '$.albumartist')", Eq{"value": artistId}),
	}
	if conf.Server.Subsonic.ArtistParticipations {
		filters = append(filters,
			persistence.Exists("json_tree(participants, '$.artist')", Eq{"value": artistId}),
		)
	}
	return addDefaultFilters(Options{
		Sort:    "max_year",
		Filters: Or(filters),
	})
}

// AlbumsByContributingArtistID matches albums where the artist performs on a track but is not the
// album artist — Jellyfin's "Featured On". The disjoint complement of AlbumsByArtistID, so an
// artist's own discography never leaks into it.
func AlbumsByContributingArtistID(artistId string) Options {
	return addDefaultFilters(Options{
		Sort: "max_year",
		Filters: And{
			persistence.Exists("json_tree(participants, '$.artist')", Eq{"value": artistId}),
			persistence.NotExists("json_tree(participants, '$.albumartist')", Eq{"value": artistId}),
		},
	})
}

func AlbumsByYear(fromYear, toYear int) Options {
	orderOption := ""
	if fromYear > toYear {
		fromYear, toYear = toYear, fromYear
		orderOption = "desc"
	}
	return addDefaultFilters(Options{
		Sort:  "max_year",
		Order: orderOption,
		Filters: Or{
			And{
				GtOrEq{"min_year": fromYear},
				LtOrEq{"min_year": toYear},
			},
			And{
				GtOrEq{"max_year": fromYear},
				LtOrEq{"max_year": toYear},
			},
		},
	})
}

func SongsByAlbum(albumId string) Options {
	return addDefaultFilters(Options{
		Filters: Eq{"album_id": albumId},
		Sort:    "album",
	})
}

// SongsByArtistID matches media files where the artist participates as album or track artist, in
// album order. Semi-joins media_file_artists; scanning the participants JSON is ~10x slower at scale.
func SongsByArtistID(artistId string) Options {
	return addDefaultFilters(Options{
		Sort: "album",
		Filters: Expr(
			"media_file.id IN (SELECT media_file_id FROM media_file_artists WHERE artist_id = ? AND role IN (?, ?))",
			artistId, model.RoleArtist.String(), model.RoleAlbumArtist.String()),
	})
}

func SongsByGenreAndYearRange(genre string, fromYear, toYear int) Options {
	options := Options{}
	ff := And{}
	if genre != "" {
		ff = append(ff, filterByGenre(genre))
	}
	if fromYear != 0 {
		ff = append(ff, GtOrEq{"year": fromYear})
	}
	if toYear != 0 {
		ff = append(ff, LtOrEq{"year": toYear})
	}
	options.Filters = ff
	return addDefaultFilters(options)
}

func ApplyLibraryFilter(opts Options, musicFolderIds []int) Options {
	if len(musicFolderIds) == 0 {
		return opts
	}

	libraryFilter := Eq{"library_id": musicFolderIds}
	if opts.Filters == nil {
		opts.Filters = libraryFilter
	} else {
		opts.Filters = And{opts.Filters, libraryFilter}
	}

	return opts
}

// ApplyArtistLibraryFilter applies a filter to the given Options to ensure that only artists
// that are associated with the specified music folders are included in the results.
func ApplyArtistLibraryFilter(opts Options, musicFolderIds []int) Options {
	if len(musicFolderIds) == 0 {
		return opts
	}

	artistLibraryFilter := Eq{"library_artist.library_id": musicFolderIds}
	if opts.Filters == nil {
		opts.Filters = artistLibraryFilter
	} else {
		opts.Filters = And{opts.Filters, artistLibraryFilter}
	}

	return opts
}

// ArtistsByRole restricts an artist query to artists appearing in the given role (album artist,
// performer, composer, ...) via library_artist.stats. An unknown role is ignored (no filter).
func ArtistsByRole(opts Options, role model.Role) Options {
	if _, ok := model.AllRoles[role.String()]; !ok {
		return opts
	}
	roleFilter := Expr("JSON_EXTRACT(library_artist.stats, '$." + role.String() + ".m') IS NOT NULL")
	if opts.Filters == nil {
		opts.Filters = roleFilter
	} else {
		opts.Filters = And{opts.Filters, roleFilter}
	}
	return opts
}

func ByGenre(genre string) Options {
	return addDefaultFilters(Options{
		Sort:    "name",
		Filters: filterByGenre(genre),
	})
}

// ByGenreID matches items (albums or songs) tagged with any of the given genre tag ids.
func ByGenreID(genreIds []string) Sqlizer {
	return genreTagFilter(Eq{"value": genreIds})
}

// ByAlbumID matches media files belonging to any of the given albums.
func ByAlbumID(albumIds []string) Sqlizer {
	return Eq{"album_id": albumIds}
}

// AlbumsByYears matches albums whose production year (max_year) is in years.
func AlbumsByYears(years []int) Sqlizer {
	return Eq{"max_year": years}
}

// SongsByYears matches media files whose year is in years.
func SongsByYears(years []int) Sqlizer {
	return Eq{"year": years}
}

// ArtistsByGenreID matches artists credited as album artist on an album with any of the given
// genre tag ids. Non-correlated semi-join: the correlated EXISTS form rescans albums per artist row.
func ArtistsByGenreID(genreIds []string) Sqlizer {
	return Expr(
		`artist.id IN (SELECT jt.value FROM album, json_tree(album.participants, '$.albumartist') jt
			WHERE jt.atom IS NOT NULL AND ?)`,
		genreTagFilter(Eq{"value": genreIds}),
	)
}

// tagIDFilter builds an EXISTS over the given tag role's entries in the tags JSON, matching each
// entry against cond (its name via Like, or its tag id via Eq/IN).
func tagIDFilter(tagName string, cond Sqlizer) Sqlizer {
	return persistence.Exists(`json_tree(tags, "$.`+tagName+`")`, And{NotEq{"atom": nil}, cond})
}

func genreTagFilter(cond Sqlizer) Sqlizer { return tagIDFilter("genre", cond) }

// ByStudioID matches items (albums or songs) whose record-label tag id is in ids.
func ByStudioID(ids []string) Sqlizer {
	return tagIDFilter("recordlabel", Eq{"value": ids})
}

func filterByGenre(genre string) Sqlizer {
	return genreTagFilter(Like{"value": genre})
}

func ByRating() Options {
	return addDefaultFilters(Options{Sort: "rating", Order: "desc", Filters: Gt{"rating": 0}})
}

func ByStarred() Options {
	return addDefaultFilters(Options{Sort: "starred_at", Order: "desc", Filters: Eq{"starred": true}})
}

func ArtistsByStarred() Options {
	return Options{Sort: "starred_at", Order: "desc", Filters: Eq{"starred": true}}
}
