// Copyright (C) MongoDB, Inc. 2014-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 mongodump

import (
	"fmt"
	"io"

	"github.com/mongodb/mongo-tools/common/bsonutil"
	"github.com/mongodb/mongo-tools/common/db"
	"github.com/mongodb/mongo-tools/common/intents"
	"github.com/mongodb/mongo-tools/common/json"
	"github.com/mongodb/mongo-tools/common/log"
	"gopkg.in/mgo.v2/bson"
)

// Metadata holds information about a collection's options and indexes.
type Metadata struct {
	Options interface{}   `json:"options,omitempty"`
	Indexes []interface{} `json:"indexes"`
	UUID    string        `json:"uuid,omitempty"`
}

// IndexDocumentFromDB is used internally to preserve key ordering.
type IndexDocumentFromDB struct {
	Options bson.M `bson:",inline"`
	Key     bson.D `bson:"key"`
}

// dumpMetadata gets the metadata for a collection and writes it
// in readable JSON format.
func (dump *MongoDump) dumpMetadata(intent *intents.Intent, buffer resettableOutputBuffer) (err error) {

	meta := Metadata{
		// We have to initialize Indexes to an empty slice, not nil, so that an empty
		// array is marshalled into json instead of null. That is, {indexes:[]} is okay
		// but {indexes:null} will cause assertions in our legacy C++ mongotools
		Indexes: []interface{}{},
	}

	// The collection options were already gathered while building the list of intents.
	// We convert them to JSON so that they can be written to the metadata json file as text.
	if intent.Options != nil {
		if meta.Options, err = bsonutil.ConvertBSONValueToJSON(*intent.Options); err != nil {
			return fmt.Errorf("error converting collection options to JSON: %v", err)
		}
	} else {
		meta.Options = nil
	}

	// If a collection has a UUID, it was gathered while building the list of
	// intents.  Otherwise, it will be the empty string.
	meta.UUID = intent.UUID

	// Second, we read the collection's index information by either calling
	// listIndexes (pre-2.7 systems) or querying system.indexes.
	// We keep a running list of all the indexes
	// for the current collection as we iterate over the cursor, and include
	// that list as the "indexes" field of the metadata document.
	log.Logvf(log.DebugHigh, "\treading indexes for `%v`", intent.Namespace())

	session, err := dump.SessionProvider.GetSession()
	if err != nil {
		return err
	}
	defer session.Close()

	if intent.IsView() {
		log.Logvf(log.DebugLow, "not dumping indexes metadata for '%v' because it is a view", intent.Namespace())
	} else {
		// get the indexes
		indexesIter, err := db.GetIndexes(session.DB(intent.DB).C(intent.C))
		if err != nil {
			return err
		}
		if indexesIter == nil {
			log.Logvf(log.Always, "the collection %v appears to have been dropped after the dump started", intent.Namespace())
			return nil
		}

		indexOpts := &bson.D{}
		for indexesIter.Next(indexOpts) {
			convertedIndex, err := bsonutil.ConvertBSONValueToJSON(*indexOpts)
			if err != nil {
				return fmt.Errorf("error converting index (%#v): %v", convertedIndex, err)
			}
			meta.Indexes = append(meta.Indexes, convertedIndex)
		}

		if err := indexesIter.Err(); err != nil {
			return fmt.Errorf("error getting indexes for collection `%v`: %v", intent.Namespace(), err)
		}
	}

	// Finally, we send the results to the writer as JSON bytes
	jsonBytes, err := json.Marshal(meta)
	if err != nil {
		return fmt.Errorf("error marshalling metadata json for collection `%v`: %v", intent.Namespace(), err)
	}

	err = intent.MetadataFile.Open()
	if err != nil {
		return err
	}
	defer func() {
		closeErr := intent.MetadataFile.Close()
		if err == nil && closeErr != nil {
			err = fmt.Errorf("error writing metadata for collection `%v` to disk: %v", intent.Namespace(), closeErr)
		}
	}()

	var f io.Writer
	f = intent.MetadataFile
	if buffer != nil {
		buffer.Reset(f)
		f = buffer
		defer func() {
			closeErr := buffer.Close()
			if err == nil && closeErr != nil {
				err = fmt.Errorf("error writing metadata for collection `%v` to disk: %v", intent.Namespace(), closeErr)
			}
		}()
	}
	_, err = f.Write(jsonBytes)
	if err != nil {
		err = fmt.Errorf("error writing metadata for collection `%v` to disk: %v", intent.Namespace(), err)
	}
	return
}
