// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.

// Package schedulemanager schedules association and submits the association to the task pool
// schedulemanager is a singleton so it can be access at the plugin level
package schedulemanager

import (
	"fmt"
	"sync"
	"time"

	complianceModel "github.com/aws/amazon-ssm-agent/agent/association/compliance/model"
	"github.com/aws/amazon-ssm-agent/agent/association/model"
	"github.com/aws/amazon-ssm-agent/agent/contracts"
	"github.com/aws/amazon-ssm-agent/agent/jsonutil"
	"github.com/aws/amazon-ssm-agent/agent/log"
	"github.com/aws/amazon-ssm-agent/agent/times"
	"github.com/aws/aws-sdk-go/aws"
)

var associations = []*model.InstanceAssociation{}
var lock sync.RWMutex

// Refresh refreshes cached associationRawData
func Refresh(log log.T, assocs []*model.InstanceAssociation) {
	lock.Lock()
	defer lock.Unlock()

	associations = []*model.InstanceAssociation{}
	log.Debugf("Refreshing schedule manager with %v associations", len(assocs))

	for _, newAssoc := range assocs {
		// if association has errors, it will be excluded for the future schedule
		// the next refresh (default to 10 minutes) will retry it
		if len(newAssoc.Errors) == 0 {
			associations = append(associations, newAssoc)
		}
	}

	numberOfNewAssoc := 0
	for _, assoc := range associations {
		assoc.SetNextScheduledDate(log)
		if assoc.NextScheduledDate != nil {
			log.Infof("Scheduling association %v, setting next ScheduledDate to %v", *assoc.Association.AssociationId, times.ToIsoDashUTC(*assoc.NextScheduledDate))
		}

		if assocContent, err := jsonutil.Marshal(assoc); err != nil {
			log.Errorf("Failed to parse scheduled association, %v", err)
		} else {
			log.Debugf("Scheduled Association content is %v", jsonutil.Indent(assocContent))
		}

		if assoc.Association.LastExecutionDate == nil {
			numberOfNewAssoc++
		}
	}

	complianceModel.RefreshAssociationComplianceItems(associations)

	log.Infof("Schedule manager refreshed with %v associations, %v new associations associated", len(associations), numberOfNewAssoc)
}

// LoadNextScheduledAssociation returns next scheduled association
func LoadNextScheduledAssociation(log log.T) (*model.InstanceAssociation, error) {
	lock.Lock()
	defer lock.Unlock()

	if len(associations) == 0 {
		return nil, nil
	}

	for _, assoc := range associations {
		currentTime := time.Now().UTC()
		if assoc.NextScheduledDate == nil {
			continue
		}

		if (*assoc.NextScheduledDate).Before(currentTime) || (*assoc.NextScheduledDate).Equal(currentTime) {
			if assocContent, err := jsonutil.Marshal(assoc); err != nil {
				return nil, fmt.Errorf("failed to parse scheduled association, %v", err)
			} else {
				log.Infof("Next scheduled association is %v", jsonutil.Indent(assocContent))
			}

			return assoc, nil
		}
	}

	return nil, nil
}

// LoadNextScheduledDate returns next scheduled date
func LoadNextScheduledDate(log log.T) *time.Time {
	lock.RLock()
	defer lock.RUnlock()

	var nextScheduleDate *time.Time
	for _, assoc := range associations {
		if assoc.NextScheduledDate == nil {
			continue
		}

		if nextScheduleDate == nil {
			nextScheduleDate = assoc.NextScheduledDate
		}

		if nextScheduleDate.After(*assoc.NextScheduledDate) {
			nextScheduleDate = assoc.NextScheduledDate
		}
	}

	return nextScheduleDate
}

// UpdateNextScheduledDate sets next scheduled date for the given association
func UpdateNextScheduledDate(log log.T, associationID string) {
	lock.Lock()
	defer lock.Unlock()

	for _, assoc := range associations {
		if *assoc.Association.AssociationId == associationID {
			assoc.Association.LastExecutionDate = aws.Time(time.Now().UTC())
			assoc.SetNextScheduledDate(log)
			if assoc.NextScheduledDate != nil {
				log.Infof("Scheduling association %v, setting next ScheduledDate to %v", *assoc.Association.AssociationId, times.ToIsoDashUTC(*assoc.NextScheduledDate))
			}
			break
		}
	}
}

// UpdateAssociationStatus sets detailed status for the given association
func UpdateAssociationStatus(associationID string, status string) {
	lock.Lock()
	defer lock.Unlock()

	for _, assoc := range associations {
		if *assoc.Association.AssociationId == associationID {
			assoc.Association.DetailedStatus = aws.String(status)
			break
		}
	}
}

// IsAssociationInProgress returns if given association has detailed status as InProgress
func IsAssociationInProgress(associationID string) bool {
	lock.Lock()
	defer lock.Unlock()

	for _, assoc := range associations {
		if *assoc.Association.AssociationId == associationID {
			if assoc.Association.DetailedStatus == nil {
				return false
			}

			if *assoc.Association.DetailedStatus == contracts.AssociationStatusInProgress {
				return true
			}

			return false
		}
	}

	return false
}

// Schedules returns all the cached schedules
func Schedules() []*model.InstanceAssociation {
	lock.RLock()
	defer lock.RUnlock()
	return associations
}

func AssociationExists(associationID string) bool {
	for _, assoc := range associations {
		if *assoc.Association.AssociationId == associationID {
			return true
		}
	}
	return false
}
