package playlists

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/bmatcuk/doublestar/v4"
	"github.com/deluan/rest"
	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/consts"
	"github.com/navidrome/navidrome/log"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/model/request"
)

type Playlists interface {
	// Reads
	GetAll(ctx context.Context, options ...model.QueryOptions) (model.Playlists, error)
	Get(ctx context.Context, id string) (*model.Playlist, error)
	GetWithTracks(ctx context.Context, id string) (*model.Playlist, error)
	GetPlaylists(ctx context.Context, mediaFileId string) (model.Playlists, error)

	// Mutations
	Create(ctx context.Context, playlistId string, name string, ids []string) (string, error)
	Delete(ctx context.Context, id string) error
	Update(ctx context.Context, playlistID string, name *string, comment *string, public *bool, idsToAdd []string, idxToRemove []int) error

	// Track management
	AddTracks(ctx context.Context, playlistID string, ids []string) (int, error)
	AddAlbums(ctx context.Context, playlistID string, albumIds []string) (int, error)
	AddArtists(ctx context.Context, playlistID string, artistIds []string) (int, error)
	AddDiscs(ctx context.Context, playlistID string, discs []model.DiscID) (int, error)
	RemoveTracks(ctx context.Context, playlistID string, trackIds []string) error
	ReorderTrack(ctx context.Context, playlistID string, pos int, newPos int) error

	// Cover art
	SetImage(ctx context.Context, playlistID string, reader io.Reader, ext string) error
	RemoveImage(ctx context.Context, playlistID string) error

	// Import
	ImportFile(ctx context.Context, absolutePath string, sync bool) (*model.Playlist, error)
	ImportFromFolder(ctx context.Context, folder *model.Folder, filename string) (*model.Playlist, error)
	ImportM3U(ctx context.Context, reader io.Reader) (*model.Playlist, error)

	// REST adapters
	NewRepository(ctx context.Context) rest.Repository
	TracksRepository(ctx context.Context, playlistId string, refreshSmartPlaylist bool) rest.Repository
}

// ImageUploadService is a local interface satisfied by core.ImageUploadService.
// Defined here to avoid an import cycle between core and core/playlists.
type ImageUploadService interface {
	SetImage(ctx context.Context, entityType string, entityID string, name string, oldPath string, reader io.Reader, ext string) (filename string, err error)
	RemoveImage(ctx context.Context, path string) error
}

type playlists struct {
	ds        model.DataStore
	imgUpload ImageUploadService
}

func NewPlaylists(ds model.DataStore, imgUpload ImageUploadService) Playlists {
	return &playlists{ds: ds, imgUpload: imgUpload}
}

func InPath(folder model.Folder) bool {
	if conf.Server.PlaylistsPath == "" {
		return true
	}
	rel, _ := filepath.Rel(folder.LibraryPath, folder.AbsolutePath())
	for path := range strings.SplitSeq(conf.Server.PlaylistsPath, string(filepath.ListSeparator)) {
		if match, _ := doublestar.Match(path, rel); match {
			return true
		}
	}
	return false
}

// --- Read operations ---

func (s *playlists) GetAll(ctx context.Context, options ...model.QueryOptions) (model.Playlists, error) {
	return s.ds.Playlist(ctx).GetAll(options...)
}

func (s *playlists) Get(ctx context.Context, id string) (*model.Playlist, error) {
	return s.ds.Playlist(ctx).Get(id)
}

func (s *playlists) GetWithTracks(ctx context.Context, id string) (*model.Playlist, error) {
	return s.ds.Playlist(ctx).GetWithTracks(id, true, false)
}

func (s *playlists) GetPlaylists(ctx context.Context, mediaFileId string) (model.Playlists, error) {
	return s.ds.Playlist(ctx).GetPlaylists(mediaFileId)
}

// --- Mutation operations ---

// Create creates a new playlist (when name is provided) or replaces tracks on an existing
// playlist (when playlistId is provided). This matches the Subsonic createPlaylist semantics.
func (s *playlists) Create(ctx context.Context, playlistId string, name string, ids []string) (string, error) {
	usr, _ := request.UserFrom(ctx)
	err := s.ds.WithTxImmediate(func(tx model.DataStore) error {
		var pls *model.Playlist
		var err error

		if playlistId != "" {
			pls, err = tx.Playlist(ctx).Get(playlistId)
			if err != nil {
				return err
			}
			if pls.IsSmartPlaylist() {
				return model.ErrNotAuthorized
			}
			if !usr.IsAdmin && pls.OwnerID != usr.ID {
				return model.ErrNotAuthorized
			}
		} else {
			pls = &model.Playlist{Name: name}
			pls.OwnerID = usr.ID
		}
		pls.Tracks = nil
		pls.AddMediaFilesByID(ids)

		err = tx.Playlist(ctx).Put(pls)
		playlistId = pls.ID
		return err
	})
	return playlistId, err
}

func (s *playlists) Delete(ctx context.Context, id string) error {
	pls, err := s.checkWritable(ctx, id)
	if err != nil {
		return err
	}

	// Clean up custom cover image file if one exists
	if path := pls.UploadedImagePath(); path != "" {
		if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
			log.Warn(ctx, "Failed to remove playlist image on delete", "path", path, err)
		}
	}

	return s.ds.Playlist(ctx).Delete(id)
}

