package workflow_execution

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

	"cloud.o-forge.io/core/oc-lib/models/common/enum"
	"cloud.o-forge.io/core/oc-lib/models/peer"
	"cloud.o-forge.io/core/oc-lib/models/utils"
	"cloud.o-forge.io/core/oc-lib/models/workflow"
	"cloud.o-forge.io/core/oc-lib/tools"
	"github.com/robfig/cron"
)

/*
* WorkflowSchedule is a struct that contains the scheduling information of a workflow
* It contains the mode of the schedule (Task or Service), the name of the schedule, the start and end time of the schedule and the cron expression
 */
// it's a flying object only use in a session time. It's not stored in the database
type WorkflowSchedule struct {
	Workflow           *workflow.Workflow    `json:"workflow,omitempty"`                    // Workflow is the workflow dependancy of the schedule
	WorkflowExecutions []*WorkflowExecutions `json:"workflow_executions,omitempty"`         // WorkflowExecutions is the list of executions of the workflow
	Message            string                `json:"message,omitempty"`                     // Message is the message of the schedule
	Warning            string                `json:"warning,omitempty"`                     // Warning is the warning message of the schedule
	Start              time.Time             `json:"start" validate:"required,ltfield=End"` // Start is the start time of the schedule, is required and must be less than the End time
	End                *time.Time            `json:"end,omitempty"`                         // End is the end time of the schedule, is required and must be greater than the Start time
	DurationS          float64               `json:"duration_s" default:"-1"`               // End is the end time of the schedule
	Cron               string                `json:"cron,omitempty"`                        // here the cron format : ss mm hh dd MM dw task
}

func NewScheduler(start string, end string, durationInS float64, cron string) *WorkflowSchedule {
	s, err := time.Parse("2006-01-02T15:04:05", start)
	if err != nil {
		return nil
	}
	ws := &WorkflowSchedule{
		Start:     s,
		DurationS: durationInS,
		Cron:      cron,
	}
	e, err := time.Parse("2006-01-02T15:04:05", end)
	if err == nil {
		ws.End = &e
	}
	return ws
}

func (ws *WorkflowSchedule) CheckBooking(wfID string, request *tools.APIRequest) (bool, *workflow.Workflow, []*WorkflowExecutions, error) {
	if request.Caller == nil && request.Caller.URLS == nil && request.Caller.URLS[tools.BOOKING] == nil || request.Caller.URLS[tools.BOOKING][tools.GET] == "" {
		return false, nil, []*WorkflowExecutions{}, errors.New("no caller defined")
	}
	access := workflow.NewAccessor(request)
	res, code, err := access.LoadOne(wfID)
	if code != 200 {
		return false, nil, []*WorkflowExecutions{}, errors.New("could not load the workflow with id: " + err.Error())
	}
	wf := res.(*workflow.Workflow)
	longest, priceds, wf, err := wf.Planify(ws.Start, ws.End, request)
	if err != nil {
		return false, wf, []*WorkflowExecutions{}, err
	}
	ws.DurationS = longest
	ws.Message = "We estimate that the workflow will start at " + ws.Start.String() + " and last " + fmt.Sprintf("%v", ws.DurationS) + " seconds."
	if ws.End != nil && ws.Start.Add(time.Duration(longest)*time.Second).After(*ws.End) {
		ws.Warning = "The workflow may be too long to be executed in the given time frame, we will try to book it anyway\n"
	}
	execs, err := ws.getExecutions(wf)
	if err != nil {
		return false, wf, []*WorkflowExecutions{}, err
	}
	for _, exec := range execs {
		bookings := exec.Book(wfID, priceds)
		for _, b := range bookings {
			meth := request.Caller.URLS[tools.BOOKING][tools.GET]
			meth = strings.ReplaceAll(meth, ":id", b.ResourceID)
			meth = strings.ReplaceAll(meth, ":start_date", b.ExpectedStartDate.Format("2006-01-02T15:04:05"))
			meth = strings.ReplaceAll(meth, ":end_date", b.ExpectedEndDate.Format("2006-01-02T15:04:05"))
			request.Caller.URLS[tools.BOOKING][tools.GET] = meth
			_, err := (&peer.Peer{}).LaunchPeerExecution(b.DestPeerID, b.ResourceID, tools.BOOKING, tools.GET, nil, request.Caller)
			if err != nil {
				return false, wf, execs, err
			}
		}
	}
	return true, wf, execs, nil
}

