// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

package unified

import (
	"fmt"

	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/internal/bsonutil"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"go.mongodb.org/mongo-driver/v2/x/mongo/driver/xoptions"
)

// newMissingArgumentError creates an error to convey that an argument that is required to run an operation is missing
// from the operation's arguments document.
func newMissingArgumentError(arg string) error {
	return fmt.Errorf("operation arguments document is missing required field %q", arg)
}

type updateArguments struct {
	filter bson.Raw
	update any
}

func createUpdateManyArguments(args bson.Raw) (*updateArguments, *options.UpdateManyOptionsBuilder, error) {
	ua := &updateArguments{}
	opts := options.UpdateMany()

	elems, _ := args.Elements()
	for _, elem := range elems {
		key := elem.Key()
		val := elem.Value()

		switch key {
		case "arrayFilters":
			opts.SetArrayFilters(
				bsonutil.RawToInterfaces(bsonutil.RawArrayToDocuments(val.Array())...),
			)
		case "bypassDocumentValidation":
			opts.SetBypassDocumentValidation(val.Boolean())
		case "collation":
			collation, err := createCollation(val.Document())
			if err != nil {
				return nil, nil, fmt.Errorf("error creating collation: %w", err)
			}
			opts.SetCollation(collation)
		case "comment":
			opts.SetComment(val)
		case "filter":
			ua.filter = val.Document()
		case "hint":
			hint, err := createHint(val)
			if err != nil {
				return nil, nil, fmt.Errorf("error creating hint: %w", err)
			}
			opts.SetHint(hint)
		case "let":
			opts.SetLet(val.Document())
		case "update":
			var err error
			ua.update, err = createUpdateValue(val)
			if err != nil {
				return nil, nil, fmt.Errorf("error processing update value: %w", err)
			}
		case "upsert":
			opts.SetUpsert(val.Boolean())
		case "rawData":
			err := xoptions.SetInternalUpdateManyOptions(opts, key, val.Boolean())
			if err != nil {
				return nil, nil, err
			}
		default:
			return nil, nil, fmt.Errorf("unrecognized update option %q", key)
		}
	}
	if ua.filter == nil {
		return nil, nil, newMissingArgumentError("filter")
	}
	if ua.update == nil {
		return nil, nil, newMissingArgumentError("update")
	}

	return ua, opts, nil
}

func createUpdateOneArguments(args bson.Raw) (*updateArguments, *options.UpdateOneOptionsBuilder, error) {
	ua := &updateArguments{}
	opts := options.UpdateOne()

	elems, _ := args.Elements()
	for _, elem := range elems {
		key := elem.Key()
		val := elem.Value()

		switch key {
		case "arrayFilters":
			opts.SetArrayFilters(
				bsonutil.RawToInterfaces(bsonutil.RawArrayToDocuments(val.Array())...),
			)
		case "bypassDocumentValidation":
			opts.SetBypassDocumentValidation(val.Boolean())
		case "collation":
			collation, err := createCollation(val.Document())
			if err != nil {
				return nil, nil, fmt.Errorf("error creating collation: %w", err)
			}
			opts.SetCollation(collation)
		case "comment":
			opts.SetComment(val)
		case "filter":
			ua.filter = val.Document()
		case "hint":
			hint, err := createHint(val)
			if err != nil {
				return nil, nil, fmt.Errorf("error creating hint: %w", err)
			}
			opts.SetHint(hint)
		case "let":
			opts.SetLet(val.Document())
		case "update":
			var err error
			ua.update, err = createUpdateValue(val)
			if err != nil {
				return nil, nil, fmt.Errorf("error processing update value: %w", err)
			}
		case "upsert":
			opts.SetUpsert(val.Boolean())
		case "sort":
			opts.SetSort(val.Document())
		case "rawData":
			err := xoptions.SetInternalUpdateOneOptions(opts, key, val.Boolean())
			if err != nil {
				return nil, nil, err
			}
		default:
			return nil, nil, fmt.Errorf("unrecognized update option %q", key)
		}
	}
	if ua.filter == nil {
		return nil, nil, newMissingArgumentError("filter")
	}
	if ua.update == nil {
		return nil, nil, newMissingArgumentError("update")
	}

	return ua, opts, nil
}

type listCollectionsArguments struct {
	filter bson.Raw
	opts   *options.ListCollectionsOptionsBuilder
}

func createListCollectionsArguments(args bson.Raw) (*listCollectionsArguments, error) {
	lca := &listCollectionsArguments{
		opts: options.ListCollections(),
	}

	lca.filter = emptyDocument
	elems, _ := args.Elements()
	for _, elem := range elems {
		key := elem.Key()
		val := elem.Value()

		switch key {
		case "batchSize":
			lca.opts.SetBatchSize(val.Int32())
		case "filter":
			lca.filter = val.Document()
		case "nameOnly":
			lca.opts.SetNameOnly(val.Boolean())
		case "rawData":
			err := xoptions.SetInternalListCollectionsOptions(lca.opts, key, val.Boolean())
			if err != nil {
				return nil, err
			}
		default:
			return nil, fmt.Errorf("unrecognized listCollections option %q", key)
		}
	}

	return lca, nil
}

func createCollation(args bson.Raw) (*options.Collation, error) {
	var collation options.Collation
	elems, _ := args.Elements()

	for _, elem := range elems {
		switch elem.Key() {
		case "locale":
			collation.Locale = elem.Value().StringValue()
		case "caseLevel":
			collation.CaseLevel = elem.Value().Boolean()
		case "caseFirst":
			collation.CaseFirst = elem.Value().StringValue()
		case "strength":
			collation.Strength = int(elem.Value().Int32())
		case "numericOrdering":
			collation.NumericOrdering = elem.Value().Boolean()
		case "alternate":
			collation.Alternate = elem.Value().StringValue()
		case "maxVariable":
			collation.MaxVariable = elem.Value().StringValue()
		case "normalization":
			collation.Normalization = elem.Value().Boolean()
		case "backwards":
			collation.Backwards = elem.Value().Boolean()
		default:
			return nil, fmt.Errorf("unrecognized collation option %q", elem.Key())
		}
	}
	return &collation, nil
}

func createHint(val bson.RawValue) (any, error) {
	var hint any

	switch val.Type {
	case bson.TypeString:
		hint = val.StringValue()
	case bson.TypeEmbeddedDocument:
		hint = val.Document()
	default:
		return nil, fmt.Errorf("unrecognized hint value type %s", val.Type)
	}
	return hint, nil
}
