package oclib

import (
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"runtime/debug"

	"cloud.o-forge.io/core/oc-lib/config"
	"cloud.o-forge.io/core/oc-lib/dbs"
	"cloud.o-forge.io/core/oc-lib/dbs/mongo"
	"cloud.o-forge.io/core/oc-lib/logs"
	"cloud.o-forge.io/core/oc-lib/models"
	"cloud.o-forge.io/core/oc-lib/models/collaborative_area"
	"cloud.o-forge.io/core/oc-lib/models/collaborative_area/rules/rule"
	"cloud.o-forge.io/core/oc-lib/models/order"
	"cloud.o-forge.io/core/oc-lib/models/peer"
	"cloud.o-forge.io/core/oc-lib/models/resources"
	"cloud.o-forge.io/core/oc-lib/models/utils"
	w2 "cloud.o-forge.io/core/oc-lib/models/workflow"
	"cloud.o-forge.io/core/oc-lib/models/workflow_execution"
	"cloud.o-forge.io/core/oc-lib/models/workspace"
	"cloud.o-forge.io/core/oc-lib/tools"
	beego "github.com/beego/beego/v2/server/web"
	"github.com/beego/beego/v2/server/web/context"
	"github.com/goraz/onion"
	"github.com/rs/zerolog"
)

type Filters = dbs.Filters

type LibDataEnum int

// init accessible constant to retrieve data from the database
const (
	INVALID             LibDataEnum = iota
	DATA_RESOURCE                   = tools.DATA_RESOURCE
	PROCESSING_RESOURCE             = tools.PROCESSING_RESOURCE
	STORAGE_RESOURCE                = tools.STORAGE_RESOURCE
	COMPUTE_RESOURCE                = tools.COMPUTE_RESOURCE
	WORKFLOW_RESOURCE               = tools.WORKFLOW_RESOURCE
	WORKFLOW                        = tools.WORKFLOW
	WORKSPACE                       = tools.WORKSPACE
	WORKFLOW_EXECUTION              = tools.WORKFLOW_EXECUTION
	PEER                            = tools.PEER
	COLLABORATIVE_AREA              = tools.COLLABORATIVE_AREA
	RULE                            = tools.RULE
	BOOKING                         = tools.BOOKING
	ORDER                           = tools.ORDER
)

// will turn into standards api hostnames
func (d LibDataEnum) API() string {
	return tools.DefaultAPI[d]
}

// will turn into standards name
func (d LibDataEnum) String() string {
	return tools.Str[d]
}

// will turn into enum index
func (d LibDataEnum) EnumIndex() int {
	return int(d)
}

func IsQueryParamsEquals(input *context.BeegoInput, name string, val interface{}) bool {
	path := strings.Split(input.URI(), "?")
	if len(path) >= 2 {
		uri := strings.Split(path[1], "&")
		for _, val := range uri {
			kv := strings.Split(val, "=")
			if kv[0] == name && fmt.Sprintf("%v", val) == kv[1] {
				return true
			}
		}
	}
	return false
}

// model to define the shallow data structure
type LibDataShallow struct {
	Data []utils.ShallowDBObject `bson:"data" json:"data"`
	Code int                     `bson:"code" json:"code"`
	Err  string                  `bson:"error" json:"error"`
}

// model to define the data structure
type LibData struct {
	Data utils.DBObject `bson:"data" json:"data"`
	Code int            `bson:"code" json:"code"`
	Err  string         `bson:"error" json:"error"`
}

func InitDaemon(appName string) {
	config.SetAppName(appName) // set the app name to the logger to define the main log chan
	// create a temporary console logger for init
	logs.SetLogger(logs.CreateLogger("main"))
	// Load the right config file
	o := GetConfLoader()

	// feed the library with the loaded config
	SetConfig(
		o.GetStringDefault("MONGO_URL", "mongodb://127.0.0.1:27017"),
		o.GetStringDefault("MONGO_DATABASE", "DC_myDC"),
		o.GetStringDefault("NATS_URL", "nats://localhost:4222"),
		o.GetStringDefault("LOKI_URL", ""),
		o.GetStringDefault("LOG_LEVEL", "info"),
	)
	// Beego init
	beego.BConfig.AppName = appName
	beego.BConfig.Listen.HTTPPort = o.GetIntDefault("port", 8080)
	beego.BConfig.WebConfig.DirectoryIndex = true
	beego.BConfig.WebConfig.StaticDir["/swagger"] = "swagger"
}

