package workflow

import (
	"errors"
	"fmt"
	"slices"
	"strings"
	"time"

	"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/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/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"
)

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

	computeResourceAccessor   utils.Accessor
	collaborativeAreaAccessor utils.Accessor
	executionAccessor         utils.Accessor
	workspaceAccessor         utils.Accessor
}

// New creates a new instance of the workflowMongoAccessor
func New(t tools.DataType, peerID string, groups []string, caller *tools.HTTPCaller) *workflowMongoAccessor {
	return &workflowMongoAccessor{
		computeResourceAccessor:   (&resources.ComputeResource{}).GetAccessor(peerID, groups, nil),
		collaborativeAreaAccessor: (&shallow_collaborative_area.ShallowCollaborativeArea{}).GetAccessor(peerID, groups, nil),
		executionAccessor:         (&workflow_execution.WorkflowExecution{}).GetAccessor(peerID, groups, nil),
		workspaceAccessor:         (&workspace.Workspace{}).GetAccessor(peerID, groups, nil),
		AbstractAccessor: utils.AbstractAccessor{
			Logger: logs.CreateLogger(t.String()), // Create a logger with the data type
			Caller: caller,
			PeerID: peerID,
			Groups: groups, // Set the caller
			Type:   t,
		},
	}
}

/*
* 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 (a *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 (a *workflowMongoAccessor) DeleteOne(id string) (utils.DBObject, int, error) {
	a.execution(id, &Workflow{
		AbstractWorkflow: AbstractWorkflow{ScheduleActive: false},
	}, true) // delete the executions
	res, code, err := utils.GenericDeleteOne(id, a)
	if res != nil && code == 200 {
		a.execute(res.(*Workflow), true, false) // up to date the workspace for the workflow
		a.share(res.(*Workflow), true, a.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 (a *workflowMongoAccessor) book(id string, realData *Workflow, execs []*workflow_execution.WorkflowExecution) error {
	if a.Caller == nil || a.Caller.URLS == nil || a.Caller.URLS[tools.BOOKING] == nil {
		return errors.New("no caller defined")
	}
	methods := a.Caller.URLS[tools.BOOKING]
	if _, ok := methods[tools.POST]; !ok {
		return errors.New("no path found")
	}
	res, code, _ := a.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)
		isDCFound := []string{}
		for _, link := range g.Links {
			if ok, dc_id := realData.isDCLink(link); ok { // check if the link is a link between a compute and a resource booking is only on compute
				if slices.Contains(isDCFound, dc_id) {
					continue
				} // if the compute is already found, skip it
				isDCFound = append(isDCFound, dc_id)
				dc, code, _ := a.computeResourceAccessor.LoadOne(dc_id)
				if code != 200 {
					continue
				}
				// CHECK BOOKING
				peerID := dc.(*resources.ComputeResource).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 compute id "WHERE"
						Executions: execs, // set the executions to book "WHAT"
					}).Serialize(), a.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 (a *workflowMongoAccessor) share(realData *Workflow, delete bool, caller *tools.HTTPCaller) {
	if realData == nil || realData.Shared == nil || len(realData.Shared) == 0 || caller == nil || caller.Disabled { // no shared no sharing
		return
	}
	for _, sharedID := range realData.Shared { // loop through the shared ids
		res, code, _ := a.collaborativeAreaAccessor.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 ok, _ := paccess.IsMySelf(); ok { // 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(res), caller)
			}
		}
		if err != nil {
			a.Logger.Error().Msg(err.Error())
		}
	}
}

/*
* execution is a create or delete function for the workflow executions depending on the schedule of the workflow
 */
func (a *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 := a.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
	}

	execs, err := a.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 = a.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 := a.executionAccessor.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 (a *workflowMongoAccessor) UpdateOne(set utils.DBObject, id string) (utils.DBObject, int, error) {
	res, code, err := a.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 = utils.GenericUpdateOne(set, id, a, &Workflow{})
	if code != 200 {
		return nil, code, err
	}
	workflow := res.(*Workflow)
	if !avoid { // if the schedule is not avoided, update the executions
		if code, err := a.execution(id, workflow, false); code != 200 {
			return nil, code, errors.New("could not update the executions : " + err.Error())
		}
	}
	fmt.Println("UPDATE", workflow.ScheduleActive, workflow.Schedule)
	if workflow.ScheduleActive && workflow.Schedule != nil { // if the workflow is scheduled, update the executions
		now := time.Now().UTC()
		if (workflow.Schedule.End != nil && now.After(*workflow.Schedule.End)) || (workflow.Schedule.End == nil && workflow.Schedule.Start != nil && now.After(*workflow.Schedule.Start)) { // if the start date is passed, then you can book
			workflow.ScheduleActive = false
			utils.GenericRawUpdateOne(workflow, id, a)
		} // if the start date is passed, update the executions
	}
	a.execute(workflow, false, false)  // update the workspace for the workflow
	a.share(workflow, false, a.Caller) // share the update to the peers
	return res, code, nil
}

// StoreOne stores a workflow in the database
func (a *workflowMongoAccessor) StoreOne(data utils.DBObject) (utils.DBObject, int, error) {
	d := data.(*Workflow)
	if d.ScheduleActive && d.Schedule != nil { // if the workflow is scheduled, update the executions
		now := time.Now().UTC()
		if (d.Schedule.End != nil && now.After(*d.Schedule.End)) || (d.Schedule.End == nil && d.Schedule.Start != nil && now.After(*d.Schedule.Start)) { // if the start date is passed, then you can book
			d.ScheduleActive = false
		} // if the start date is passed, update the executions
	}
	res, code, err := utils.GenericStoreOne(d, a)
	if err != nil || code != 200 {
		return nil, code, err
	}
	workflow := res.(*Workflow)

	a.share(workflow, false, a.Caller) // share the creation to the peers
	//store the executions
	if code, err := a.execution(res.GetID(), workflow, false); err != nil {
		return nil, code, err
	}
	a.execute(workflow, false, false) // store the workspace for the workflow
	return res, code, nil
}

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

// 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 (a *workflowMongoAccessor) execute(workflow *Workflow, delete bool, active bool) {

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

func (a *workflowMongoAccessor) LoadOne(id string) (utils.DBObject, int, error) {
	return utils.GenericLoadOne[*Workflow](id, func(d utils.DBObject) (utils.DBObject, int, error) {
		w := d.(*Workflow)
		if w.ScheduleActive && w.Schedule != nil { // if the workflow is scheduled, update the executions
			now := time.Now().UTC()
			if (w.Schedule.End != nil && now.After(*w.Schedule.End)) || (w.Schedule.End == nil && w.Schedule.Start != nil && now.After(*w.Schedule.Start)) { // if the start date is passed, then you can book
				w.ScheduleActive = false
				utils.GenericRawUpdateOne(d, id, a)
			} // if the start date is passed, update the executions
		}
		a.execute(w, false, true) // if no workspace is attached to the workflow, create it
		return d, 200, nil
	}, a)
}

func (a *workflowMongoAccessor) LoadAll() ([]utils.ShallowDBObject, int, error) {
	return utils.GenericLoadAll[*Workflow](func(d utils.DBObject) utils.ShallowDBObject { return &d.(*Workflow).AbstractObject }, a)
}

func (a *workflowMongoAccessor) Search(filters *dbs.Filters, search string) ([]utils.ShallowDBObject, int, error) {
	return utils.GenericSearch[*Workflow](filters, search, (&Workflow{}).GetObjectFilters(search), func(d utils.DBObject) utils.ShallowDBObject { return d }, a)
}
