// 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 mongo

import (
	"fmt"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/x/mongo/driver/operation"
)

// BulkWriteResult is the result type returned by a BulkWrite operation.
type BulkWriteResult struct {
	// The number of documents inserted.
	InsertedCount int64

	// The number of documents matched by filters in update and replace operations.
	MatchedCount int64

	// The number of documents modified by update and replace operations.
	ModifiedCount int64

	// The number of documents deleted.
	DeletedCount int64

	// The number of documents upserted by update and replace operations.
	UpsertedCount int64

	// A map of operation index to the _id of each upserted document.
	UpsertedIDs map[int64]interface{}
}

// InsertOneResult is the result type returned by an InsertOne operation.
type InsertOneResult struct {
	// The _id of the inserted document. A value generated by the driver will be of type primitive.ObjectID.
	InsertedID interface{}
}

// InsertManyResult is a result type returned by an InsertMany operation.
type InsertManyResult struct {
	// The _id values of the inserted documents. Values generated by the driver will be of type primitive.ObjectID.
	InsertedIDs []interface{}
}

// TODO(GODRIVER-2367): Remove the BSON struct tags on DeleteResult.

// DeleteResult is the result type returned by DeleteOne and DeleteMany operations.
type DeleteResult struct {
	DeletedCount int64 `bson:"n"` // The number of documents deleted.
}

// RewrapManyDataKeyResult is the result of the bulk write operation used to update the key vault collection with
// rewrapped data keys.
type RewrapManyDataKeyResult struct {
	*BulkWriteResult
}

// ListDatabasesResult is a result of a ListDatabases operation.
type ListDatabasesResult struct {
	// A slice containing one DatabaseSpecification for each database matched by the operation's filter.
	Databases []DatabaseSpecification

	// The total size of the database files of the returned databases in bytes.
	// This will be the sum of the SizeOnDisk field for each specification in Databases.
	TotalSize int64
}

func newListDatabasesResultFromOperation(res operation.ListDatabasesResult) ListDatabasesResult {
	var ldr ListDatabasesResult
	ldr.Databases = make([]DatabaseSpecification, 0, len(res.Databases))
	for _, spec := range res.Databases {
		ldr.Databases = append(
			ldr.Databases,
			DatabaseSpecification{Name: spec.Name, SizeOnDisk: spec.SizeOnDisk, Empty: spec.Empty},
		)
	}
	ldr.TotalSize = res.TotalSize
	return ldr
}

// DatabaseSpecification contains information for a database. This type is returned as part of ListDatabasesResult.
type DatabaseSpecification struct {
	Name       string // The name of the database.
	SizeOnDisk int64  // The total size of the database files on disk in bytes.
	Empty      bool   // Specfies whether or not the database is empty.
}

// UpdateResult is the result type returned from UpdateOne, UpdateMany, and ReplaceOne operations.
type UpdateResult struct {
	MatchedCount  int64       // The number of documents matched by the filter.
	ModifiedCount int64       // The number of documents modified by the operation.
	UpsertedCount int64       // The number of documents upserted by the operation.
	UpsertedID    interface{} // The _id field of the upserted document, or nil if no upsert was done.
}

// UnmarshalBSON implements the bson.Unmarshaler interface.
//
// Deprecated: Unmarshalling an UpdateResult directly from BSON is not supported and may produce
// different results compared to running Update* operations directly.
func (result *UpdateResult) UnmarshalBSON(b []byte) error {
	// TODO(GODRIVER-2367): Remove the ability to unmarshal BSON directly to an UpdateResult.
	elems, err := bson.Raw(b).Elements()
	if err != nil {
		return err
	}

	for _, elem := range elems {
		switch elem.Key() {
		case "n":
			switch elem.Value().Type {
			case bson.TypeInt32:
				result.MatchedCount = int64(elem.Value().Int32())
			case bson.TypeInt64:
				result.MatchedCount = elem.Value().Int64()
			default:
				return fmt.Errorf("Received invalid type for n, should be Int32 or Int64, received %s", elem.Value().Type)
			}
		case "nModified":
			switch elem.Value().Type {
			case bson.TypeInt32:
				result.ModifiedCount = int64(elem.Value().Int32())
			case bson.TypeInt64:
				result.ModifiedCount = elem.Value().Int64()
			default:
				return fmt.Errorf("Received invalid type for nModified, should be Int32 or Int64, received %s", elem.Value().Type)
			}
		case "upserted":
			switch elem.Value().Type {
			case bson.TypeArray:
				e, err := elem.Value().Array().IndexErr(0)
				if err != nil {
					break
				}
				if e.Value().Type != bson.TypeEmbeddedDocument {
					break
				}
				var d struct {
					ID interface{} `bson:"_id"`
				}
				err = bson.Unmarshal(e.Value().Document(), &d)
				if err != nil {
					return err
				}
				result.UpsertedID = d.ID
			default:
				return fmt.Errorf("Received invalid type for upserted, should be Array, received %s", elem.Value().Type)
			}
		}
	}

	return nil
}