func (s *playlists) Update(ctx context.Context, playlistID string,
	name *string, comment *string, public *bool,
	idsToAdd []string, idxToRemove []int) error {
	var pls *model.Playlist
	var err error
	hasTrackChanges := len(idsToAdd) > 0 || len(idxToRemove) > 0
	if hasTrackChanges {
		pls, err = s.checkTracksEditable(ctx, playlistID)
	} else {
		pls, err = s.checkWritable(ctx, playlistID)
	}
	if err != nil {
		return err
	}
	return s.ds.WithTxImmediate(func(tx model.DataStore) error {
		repo := tx.Playlist(ctx)

		if len(idxToRemove) > 0 {
			tracksRepo := repo.Tracks(playlistID, false)
			// Convert 0-based indices to 1-based position IDs and delete them directly,
			// avoiding the need to load all tracks into memory.
			positions := make([]string, len(idxToRemove))
			for i, idx := range idxToRemove {
				positions[i] = strconv.Itoa(idx + 1)
			}
			if err := tracksRepo.Delete(positions...); err != nil {
				return err
			}
			if len(idsToAdd) > 0 {
				if _, err := tracksRepo.Add(idsToAdd); err != nil {
					return err
				}
			}
			return s.updateMetadata(ctx, tx, pls, name, comment, public)
		}

		if len(idsToAdd) > 0 {
			if _, err := repo.Tracks(playlistID, false).Add(idsToAdd); err != nil {
				return err
			}
		}
		if name == nil && comment == nil && public == nil {
			return nil
		}
		// Reuse the playlist from checkWritable (no tracks loaded, so Put only refreshes counters)
		return s.updateMetadata(ctx, tx, pls, name, comment, public)
	})
}

// --- Permission helpers ---

// checkWritable fetches the playlist and verifies the current user can modify it.
func (s *playlists) checkWritable(ctx context.Context, id string) (*model.Playlist, error) {
	pls, err := s.ds.Playlist(ctx).Get(id)
	if err != nil {
		return nil, err
	}
	usr, _ := request.UserFrom(ctx)
	if !usr.IsAdmin && pls.OwnerID != usr.ID {
		return nil, model.ErrNotAuthorized
	}
	return pls, nil
}

// checkTracksEditable verifies the user can modify tracks (ownership + not smart playlist).
func (s *playlists) checkTracksEditable(ctx context.Context, playlistID string) (*model.Playlist, error) {
	pls, err := s.checkWritable(ctx, playlistID)
	if err != nil {
		return nil, err
	}
	if pls.IsSmartPlaylist() {
		return nil, model.ErrNotAuthorized
	}
	return pls, nil
}

// updateMetadata applies optional metadata changes to a playlist and persists it.
// Accepts a DataStore parameter so it can be used inside transactions.
// The caller is responsible for permission checks.
func (s *playlists) updateMetadata(ctx context.Context, ds model.DataStore, pls *model.Playlist, name *string, comment *string, public *bool) error {
	if name != nil {
		pls.Name = *name
	}
	if comment != nil {
		pls.Comment = *comment
	}
	if public != nil {
		pls.Public = *public
	}
	return ds.Playlist(ctx).Put(pls)
}

// --- Track management operations ---

func (s *playlists) AddTracks(ctx context.Context, playlistID string, ids []string) (int, error) {
	if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
		return 0, err
	}
	return s.ds.Playlist(ctx).Tracks(playlistID, false).Add(ids)
}

func (s *playlists) AddAlbums(ctx context.Context, playlistID string, albumIds []string) (int, error) {
	if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
		return 0, err
	}
	return s.ds.Playlist(ctx).Tracks(playlistID, false).AddAlbums(albumIds)
}

func (s *playlists) AddArtists(ctx context.Context, playlistID string, artistIds []string) (int, error) {
	if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
		return 0, err
	}
	return s.ds.Playlist(ctx).Tracks(playlistID, false).AddArtists(artistIds)
}

func (s *playlists) AddDiscs(ctx context.Context, playlistID string, discs []model.DiscID) (int, error) {
	if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
		return 0, err
	}
	return s.ds.Playlist(ctx).Tracks(playlistID, false).AddDiscs(discs)
}

func (s *playlists) RemoveTracks(ctx context.Context, playlistID string, trackIds []string) error {
	if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
		return err
	}
	return s.ds.WithTx(func(tx model.DataStore) error {
		return tx.Playlist(ctx).Tracks(playlistID, false).Delete(trackIds...)
	})
}

func (s *playlists) ReorderTrack(ctx context.Context, playlistID string, pos int, newPos int) error {
	if _, err := s.checkTracksEditable(ctx, playlistID); err != nil {
		return err
	}
	return s.ds.WithTx(func(tx model.DataStore) error {
		return tx.Playlist(ctx).Tracks(playlistID, false).Reorder(pos, newPos)
	})
}

// --- Cover art operations ---

func (s *playlists) SetImage(ctx context.Context, playlistID string, reader io.Reader, ext string) error {
	pls, err := s.checkWritable(ctx, playlistID)
	if err != nil {
		return err
	}

	oldPath := pls.UploadedImagePath()
	filename, err := s.imgUpload.SetImage(ctx, consts.EntityPlaylist, pls.ID, pls.Name, oldPath, reader, ext)
	if err != nil {
		return err
	}

	pls.UploadedImage = filename
	return s.ds.Playlist(ctx).Put(pls)
}

func (s *playlists) RemoveImage(ctx context.Context, playlistID string) error {
	pls, err := s.checkWritable(ctx, playlistID)
	if err != nil {
		return err
	}

	if err := s.imgUpload.RemoveImage(ctx, pls.UploadedImagePath()); err != nil {
		return err
	}

	pls.UploadedImage = ""
	return s.ds.Playlist(ctx).Put(pls)
}