func (ws *WorkflowSchedule) Schedules(wfID string, request *tools.APIRequest) (*WorkflowSchedule, *workflow.Workflow, []*WorkflowExecutions, error) {
	if request == nil {
		return ws, nil, []*WorkflowExecutions{}, errors.New("no request found")
	}
	c := request.Caller
	if c == nil || c.URLS == nil || c.URLS[tools.BOOKING] == nil {
		return ws, nil, []*WorkflowExecutions{}, errors.New("no caller defined")
	}
	methods := c.URLS[tools.BOOKING]
	if _, ok := methods[tools.GET]; !ok {
		return ws, nil, []*WorkflowExecutions{}, errors.New("no path found")
	}
	ok, wf, executions, err := ws.CheckBooking(wfID, request)
	if !ok || err != nil {
		return ws, nil, []*WorkflowExecutions{}, errors.New("could not book the workflow : " + fmt.Sprintf("%v", err))
	}

	ws.Workflow = wf
	ws.WorkflowExecutions = executions

	for _, exec := range executions {
		err := exec.PurgeDraft(request)
		if err != nil {
			return ws, nil, []*WorkflowExecutions{}, errors.New("purge draft" + fmt.Sprintf("%v", err))
		}
		exec.GenerateID()
		exec.StoreDraftDefault()
		// Should DELETE the previous execution2
		utils.GenericStoreOne(exec, NewAccessor(request))
	}
	return ws, wf, executions, nil
}

/*
BOOKING IMPLIED TIME, not of subscription but of execution
so is processing time execution time applied on computes
data can improve the processing time
time should implied a security time border (10sec) if not from the same executions
VERIFY THAT WE HANDLE DIFFERENCE BETWEEN LOCATION TIME && BOOKING
*/

/*
* getExecutions is a function that returns the executions of a workflow
* it returns an array of workflow_execution.WorkflowExecution
 */
func (ws *WorkflowSchedule) getExecutions(workflow *workflow.Workflow) ([]*WorkflowExecutions, error) {
	workflows_executions := []*WorkflowExecutions{}
	dates, err := ws.getDates()
	if err != nil {
		return workflows_executions, err
	}
	for _, date := range dates {
		obj := &WorkflowExecutions{
			AbstractObject: utils.AbstractObject{
				Name: workflow.Name + "_execution_" + date.Start.String(), // set the name of the execution
			},
			ExecDate:   date.Start,       // set the execution date
			EndDate:    date.End,         // set the end date
			State:      enum.DRAFT,       // set the state to 1 (scheduled)
			WorkflowID: workflow.GetID(), // set the workflow id dependancy of the execution
		}
		workflows_executions = append(workflows_executions, obj)
	}
	return workflows_executions, nil
}

func (ws *WorkflowSchedule) getDates() ([]Schedule, error) {
	schedule := []Schedule{}
	if len(ws.Cron) > 0 { // if cron is set then end date should be set
		if ws.End == nil {
			return schedule, errors.New("a cron task should have an end date")
		}
		if ws.DurationS <= 0 {
			ws.DurationS = ws.End.Sub(ws.Start).Seconds()
		}
		cronStr := strings.Split(ws.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 schedule, errors.New("Bad cron message: (" + ws.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 schedule, errors.New("Bad cron message: " + err.Error())
		}
		// loop through the cron schedule to set the executions
		for s := sched.Next(ws.Start); !s.IsZero() && s.Before(*ws.End); s = sched.Next(s) {
			e := s.Add(time.Duration(ws.DurationS) * time.Second)
			schedule = append(schedule, Schedule{
				Start: s,
				End:   &e,
			})
		}
	} else { // if no cron, set the execution to the start date
		schedule = append(schedule, Schedule{
			Start: ws.Start,
			End:   ws.End,
		})
	}
	return schedule, nil
}

type Schedule struct {
	Start time.Time
	End   *time.Time
}

/*
* TODO : LARGEST GRAIN PLANIFYING THE WORKFLOW WHEN OPTION IS SET
* SET PROTECTION BORDER TIME
 */
