package workflow

import (
	"errors"
	"fmt"
	"strings"

	"cloud.o-forge.io/core/oc-lib/dbs"
	"cloud.o-forge.io/core/oc-lib/dbs/mongo"
	"cloud.o-forge.io/core/oc-lib/models/collaborative_area/shallow_collaborative_area"
	"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/resources/datacenter"
	"cloud.o-forge.io/core/oc-lib/models/utils"
	"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"
	cron "github.com/robfig/cron/v3"
)

type workflowMongoAccessor struct {
	utils.AbstractAccessor // AbstractAccessor contains the basic fields of an accessor (model, caller)
}

// New creates a new instance of the workflowMongoAccessor
func New() *workflowMongoAccessor {
	return &workflowMongoAccessor{}
}

/*
* THERE IS A LOT IN THIS FILE SHOULD BE AWARE OF THE COMMENTS
 */

/*
* getExecutions is a function that returns the executions of a workflow
* it returns an array of workflow_execution.WorkflowExecution
 */
func (wfa *workflowMongoAccessor) getExecutions(id string, data *Workflow) ([]*workflow_execution.WorkflowExecution, error) {
	workflows_execution := []*workflow_execution.WorkflowExecution{}
	if data.Schedule != nil { // only set execution on a scheduled workflow
		if data.Schedule.Start == nil { // if no start date, return an error
			return workflows_execution, errors.New("should get a start date on the scheduler.")
		}
		if data.Schedule.End != nil && data.Schedule.End.IsZero() { // if end date is zero, set it to nil
			data.Schedule.End = nil
		}
		if len(data.Schedule.Cron) > 0 { // if cron is set then end date should be set
			if data.Schedule.End == nil {
				return workflows_execution, errors.New("a cron task should have an end date.")
			}
			cronStr := strings.Split(data.Schedule.Cron, " ") // split the cron string to treat it
			if len(cronStr) < 6 {                             // if the cron string is less than 6 fields, return an error because format is : ss mm hh dd MM dw (6 fields)
				return nil, errors.New("Bad cron message: " + data.Schedule.Cron + ". Should be at least ss mm hh dd MM dw")
			}
			subCron := strings.Join(cronStr[:6], " ")
			// cron should be parsed as ss mm hh dd MM dw t (min 6 fields)
			specParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) // create a new cron parser
			sched, err := specParser.Parse(subCron)                                                                // parse the cron string
			if err != nil {
				return workflows_execution, errors.New("Bad cron message: " + err.Error())
			}
			// loop through the cron schedule to set the executions
			for s := sched.Next(*data.Schedule.Start); !s.IsZero() && s.Before(*data.Schedule.End); s = sched.Next(s) {
				obj := &workflow_execution.WorkflowExecution{
					AbstractObject: utils.AbstractObject{
						Name: data.Schedule.Name, // set the name of the execution
					},
					ExecDate:   &s,                // set the execution date
					EndDate:    data.Schedule.End, // set the end date
					State:      1,                 // set the state to 1 (scheduled)
					WorkflowID: id,                // set the workflow id dependancy of the execution
				}
				workflows_execution = append(workflows_execution, obj) // append the execution to the array
			}

		} else { // if no cron, set the execution to the start date
			obj := &workflow_execution.WorkflowExecution{ // create a new execution
				AbstractObject: utils.AbstractObject{
					Name: data.Schedule.Name,
				},
				ExecDate:   data.Schedule.Start,
				EndDate:    data.Schedule.End,
				State:      1,
				WorkflowID: id,
			}
			workflows_execution = append(workflows_execution, obj) // append the execution to the array
		}
	}
	return workflows_execution, nil
}