type IDTokenClaims struct {
	UserID string   `json:"user_id"`
	PeerID string   `json:"peer_id"`
	Groups []string `json:"groups"`
}

// SessionClaims struct
type SessionClaims struct {
	AccessToken map[string]interface{} `json:"access_token"`
	IDToken     IDTokenClaims          `json:"id_token"`
}

// Claims struct
type Claims struct {
	Session SessionClaims `json:"session"`
}

func ExtractTokenInfo(request http.Request) (string, string, []string) {
	reqToken := request.Header.Get("Authorization")
	splitToken := strings.Split(reqToken, "Bearer ")
	if len(splitToken) < 2 {
		reqToken = ""
	} else {
		reqToken = splitToken[1]
	}
	if reqToken != "" {
		token := strings.Split(reqToken, ".")
		if len(token) > 2 {
			bytes, err := base64.StdEncoding.DecodeString(token[2])
			if err != nil {
				return "", "", []string{}
			}
			var c Claims
			err = json.Unmarshal(bytes, &c)
			if err != nil {
				return "", "", []string{}
			}
			return c.Session.IDToken.UserID, c.Session.IDToken.PeerID, c.Session.IDToken.Groups
		}
	}
	return "", "", []string{}
}

func Init(appName string) {
	InitDaemon(appName)
	api := &tools.API{}
	api.Discovered(beego.BeeApp.Handlers.GetAllControllerInfo())
}

//
// Expose subpackages
//

/* GetLogger returns the main logger
* @return zerolog.Logger
 */
func GetLogger() zerolog.Logger {
	return logs.GetLogger()
}

/* SetConfig will set the config and create a logger according to app configuration and initialize mongo accessor
* @param url string
* @param database string
* @param natsUrl string
* @param lokiUrl string
* @param logLevel string
* @return *Config
 */
func SetConfig(mongoUrl string, database string, natsUrl string, lokiUrl string, logLevel string) *config.Config {
	cfg := config.SetConfig(mongoUrl, database, natsUrl, lokiUrl, logLevel)
	defer func() {
		if r := recover(); r != nil {
			tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Init : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
			fmt.Printf("Panic recovered in Init : %v - %v\n", r, string(debug.Stack()))
		}
	}()
	logs.CreateLogger("main")
	mongo.MONGOService.Init(models.GetModelsNames(), config.GetConfig()) // init the mongo service
	return cfg
}

/* GetConfig will get the config
* @return *Config
 */
func GetConfig() *config.Config {
	return config.GetConfig()
}

/* GetConfLoader
* Get the configuration loader for the application
* Parameters:
* - AppName: string : the name of the application
* Returns:
* - *onion.Onion : the configuration loader
*  The configuration loader will load the configuration from the following sources:
*  - the environment variables with the prefix OCAPPNAME_
*  - the file /etc/oc/appname.json
*  - the file ./appname.json
*  The configuration loader will merge the configuration from the different sources
*  The configuration loader will give priority to the environment variables
*  The configuration loader will give priority to the local file over the default file
 */

func GetConfLoader() *onion.Onion {
	return config.GetConfLoader()
}

type Request struct {
	collection LibDataEnum
	user       string
	peerID     string
	groups     []string
	caller     *tools.HTTPCaller
}

func NewRequest(collection LibDataEnum, user string, peerID string, groups []string, caller *tools.HTTPCaller) *Request {
	return &Request{collection: collection, user: user, peerID: peerID, groups: groups, caller: caller}
}

func ToScheduler(m interface{}) (n *workflow_execution.WorkflowSchedule) {
	defer func() {
		if r := recover(); r != nil {
			return
		}
	}()
	return m.(*workflow_execution.WorkflowSchedule)
}

func (r *Request) Schedule(wfID string, scheduler *workflow_execution.WorkflowSchedule) (*workflow_execution.WorkflowSchedule, error) {
	if _, _, err := scheduler.Schedules(wfID, &tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	}); err != nil {
		return nil, err
	}
	return scheduler, nil
}

func (r *Request) CheckBooking(wfID string, start string, end string, durationInS float64, cron string) bool {
	ok, _, _, err := workflow_execution.NewScheduler(start, end, durationInS, cron).CheckBooking(wfID, &tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	})
	if err != nil {
		fmt.Println(err)
		return false
	}
	return ok
}

