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

import (
	"fmt"
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
	"strings"
)

// Query flags
const (
	Snapshot = 1 << iota
	LogReplay
	Prefetch
)

type NodeType string

const (
	Mongos     NodeType = "mongos"
	Standalone          = "standalone"
	ReplSet             = "replset"
	Unknown             = "unknown"
)

// CommandRunner exposes functions that can be run against a server
type CommandRunner interface {
	Run(command interface{}, out interface{}, database string) error
	FindOne(db, collection string, skip int, query interface{}, sort []string, into interface{}, opts int) error
	Remove(db, collection string, query interface{}) error
	DatabaseNames() ([]string, error)
	CollectionNames(db string) ([]string, error)
}

// Remove removes all documents matched by query q in the db database and c collection.
func (sp *SessionProvider) Remove(db, c string, q interface{}) error {
	session, err := sp.GetSession()
	if err != nil {
		return err
	}
	defer session.Close()
	_, err = session.DB(db).C(c).RemoveAll(q)
	return err
}

// Run issues the provided command on the db database and unmarshals its result
// into out.
func (sp *SessionProvider) Run(command interface{}, out interface{}, db string) error {
	session, err := sp.GetSession()
	if err != nil {
		return err
	}
	defer session.Close()
	return session.DB(db).Run(command, out)
}

// DatabaseNames returns a slice containing the names of all the databases on the
// connected server.
func (sp *SessionProvider) DatabaseNames() ([]string, error) {
	session, err := sp.GetSession()
	if err != nil {
		return nil, err
	}
	session.SetSocketTimeout(0)
	defer session.Close()
	return session.DatabaseNames()
}

// CollectionNames returns the names of all the collections in the dbName database.
func (sp *SessionProvider) CollectionNames(dbName string) ([]string, error) {
	session, err := sp.GetSession()
	if err != nil {
		return nil, err
	}
	defer session.Close()
	session.SetSocketTimeout(0)
	return session.DB(dbName).CollectionNames()
}

// GetNodeType checks if the connected SessionProvider is a mongos, standalone, or replset,
// by looking at the result of calling isMaster.
func (sp *SessionProvider) GetNodeType() (NodeType, error) {
	session, err := sp.GetSession()
	if err != nil {
		return Unknown, err
	}
	session.SetSocketTimeout(0)
	defer session.Close()
	masterDoc := struct {
		SetName interface{} `bson:"setName"`
		Hosts   interface{} `bson:"hosts"`
		Msg     string      `bson:"msg"`
	}{}
	err = session.Run("isMaster", &masterDoc)
	if err != nil {
		return Unknown, err
	}

	if masterDoc.SetName != nil || masterDoc.Hosts != nil {
		return ReplSet, nil
	} else if masterDoc.Msg == "isdbgrid" {
		// isdbgrid is always the msg value when calling isMaster on a mongos
		// see http://docs.mongodb.org/manual/core/sharded-cluster-query-router/
		return Mongos, nil
	}
	return Standalone, nil
}

// IsReplicaSet returns a boolean which is true if the connected server is part
// of a replica set.
func (sp *SessionProvider) IsReplicaSet() (bool, error) {
	nodeType, err := sp.GetNodeType()
	if err != nil {
		return false, err
	}
	return nodeType == ReplSet, nil
}

// IsMongos returns true if the connected server is a mongos.
func (sp *SessionProvider) IsMongos() (bool, error) {
	nodeType, err := sp.GetNodeType()
	if err != nil {
		return false, err
	}
	return nodeType == Mongos, nil
}

// SupportsRepairCursor takes in an example db and collection name and
// returns true if the connected server supports the repairCursor command.
// It returns false and the error that occurred if it is not supported.
func (sp *SessionProvider) SupportsRepairCursor(db, collection string) (bool, error) {
	session, err := sp.GetSession()
	if err != nil {
		return false, err
	}
	session.SetSocketTimeout(0)
	defer session.Close()

	// This check is slightly hacky, but necessary to allow users to run repair without
	// permissions to all collections. There are multiple reasons a repair command could fail,
	// but we are only interested in the ones that imply that the repair command is not
	// usable by the connected server. If we do not get one of these specific error messages,
	// we will let the error happen again later.
	repairIter := session.DB(db).C(collection).Repair()
	repairIter.Next(bson.D{})
	err = repairIter.Err()
	if err == nil {
		return true, nil
	}
	if strings.Index(err.Error(), "no such cmd: repairCursor") > -1 {
		// return a helpful error message for early server versions
		return false, fmt.Errorf("--repair flag cannot be used on mongodb versions before 2.7.8")
	}
	if strings.Index(err.Error(), "repair iterator not supported") > -1 {
		// helpful error message if the storage engine does not support repair (WiredTiger)
		return false, fmt.Errorf("--repair is not supported by the connected storage engine")
	}

	return true, nil
}

// SupportsWriteCommands returns true if the connected server supports write
// commands, returns false otherwise.
func (sp *SessionProvider) SupportsWriteCommands() (bool, error) {
	session, err := sp.GetSession()
	if err != nil {
		return false, err
	}
	session.SetSocketTimeout(0)
	defer session.Close()
	masterDoc := struct {
		Ok      int `bson:"ok"`
		MaxWire int `bson:"maxWireVersion"`
	}{}
	err = session.Run("isMaster", &masterDoc)
	if err != nil {
		return false, err
	}
	// the connected server supports write commands if
	// the maxWriteVersion field is present
	return (masterDoc.Ok == 1 && masterDoc.MaxWire >= 2), nil
}

// FindOne retuns the first document in the collection and database that matches
// the query after skip, sort and query flags are applied.
func (sp *SessionProvider) FindOne(db, collection string, skip int, query interface{}, sort []string, into interface{}, flags int) error {
	session, err := sp.GetSession()
	if err != nil {
		return err
	}
	defer session.Close()

	q := session.DB(db).C(collection).Find(query).Sort(sort...).Skip(skip)
	q = ApplyFlags(q, session, flags)
	return q.One(into)
}

// ApplyFlags applies flags to the given query session.
func ApplyFlags(q *mgo.Query, session *mgo.Session, flags int) *mgo.Query {
	if flags&Snapshot > 0 {
		q = q.Snapshot()
	}
	if flags&LogReplay > 0 {
		q = q.LogReplay()
	}
	if flags&Prefetch > 0 {
		session.SetPrefetch(1.0)
	}
	return q
}
