package persistence

import (
	"database/sql"
	"errors"
	"fmt"
	"time"

	. "github.com/Masterminds/squirrel"
	"github.com/navidrome/navidrome/log"
	"github.com/navidrome/navidrome/model"
	"github.com/navidrome/navidrome/model/request"
)

const bookmarkTable = "bookmark"

func (r sqlRepository) withBookmark(query SelectBuilder, idField string) SelectBuilder {
	if userId(r.ctx) == invalidUserId {
		return query
	}
	return query.
		LeftJoin("bookmark on (" +
			"bookmark.item_id = " + idField +
			// item_ids are unique across different item_types, so the clause below is not needed
			//" AND bookmark.item_type = '" + r.tableName + "'" +
			" AND bookmark.user_id = '" + userId(r.ctx) + "')").
		Columns("coalesce(position, 0) as bookmark_position")
}

func (r sqlRepository) bmkID(itemID ...string) And {
	return And{
		Eq{bookmarkTable + ".user_id": userId(r.ctx)},
		Eq{bookmarkTable + ".item_type": r.tableName},
		Eq{bookmarkTable + ".item_id": itemID},
	}
}

func (r sqlRepository) bmkUpsert(itemID, comment string, position int64) error {
	client, _ := request.ClientFrom(r.ctx)
	user, _ := request.UserFrom(r.ctx)
	values := map[string]interface{}{
		"comment":    comment,
		"position":   position,
		"updated_at": time.Now(),
		"changed_by": client,
	}

	upd := Update(bookmarkTable).Where(r.bmkID(itemID)).SetMap(values)
	c, err := r.executeSQL(upd)
	if err == nil {
		log.Debug(r.ctx, "Updated bookmark", "id", itemID, "user", user.UserName, "position", position, "comment", comment)
	}
	if c == 0 || errors.Is(err, sql.ErrNoRows) {
		values["user_id"] = user.ID
		values["item_type"] = r.tableName
		values["item_id"] = itemID
		values["created_at"] = time.Now()
		values["updated_at"] = time.Now()
		ins := Insert(bookmarkTable).SetMap(values)
		_, err = r.executeSQL(ins)
		if err != nil {
			return err
		}
		log.Debug(r.ctx, "Added bookmark", "id", itemID, "user", user.UserName, "position", position, "comment", comment)
	}

	return err
}

func (r sqlRepository) AddBookmark(id, comment string, position int64) error {
	user, _ := request.UserFrom(r.ctx)
	err := r.bmkUpsert(id, comment, position)
	if err != nil {
		log.Error(r.ctx, "Error adding bookmark", "id", id, "user", user.UserName, "position", position, "comment", comment)
	}
	return err
}

func (r sqlRepository) DeleteBookmark(id string) error {
	user, _ := request.UserFrom(r.ctx)
	del := Delete(bookmarkTable).Where(r.bmkID(id))
	_, err := r.executeSQL(del)
	if err != nil {
		log.Error(r.ctx, "Error removing bookmark", "id", id, "user", user.UserName)
	}
	return err
}

type bookmark struct {
	UserID    string    `json:"user_id"`
	ItemID    string    `json:"item_id"`
	ItemType  string    `json:"item_type"`
	Comment   string    `json:"comment"`
	Position  int64     `json:"position"`
	ChangedBy string    `json:"changed_by"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

func (r sqlRepository) GetBookmarks() (model.Bookmarks, error) {
	user, _ := request.UserFrom(r.ctx)

	idField := r.tableName + ".id"
	sq := r.newSelect().Columns(r.tableName + ".*")
	sq = r.withAnnotation(sq, idField)
	sq = r.withBookmark(sq, idField).Where(NotEq{bookmarkTable + ".item_id": nil})
	var mfs dbMediaFiles // TODO Decouple from media_file
	err := r.queryAll(sq, &mfs)
	if err != nil {
		log.Error(r.ctx, "Error getting mediafiles with bookmarks", "user", user.UserName, err)
		return nil, err
	}

	ids := make([]string, len(mfs))
	mfMap := make(map[string]int)
	for i, mf := range mfs {
		ids[i] = mf.ID
		mfMap[mf.ID] = i
	}

	sq = Select("*").From(bookmarkTable).Where(r.bmkID(ids...))
	var bmks []bookmark
	err = r.queryAll(sq, &bmks)
	if err != nil {
		log.Error(r.ctx, "Error getting bookmarks", "user", user.UserName, "ids", ids, err)
		return nil, err
	}

	resp := make(model.Bookmarks, len(bmks))
	for i, bmk := range bmks {
		if itemIdx, ok := mfMap[bmk.ItemID]; !ok {
			log.Debug(r.ctx, "Invalid bookmark", "id", bmk.ItemID, "user", user.UserName)
			continue
		} else {
			resp[i] = model.Bookmark{
				Comment:   bmk.Comment,
				Position:  bmk.Position,
				CreatedAt: bmk.CreatedAt,
				UpdatedAt: bmk.UpdatedAt,
				ChangedBy: bmk.ChangedBy,
				Item:      *mfs[itemIdx].MediaFile,
			}
		}
	}
	return resp, nil
}

func (r sqlRepository) cleanBookmarks() error {
	del := Delete(bookmarkTable).Where(Eq{"item_type": r.tableName}).Where("item_id not in (select id from " + r.tableName + ")")
	c, err := r.executeSQL(del)
	if err != nil {
		return fmt.Errorf("error cleaning up bookmarks: %w", err)
	}
	if c > 0 {
		log.Debug(r.ctx, "Clean-up bookmarks", "totalDeleted", c)
	}
	return nil
}