func (r *Request) DraftOrder(scheduler *workflow_execution.WorkflowSchedule) (*order.Order, error) {
	o := &order.Order{}
	if err := o.DraftOrder(scheduler, &tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	}); err != nil {
		return nil, err
	}
	return o, nil
}

func (r *Request) PaymentTunnel(o *order.Order, scheduler *workflow_execution.WorkflowSchedule) error {
	return o.Pay(scheduler, &tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	})
}

/*
* Search will search for the data in the database
* @param filters *dbs.Filters
* @param word string
* @param collection LibDataEnum
* @param c ...*tools.HTTPCaller
* @return data LibDataShallow
 */
func (r *Request) Search(filters *dbs.Filters, word string, isDraft bool) (data LibDataShallow) {
	defer func() { // recover the panic
		if r := recover(); r != nil {
			tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in Search : "+fmt.Sprintf("%v", r)))
			data = LibDataShallow{Data: nil, Code: 500, Err: "Panic recovered in LoadAll : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
		}
	}()
	d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	}).Search(filters, word, isDraft)
	if err != nil {
		data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
		return
	}
	data = LibDataShallow{Data: d, Code: code}
	return
}

/*
* LoadAll will load all the data from the database
* @param collection LibDataEnum
* @param c ...*tools.HTTPCaller
* @return data LibDataShallow
 */
func (r *Request) LoadAll(isDraft bool) (data LibDataShallow) {
	defer func() { // recover the panic
		if r := recover(); r != nil {
			tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in LoadAll : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
			data = LibDataShallow{Data: nil, Code: 500, Err: "Panic recovered in LoadAll : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
		}
	}()
	d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	}).LoadAll(isDraft)
	if err != nil {
		data = LibDataShallow{Data: d, Code: code, Err: err.Error()}
		return
	}
	data = LibDataShallow{Data: d, Code: code}
	return
}

/*
* LoadOne will load one data from the database
* @param collection LibDataEnum
* @param id string
* @param c ...*tools.HTTPCaller
* @return data LibData
 */
func (r *Request) LoadOne(id string) (data LibData) {
	defer func() { // recover the panic
		if r := recover(); r != nil {
			tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in LoadOne : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
			data = LibData{Data: nil, Code: 500, Err: "Panic recovered in LoadOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
		}
	}()
	d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	}).LoadOne(id)
	if err != nil {
		data = LibData{Data: d, Code: code, Err: err.Error()}
		return
	}
	data = LibData{Data: d, Code: code}
	return
}

/*
* UpdateOne will update one data from the database
* @param collection LibDataEnum
* @param set map[string]interface{}
* @param id string
* @param c ...*tools.HTTPCaller
* @return data LibData
 */
func (r *Request) UpdateOne(set map[string]interface{}, id string) (data LibData) {
	defer func() { // recover the panic
		if r := recover(); r != nil {
			tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in UpdateOne : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
			data = LibData{Data: nil, Code: 500, Err: "Panic recovered in UpdateOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
		}
	}()
	model := models.Model(r.collection.EnumIndex())
	d, code, err := model.GetAccessor(&tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	}).UpdateOne(model.Deserialize(set, model), id)
	if err != nil {
		data = LibData{Data: d, Code: code, Err: err.Error()}
		return
	}
	data = LibData{Data: d, Code: code}
	return
}

/*
* DeleteOne will delete one data from the database
* @param collection LibDataEnum
* @param id string
* @param c ...*tools.HTTPCaller
* @return data LibData
 */
func (r *Request) DeleteOne(id string) (data LibData) {
	defer func() { // recover the panic
		if r := recover(); r != nil {
			tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in DeleteOne : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
			data = LibData{Data: nil, Code: 500, Err: "Panic recovered in DeleteOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
		}
	}()
	d, code, err := models.Model(r.collection.EnumIndex()).GetAccessor(&tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	}).DeleteOne(id)
	if err != nil {
		data = LibData{Data: d, Code: code, Err: err.Error()}
		return
	}
	data = LibData{Data: d, Code: code}
	return
}

/*
* StoreOne will store one data from the database
* @param collection LibDataEnum
* @param object map[string]interface{}
* @param c ...*tools.HTTPCaller
* @return data LibData
 */
