package store

import (
	"context"
	"errors"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	// up to you:
	// "go.mongodb.org/mongo-driver/mongo/options"
)

type Movie struct {
	ID          primitive.ObjectID `json:"_id" bson:"_id"` /* you need the bson:"_id" to be able to retrieve with ID filled */
	Name        string             `json:"name"`
	Cover       string             `json:"cover"`
	Description string             `json:"description"`
}

type MovieService interface {
	GetAll(ctx context.Context) ([]Movie, error)
	GetByID(ctx context.Context, id string) (Movie, error)
	Create(ctx context.Context, m *Movie) error
	Update(ctx context.Context, id string, m Movie) error
	Delete(ctx context.Context, id string) error
}

type movieService struct {
	C *mongo.Collection
}

var _ MovieService = (*movieService)(nil)

func NewMovieService(collection *mongo.Collection) MovieService {
	// up to you:
	// indexOpts := new(options.IndexOptions)
	// indexOpts.SetName("movieIndex").
	// 	SetUnique(true).
	// 	SetBackground(true).
	// 	SetSparse(true)

	// collection.Indexes().CreateOne(context.Background(), mongo.IndexModel{
	// 	Keys:    []string{"_id", "name"},
	// 	Options: indexOpts,
	// })

	return &movieService{C: collection}
}

func (s *movieService) GetAll(ctx context.Context) ([]Movie, error) {
	// Note:
	// The mongodb's go-driver's docs says that you can pass `nil` to "find all" but this gives NilDocument error,
	// probably it's a bug or a documentation's mistake, you have to pass `bson.D{}` instead.
	cur, err := s.C.Find(ctx, bson.D{})
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)

	var results []Movie

	for cur.Next(ctx) {
		if err = cur.Err(); err != nil {
			return nil, err
		}

		//	elem := bson.D{}
		var elem Movie
		err = cur.Decode(&elem)
		if err != nil {
			return nil, err
		}

		// results = append(results, Movie{ID: elem[0].Value.(primitive.ObjectID)})

		results = append(results, elem)
	}

	return results, nil
}

func matchID(id string) (bson.D, error) {
	objectID, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return nil, err
	}

	filter := bson.D{{Key: "_id", Value: objectID}}
	return filter, nil
}

var ErrNotFound = errors.New("not found")

func (s *movieService) GetByID(ctx context.Context, id string) (Movie, error) {
	var movie Movie
	filter, err := matchID(id)
	if err != nil {
		return movie, err
	}

	err = s.C.FindOne(ctx, filter).Decode(&movie)
	if err == mongo.ErrNoDocuments {
		return movie, ErrNotFound
	}
	return movie, err
}

func (s *movieService) Create(ctx context.Context, m *Movie) error {
	if m.ID.IsZero() {
		m.ID = primitive.NewObjectID()
	}

	_, err := s.C.InsertOne(ctx, m)
	if err != nil {
		return err
	}

	// The following doesn't work if you have the `bson:"_id` on Movie.ID field,
	// therefore we manually generate a new ID (look above).
	// res, err := ...InsertOne
	// objectID := res.InsertedID.(primitive.ObjectID)
	// m.ID = objectID
	return nil
}

func (s *movieService) Update(ctx context.Context, id string, m Movie) error {
	filter, err := matchID(id)
	if err != nil {
		return err
	}

	// update := bson.D{
	// 	{Key: "$set", Value: m},
	// }
	// ^ this will override all fields, you can do that, depending on your design. but let's check each field:
	elem := bson.D{}

	if m.Name != "" {
		elem = append(elem, bson.E{Key: "name", Value: m.Name})
	}

	if m.Description != "" {
		elem = append(elem, bson.E{Key: "description", Value: m.Description})
	}

	if m.Cover != "" {
		elem = append(elem, bson.E{Key: "cover", Value: m.Cover})
	}

	update := bson.D{
		{Key: "$set", Value: elem},
	}

	_, err = s.C.UpdateOne(ctx, filter, update)
	if err != nil {
		if err == mongo.ErrNoDocuments {
			return ErrNotFound
		}
		return err
	}

	return nil
}

func (s *movieService) Delete(ctx context.Context, id string) error {
	filter, err := matchID(id)
	if err != nil {
		return err
	}
	_, err = s.C.DeleteOne(ctx, filter)
	if err != nil {
		if err == mongo.ErrNoDocuments {
			return ErrNotFound
		}
		return err
	}

	return nil
}