package mongodb

import (
	"encoding/json"
	"sync"

	"github.com/tidwall/gjson"

	"corelab.mkcl.org/MKCLOS/coredevelopmentplatform/corepkgv2/errormdl"
	"corelab.mkcl.org/MKCLOS/coredevelopmentplatform/corepkgv2/loggermdl"

	"corelab.mkcl.org/MKCLOS/coredevelopmentplatform/corepkgv2/configmdl"

	mgo "gopkg.in/mgo.v2"
)

type mongoHost struct {
	Server   string
	Username string
	Password string
	Database string
}

type tomlConfig struct {
	MongoHosts map[string]mongoHost
}

var instances map[string]*mgo.Session
var sessionError error
var once sync.Once
var config tomlConfig
var defaultHost string

// Init initializes Mongo Connections for give toml file
func Init(tomlFilepath, defaultHostName string) error {
	once.Do(func() {
		instances = make(map[string]*mgo.Session)
		_, err := configmdl.InitConfig(tomlFilepath, &config)
		if errormdl.CheckErr(err) != nil {
			loggermdl.LogError(err)
			sessionError = err
			return
		}
		for hostName, hostDetails := range config.MongoHosts {
			session, err := mgo.DialWithInfo(&mgo.DialInfo{
				Addrs:    []string{hostDetails.Server},
				Username: hostDetails.Username,
				Password: hostDetails.Password,
				Database: hostDetails.Database,
			})
			if err != nil {
				sessionError = err
				return
			}
			instances[hostName] = session
		}
		defaultHost = defaultHostName
	})
	return sessionError
}

//getMongoConnection method
func getMongoConnection(hostName string) (*mgo.Session, error) {
	if instances == nil {
		return nil, errormdl.Wrap("MONGO_INIT_NOT_DONE")
	}
	if hostName == "" {
		if instance, ok := instances[defaultHost]; ok {
			return instance, nil
		}
	}
	if instance, ok := instances[hostName]; ok {
		return instance, nil
	}
	return nil, errormdl.Wrap("Session not found for instance: " + hostName)
}

// MongoDAO mongo DAO struct
type MongoDAO struct {
	hostName       string
	collectionName string
}

// GetMongoDAOWithHost return mongo DAO instance
func GetMongoDAOWithHost(host, collection string) *MongoDAO {
	return &MongoDAO{
		hostName:       host,
		collectionName: collection,
	}
}

// GetMongoDAO return mongo DAO instance
func GetMongoDAO(collection string) *MongoDAO {
	return &MongoDAO{
		collectionName: collection,
	}
}

// SaveData Save data in mongo db
func (mg *MongoDAO) SaveData(data interface{}) error {
	session, sessionError := getMongoConnection(mg.hostName)
	if errormdl.CheckErr(sessionError) != nil {
		return errormdl.CheckErr(sessionError)
	}
	if mg.hostName == "" {
		mg.hostName = defaultHost
	}
	db, ok := config.MongoHosts[mg.hostName]
	if errormdl.CheckBool(!ok) {
		return errormdl.Wrap("No_Configuration_Found_For_Host: " + mg.hostName)
	}
	collection := session.DB(db.Database).C(mg.collectionName)
	insertError := collection.Insert(data)
	if errormdl.CheckErr1(insertError) != nil {
		return errormdl.CheckErr1(insertError)
	}
	return nil
}

// UpdateAll update all
func (mg *MongoDAO) UpdateAll(selector map[string]interface{}, data interface{}) error {
	session, sessionError := getMongoConnection(mg.hostName)
	if errormdl.CheckErr(sessionError) != nil {
		return errormdl.CheckErr(sessionError)
	}
	if mg.hostName == "" {
		mg.hostName = defaultHost
	}
	db, ok := config.MongoHosts[mg.hostName]
	if !ok {
		return errormdl.Wrap("No_Configuration_Found_For_Host: " + mg.hostName)
	}
	collection := session.DB(db.Database).C(mg.collectionName)
	_, updateError := collection.UpdateAll(selector, data)
	if errormdl.CheckErr1(updateError) != nil {
		return errormdl.CheckErr1(updateError)
	}
	return nil
}

// Update will update single entry
func (mg *MongoDAO) Update(selector map[string]interface{}, data interface{}) error {
	session, sessionError := getMongoConnection(mg.hostName)
	if errormdl.CheckErr(sessionError) != nil {
		return errormdl.CheckErr(sessionError)
	}
	if mg.hostName == "" {
		mg.hostName = defaultHost
	}
	db, ok := config.MongoHosts[mg.hostName]
	if !ok {
		return errormdl.Wrap("No_Configuration_Found_For_Host: " + mg.hostName)
	}
	collection := session.DB(db.Database).C(mg.collectionName)
	updateError := collection.Update(selector, data)
	if errormdl.CheckErr1(updateError) != nil {
		return errormdl.CheckErr1(updateError)
	}
	return nil
}

// GetData will return query for selector
func (mg *MongoDAO) GetData(selector map[string]interface{}) (*gjson.Result, error) {
	session, sessionError := getMongoConnection(mg.hostName)
	if errormdl.CheckErr(sessionError) != nil {
		return nil, errormdl.CheckErr(sessionError)
	}
	if mg.hostName == "" {
		mg.hostName = defaultHost
	}
	db, ok := config.MongoHosts[mg.hostName]
	if !ok {
		return nil, errormdl.Wrap("No_Configuration_Found_For_Host: " + mg.hostName)
	}
	collection := session.DB(db.Database).C(mg.collectionName)
	var result []interface{}
	collection.Find(selector).All(&result)
	ba, marshalError := json.Marshal(result)
	if errormdl.CheckErr2(marshalError) != nil {
		return nil, errormdl.CheckErr2(marshalError)
	}
	rs := gjson.ParseBytes(ba)
	return &rs, nil
}

// DeleteData will delete data given for selector
func (mg *MongoDAO) DeleteData(selector map[string]interface{}) error {
	session, sessionError := getMongoConnection(mg.hostName)
	if errormdl.CheckErr(sessionError) != nil {
		return errormdl.CheckErr(sessionError)
	}
	if mg.hostName == "" {
		mg.hostName = defaultHost
	}
	db, ok := config.MongoHosts[mg.hostName]
	if !ok {
		return errormdl.Wrap("No_Configuration_Found_For_Host: " + mg.hostName)
	}
	collection := session.DB(db.Database).C(mg.collectionName)
	deleteError := collection.Remove(selector)
	if errormdl.CheckErr1(deleteError) != nil {
		return errormdl.CheckErr1(deleteError)
	}
	return deleteError
}

// DeleteAll will delete all the matching data given for selector
func (mg *MongoDAO) DeleteAll(selector map[string]interface{}) error {
	session, sessionError := getMongoConnection(mg.hostName)
	if errormdl.CheckErr(sessionError) != nil {
		return errormdl.CheckErr(sessionError)
	}
	if mg.hostName == "" {
		mg.hostName = defaultHost
	}
	db, ok := config.MongoHosts[mg.hostName]
	if !ok {
		return errormdl.Wrap("No_Configuration_Found_For_Host: " + mg.hostName)
	}
	collection := session.DB(db.Database).C(mg.collectionName)
	_, deleteError := collection.RemoveAll(selector)
	if errormdl.CheckErr1(deleteError) != nil {
		return errormdl.CheckErr1(deleteError)
	}
	return deleteError
}