// IndexSpecification represents an index in a database. This type is returned by the IndexView.ListSpecifications
// function and is also used in the CollectionSpecification type.
type IndexSpecification struct {
	// The index name.
	Name string

	// The namespace for the index. This is a string in the format "databaseName.collectionName".
	Namespace string

	// The keys specification document for the index.
	KeysDocument bson.Raw

	// The index version.
	Version int32

	// The length of time, in seconds, for documents to remain in the collection. The default value is 0, which means
	// that documents will remain in the collection until they're explicitly deleted or the collection is dropped.
	ExpireAfterSeconds *int32

	// If true, the index will only reference documents that contain the fields specified in the index. The default is
	// false.
	Sparse *bool

	// If true, the collection will not accept insertion or update of documents where the index key value matches an
	// existing value in the index. The default is false.
	Unique *bool

	// The clustered index.
	Clustered *bool
}

var _ bson.Unmarshaler = (*IndexSpecification)(nil)

type unmarshalIndexSpecification struct {
	Name               string   `bson:"name"`
	Namespace          string   `bson:"ns"`
	KeysDocument       bson.Raw `bson:"key"`
	Version            int32    `bson:"v"`
	ExpireAfterSeconds *int32   `bson:"expireAfterSeconds"`
	Sparse             *bool    `bson:"sparse"`
	Unique             *bool    `bson:"unique"`
	Clustered          *bool    `bson:"clustered"`
}

// UnmarshalBSON implements the bson.Unmarshaler interface.
func (i *IndexSpecification) UnmarshalBSON(data []byte) error {
	var temp unmarshalIndexSpecification
	if err := bson.Unmarshal(data, &temp); err != nil {
		return err
	}

	i.Name = temp.Name
	i.Namespace = temp.Namespace
	i.KeysDocument = temp.KeysDocument
	i.Version = temp.Version
	i.ExpireAfterSeconds = temp.ExpireAfterSeconds
	i.Sparse = temp.Sparse
	i.Unique = temp.Unique
	i.Clustered = temp.Clustered
	return nil
}

// CollectionSpecification represents a collection in a database. This type is returned by the
// Database.ListCollectionSpecifications function.
type CollectionSpecification struct {
	// The collection name.
	Name string

	// The type of the collection. This will either be "collection" or "view".
	Type string

	// Whether or not the collection is readOnly. This will be false for MongoDB versions < 3.4.
	ReadOnly bool

	// The collection UUID. This field will be nil for MongoDB versions < 3.6. For versions 3.6 and higher, this will
	// be a primitive.Binary with Subtype 4.
	UUID *primitive.Binary

	// A document containing the options used to construct the collection.
	Options bson.Raw

	// An IndexSpecification instance with details about the collection's _id index. This will be nil if the NameOnly
	// option is used and for MongoDB versions < 3.4.
	IDIndex *IndexSpecification
}

var _ bson.Unmarshaler = (*CollectionSpecification)(nil)

// unmarshalCollectionSpecification is used to unmarshal BSON bytes from a listCollections command into a
// CollectionSpecification.
type unmarshalCollectionSpecification struct {
	Name string `bson:"name"`
	Type string `bson:"type"`
	Info *struct {
		ReadOnly bool              `bson:"readOnly"`
		UUID     *primitive.Binary `bson:"uuid"`
	} `bson:"info"`
	Options bson.Raw            `bson:"options"`
	IDIndex *IndexSpecification `bson:"idIndex"`
}

// UnmarshalBSON implements the bson.Unmarshaler interface.
func (cs *CollectionSpecification) UnmarshalBSON(data []byte) error {
	var temp unmarshalCollectionSpecification
	if err := bson.Unmarshal(data, &temp); err != nil {
		return err
	}

	cs.Name = temp.Name
	cs.Type = temp.Type
	if cs.Type == "" {
		// The "type" field is only present on 3.4+ because views were introduced in 3.4, so we implicitly set the
		// value to "collection" if it's empty.
		cs.Type = "collection"
	}
	if temp.Info != nil {
		cs.ReadOnly = temp.Info.ReadOnly
		cs.UUID = temp.Info.UUID
	}
	cs.Options = temp.Options
	cs.IDIndex = temp.IDIndex
	return nil
}