// DeleteOne deletes a workflow from the database, delete depending executions and bookings
func (wfa *workflowMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
	wfa.execution(id, &Workflow{
		AbstractWorkflow: AbstractWorkflow{ScheduleActive: false},
	}, true) // delete the executions
	res, code, err := wfa.GenericDeleteOne(id, wfa)
	if res != nil && code == 200 {
		wfa.execute(res.(*Workflow), true, false) // up to date the workspace for the workflow
		wfa.share(res.(*Workflow), true, wfa.Caller)
	}
	return res, code, err
}

/*
* book is a function that books a workflow on the peers
* it takes the workflow id, the real data and the executions
* it returns an error if the booking fails
 */
func (wfa *workflowMongoAccessor) book(id string, realData *Workflow, execs []*workflow_execution.WorkflowExecution) error {
	if wfa.Caller == nil || wfa.Caller.URLS == nil || wfa.Caller.URLS[tools.BOOKING] == nil {
		return errors.New("no caller defined")
	}
	methods := wfa.Caller.URLS[tools.BOOKING]
	if _, ok := methods[tools.POST]; !ok {
		return errors.New("no path found")
	}
	res, code, _ := wfa.LoadOne(id)
	if code != 200 {
		return errors.New("could not load workflow")
	}
	r := res.(*Workflow)
	g := r.Graph
	if realData.Graph != nil { // if the graph is set, set it to the real data
		g = realData.Graph
	}
	if g != nil && g.Links != nil && len(g.Links) > 0 { // if the graph is set and has links then book the workflow (even on ourselves)
		accessor := (&datacenter.DatacenterResource{}).GetAccessor(nil)
		for _, link := range g.Links {
			if ok, dc_id := realData.isDCLink(link); ok { // check if the link is a link between a datacenter and a resource booking is only on datacenter
				dc, code, _ := accessor.LoadOne(dc_id)
				if code != 200 {
					continue
				}
				// CHECK BOOKING
				peerID := dc.(*datacenter.DatacenterResource).PeerID
				if peerID == "" { // no peer id no booking
					continue
				}
				// BOOKING ON PEER
				_, err := (&peer.Peer{}).LaunchPeerExecution(peerID, "", tools.BOOKING, tools.POST,
					(&workflow_execution.WorkflowExecutions{ // it's the standard model for booking see OC-PEER
						WorkflowID: id,    // set the workflow id "WHO"
						ResourceID: dc_id, // set the datacenter id "WHERE"
						Executions: execs, // set the executions to book "WHAT"
					}).Serialize(), wfa.Caller)
				if err != nil {
					fmt.Println("BOOKING", err)
					return err
				}
			}
		}
	}
	return nil
}

/*
* share is a function that shares a workflow to the peers if the workflow is shared
 */
func (wfa *workflowMongoAccessor) share(realData *Workflow, delete bool, caller *tools.HTTPCaller) {
	if realData.Shared == nil || len(realData.Shared) == 0 || caller.Disabled { // no shared no sharing
		return
	}
	for _, sharedID := range realData.Shared { // loop through the shared ids
		access := (&shallow_collaborative_area.ShallowCollaborativeArea{}).GetAccessor(nil)
		res, code, _ := access.LoadOne(sharedID)
		if code != 200 {
			continue
		}
		var err error
		paccess := &peer.Peer{}
		for _, p := range res.(*shallow_collaborative_area.ShallowCollaborativeArea).Peers {
			paccess.UUID = p
			if paccess.IsMySelf() { // if the peer is the current peer, never share because it will create a loop
				continue
			}
			if delete { // if the workflow is deleted, share the deletion
				history := NewHistory()
				history.StoreOne(history.MapFromWorkflow(res.(*Workflow)))
				_, err = paccess.LaunchPeerExecution(p, res.GetID(), tools.WORKFLOW, tools.DELETE, map[string]interface{}{}, caller)
			} else { // if the workflow is updated, share the update
				_, err = paccess.LaunchPeerExecution(p, res.GetID(), tools.WORKFLOW, tools.PUT, res.Serialize(), caller)
			}
		}
		if err != nil {
			wfa.Logger.Error().Msg(err.Error())
		}
	}
}