func (r *Request) StoreOne(object map[string]interface{}) (data LibData) {
	defer func() { // recover the panic
		if r := recover(); r != nil {
			tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in StoreOne : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
			data = LibData{Data: nil, Code: 500, Err: "Panic recovered in StoreOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
		}
	}()
	model := models.Model(r.collection.EnumIndex())
	d, code, err := model.GetAccessor(&tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	}).StoreOne(model.Deserialize(object, model))
	if err != nil {
		data = LibData{Data: d, Code: code, Err: err.Error()}
		return
	}
	data = LibData{Data: d, Code: code}
	return
}

/*
* CopyOne will copy one data from the database
* @param collection LibDataEnum
* @param object map[string]interface{}
* @param c ...*tools.HTTPCaller
* @return data LibData
 */
func (r *Request) CopyOne(object map[string]interface{}) (data LibData) {
	defer func() { // recover the panic
		if r := recover(); r != nil {
			tools.UncatchedError = append(tools.UncatchedError, errors.New("Panic recovered in CopyOne : "+fmt.Sprintf("%v", r)+" - "+string(debug.Stack())))
			data = LibData{Data: nil, Code: 500, Err: "Panic recovered in UpdateOne : " + fmt.Sprintf("%v", r) + " - " + string(debug.Stack())}
		}
	}()
	model := models.Model(r.collection.EnumIndex())
	d, code, err := model.GetAccessor(&tools.APIRequest{
		Caller:   r.caller,
		Username: r.user,
		PeerID:   r.peerID,
		Groups:   r.groups,
	}).CopyOne(model.Deserialize(object, model))
	if err != nil {
		data = LibData{Data: d, Code: code, Err: err.Error()}
		return
	}
	data = LibData{Data: d, Code: code}
	return
}

// ================ CAST ========================= //

func (l *LibData) ToDataResource() *resources.DataResource {
	if l.Data.GetAccessor(nil).GetType() == tools.DATA_RESOURCE {
		return l.Data.(*resources.DataResource)
	}
	return nil
}

func (l *LibData) ToComputeResource() *resources.ComputeResource {
	if l.Data != nil && l.Data.GetAccessor(nil).GetType() == tools.COMPUTE_RESOURCE {
		return l.Data.(*resources.ComputeResource)
	}
	return nil
}
func (l *LibData) ToStorageResource() *resources.StorageResource {
	if l.Data.GetAccessor(nil).GetType() == tools.STORAGE_RESOURCE {
		return l.Data.(*resources.StorageResource)
	}
	return nil
}
func (l *LibData) ToProcessingResource() *resources.ProcessingResource {
	if l.Data.GetAccessor(nil).GetType() == tools.PROCESSING_RESOURCE {
		return l.Data.(*resources.ProcessingResource)
	}
	return nil
}
func (l *LibData) ToWorkflowResource() *resources.WorkflowResource {
	if l.Data.GetAccessor(nil).GetType() == tools.WORKFLOW_RESOURCE {
		return l.Data.(*resources.WorkflowResource)
	}
	return nil
}
func (l *LibData) ToPeer() *peer.Peer {
	if l.Data.GetAccessor(nil).GetType() == tools.PEER {
		return l.Data.(*peer.Peer)
	}
	return nil
}

func (l *LibData) ToWorkflow() *w2.Workflow {
	if l.Data.GetAccessor(nil).GetType() == tools.WORKFLOW {
		return l.Data.(*w2.Workflow)
	}
	return nil
}
func (l *LibData) ToWorkspace() *workspace.Workspace {
	if l.Data.GetAccessor(nil).GetType() == tools.WORKSPACE {
		return l.Data.(*workspace.Workspace)
	}
	return nil
}

func (l *LibData) ToCollaborativeArea() *collaborative_area.CollaborativeArea {
	if l.Data.GetAccessor(nil).GetType() == tools.COLLABORATIVE_AREA {
		return l.Data.(*collaborative_area.CollaborativeArea)
	}
	return nil
}

func (l *LibData) ToRule() *rule.Rule {
	if l.Data.GetAccessor(nil).GetType() == tools.COLLABORATIVE_AREA {
		return l.Data.(*rule.Rule)
	}
	return nil
}

func (l *LibData) ToWorkflowExecution() *workflow_execution.WorkflowExecutions {
	if l.Data.GetAccessor(nil).GetType() == tools.WORKFLOW_EXECUTION {
		return l.Data.(*workflow_execution.WorkflowExecutions)
	}
	return nil
}

func (l *LibData) ToOrder() *order.Order {
	if l.Data.GetAccessor(nil).GetType() == tools.ORDER {
		return l.Data.(*order.Order)
	}
	return nil
}