/*
* execution is a create or delete function for the workflow executions depending on the schedule of the workflow
 */
func (wfa *workflowMongoAccessor) execution(id string, realData *Workflow, delete bool) (int, error) {
	nats := tools.NewNATSCaller() // create a new nats caller because executions are sent to the nats for daemons
	mongo.MONGOService.DeleteMultiple(map[string]interface{}{
		"state":       1, // only delete the scheduled executions only scheduled if executions are in progress or ended, they should not be deleted for registration
		"workflow_id": id,
	}, tools.WORKFLOW_EXECUTION.String())
	err := wfa.book(id, realData, []*workflow_execution.WorkflowExecution{}) // delete the booking of the workflow on the peers
	fmt.Println("DELETE BOOKING", err)
	nats.SetNATSPub(tools.WORKFLOW.String(), tools.REMOVE, realData) // send the deletion to the nats
	if err != nil {
		return 409, err
	}

	accessor := (&workflow_execution.WorkflowExecution{}).GetAccessor(nil)
	execs, err := wfa.getExecutions(id, realData) // get the executions of the workflow
	if err != nil {
		return 422, err
	}
	if !realData.ScheduleActive || delete { // if the schedule is not active, delete the executions
		execs = []*workflow_execution.WorkflowExecution{}
	}
	err = wfa.book(id, realData, execs) // book the workflow on the peers
	fmt.Println("BOOKING", err)
	if err != nil {
		return 409, err // if the booking fails, return an error for integrity between peers
	}
	fmt.Println("BOOKING", delete)
	for _, obj := range execs {
		_, code, err := accessor.StoreOne(obj)
		fmt.Println("EXEC", code, err)
		if code != 200 {
			return code, err
		}
	}
	nats.SetNATSPub(tools.WORKFLOW.String(), tools.CREATE, realData) // send the creation to the nats
	return 200, nil
}

// UpdateOne updates a workflow in the database
func (wfa *workflowMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
	res, code, err := wfa.LoadOne(id)
	if code != 200 {
		return nil, 409, err
	}
	// avoid the update if the schedule is the same
	avoid := set.(*Workflow).Schedule == nil || (res.(*Workflow).Schedule != nil && res.(*Workflow).ScheduleActive == set.(*Workflow).ScheduleActive && res.(*Workflow).Schedule.Start == set.(*Workflow).Schedule.Start && res.(*Workflow).Schedule.End == set.(*Workflow).Schedule.End && res.(*Workflow).Schedule.Cron == set.(*Workflow).Schedule.Cron)
	res, code, err = wfa.GenericUpdateOne(set, id, wfa, &Workflow{})
	if code != 200 {
		return nil, code, err
	}
	fmt.Println("UPDATE", avoid, res.(*Workflow).ScheduleActive)
	if !avoid { // if the schedule is not avoided, update the executions
		if code, err := wfa.execution(id, res.(*Workflow), false); code != 200 {
			return nil, code, errors.New("could not update the executions : " + err.Error())
		}
	}
	wfa.execute(res.(*Workflow), false, false)    // update the workspace for the workflow
	wfa.share(res.(*Workflow), false, wfa.Caller) // share the update to the peers
	return res, code, nil
}

// StoreOne stores a workflow in the database
func (wfa *workflowMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
	res, code, err := wfa.GenericStoreOne(data, wfa)
	if err != nil || code != 200 {
		return nil, code, err
	}
	wfa.share(res.(*Workflow), false, wfa.Caller) // share the creation to the peers
	//store the executions
	if code, err := wfa.execution(res.GetID(), res.(*Workflow), false); err != nil {
		return nil, code, err
	}
	wfa.execute(res.(*Workflow), false, false) // store the workspace for the workflow
	return res, code, nil
}

// CopyOne copies a workflow in the database
func (wfa *workflowMongoAccessor) CopyOne(data utils.DBObject) (utils.DBObject, int, error) {
	return wfa.GenericStoreOne(data, wfa)
}

// execute is a function that executes a workflow
// it stores the workflow resources in a specific workspace to never have a conflict in UI and logic
func (wfa *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active bool) {

	accessor := (&workspace.Workspace{}).GetAccessor(nil)
	filters := &dbs.Filters{
		Or: map[string][]dbs.Filter{ // filter by standard workspace name attached to a workflow
			"abstractobject.name": {{dbs.LIKE.String(), workflow.Name + "_workspace"}},
		},
	}
	resource, _, err := accessor.Search(filters, "")
	if delete { // if delete is set to true, delete the workspace
		for _, r := range resource {
			accessor.DeleteOne(r.GetID())
		}
		return
	}
	if err == nil && len(resource) > 0 { // if the workspace already exists, update it
		accessor.UpdateOne(&workspace.Workspace{
			Active: active,
			ResourceSet: resources.ResourceSet{
				Datas:       workflow.Datas,
				Processings: workflow.Processings,
				Storages:    workflow.Storages,
				Workflows:   workflow.Workflows,
				Datacenters: workflow.Datacenters,
			},
		}, resource[0].GetID())
	} else { // if the workspace does not exist, create it
		accessor.StoreOne(&workspace.Workspace{
			Active:         active,
			AbstractObject: utils.AbstractObject{Name: workflow.Name + "_workspace"},
			ResourceSet: resources.ResourceSet{
				Datas:       workflow.Datas,
				Processings: workflow.Processings,
				Storages:    workflow.Storages,
				Workflows:   workflow.Workflows,
				Datacenters: workflow.Datacenters,
			},
		})
	}
}

// LoadOne loads a workflow from the database
func (wfa *workflowMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
	var workflow Workflow
	res_mongo, code, err := mongo.MONGOService.LoadOne(id, wfa.GetType())
	if err != nil {
		wfa.Logger.Error().Msg("Could not retrieve " + id + " from db. Error: " + err.Error())
		return nil, code, err
	}
	res_mongo.Decode(&workflow)
	wfa.execute(&workflow, false, true) // if no workspace is attached to the workflow, create it

	return &workflow, 200, nil
}

// LoadAll loads all the workflows from the database
func (wfa workflowMongoAccessor) LoadAll() ([]utils.ShallowDBObject, int, error) {
	objs := []utils.ShallowDBObject{}
	res_mongo, code, err := mongo.MONGOService.LoadAll(wfa.GetType())
	if err != nil {
		wfa.Logger.Error().Msg("Could not retrieve any from db. Error: " + err.Error())
		return nil, code, err
	}
	var results []Workflow
	if err = res_mongo.All(mongo.MngoCtx, &results); err != nil {
		return nil, 404, err
	}
	for _, r := range results {
		objs = append(objs, &r.AbstractObject) // only AbstractObject fields !
	}
	return objs, 200, nil
}

func (wfa *workflowMongoAccessor) Search(filters *dbs.Filters, search string) ([]utils.ShallowDBObject, int, error) {
	objs := []utils.ShallowDBObject{}
	if (filters == nil || len(filters.And) == 0 || len(filters.Or) == 0) && search != "" {
		filters = &dbs.Filters{
			Or: map[string][]dbs.Filter{ // filter by name if no filters are provided
				"abstractobject.name": {{Operator: dbs.LIKE.String(), Value: search}},
			},
		}
	}
	res_mongo, code, err := mongo.MONGOService.Search(filters, wfa.GetType())
	if err != nil {
		wfa.Logger.Error().Msg("Could not store to db. Error: " + err.Error())
		return nil, code, err
	}
	var results []Workflow
	if err = res_mongo.All(mongo.MngoCtx, &results); err != nil {
		return nil, 404, err
	}
	for _, r := range results {
		objs = append(objs, &r)
	}
	return objs, 200, nil
}
