package csbi

import (
	"context"
	"fmt"
	"os"
	"time"

	pb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/csbi"
	"code.fbi.h-da.de/danet/gosdn/controller/customerrs"
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"
)

// Deployment contains a UUID, a name for the deployment, and its state.
type Deployment struct {
	ID    uuid.UUID
	Name  string
	State pb.State
}

// DeploymentStore is a collection of channels that are used to access
// Deployments to read or write them. Access to the elements in the store
// is safe for concurrent use. Each instance of the store has a garbage
// collector periodically cleaning up decommissioned deployments.
type DeploymentStore struct {
	deploymentIDChannel chan<- uuid.UUID
	in                  <-chan Deployment
	out                 chan<- Deployment
	shutdown            chan<- context.Context
}

// NewDeploymentStore returns a DeploymentStore. It takes a time.Duration
// variable to configure the garbage collection interval.
func NewDeploymentStore(garbageCollectionInterval ...time.Duration) DeploymentStore {
	var gcInterval time.Duration
	if len(garbageCollectionInterval) > 0 {
		gcInterval = garbageCollectionInterval[0]
	} else {
		gcInterval = time.Minute
	}
	log.Infof("garbage collection interval set to %v", gcInterval)
	idChan, in, out, shutdown := setup(gcInterval)
	return DeploymentStore{
		deploymentIDChannel: idChan,
		in:                  in,
		out:                 out,
		shutdown:            shutdown,
	}
}

// Set takes a Deployment and saves it in the store.
func (store DeploymentStore) Set(deployment Deployment) {
	log.Tracef("entered Set func for %v", deployment.ID)
	store.out <- deployment
	log.Tracef("leaving Set func for %v", deployment.ID)
}

// Get takes a UUID and returns a Deployment. If no Deployment with the
// specified UUID can be found an error is returned.
func (store DeploymentStore) Get(id uuid.UUID) (Deployment, error) {
	log.Tracef("entered Get func for %v", id)
	store.deploymentIDChannel <- id
	log.Tracef("requested %v from store", id)
	deployment := <-store.in
	if deployment.ID != id {
		return Deployment{}, &customerrs.CouldNotFindError{ID: id}
	}
	log.Tracef("received %v from store", id)
	log.Tracef("leaving Get func for %v", id)
	return deployment, nil
}

// Delete takes a UUID and marks the Deployment with this UUID as decommissioned
// and ready for garbage collection. If no Deployment with the specified UUID can
// be found an error is returned.
func (store DeploymentStore) Delete(id uuid.UUID) error {
	log.Tracef("entered Delete func for %v", id)
	store.deploymentIDChannel <- id
	log.Tracef("requested %v from store", id)
	deployment := <-store.in
	if deployment.ID != id {
		return &customerrs.CouldNotFindError{ID: id}
	}
	log.Tracef("received %v from store", id)
	deployment.State = pb.State_STATE_DECOMMISSIONED
	log.Tracef("changed %v state to DECOMMISSIONED", id)
	store.out <- deployment
	log.Tracef("leaving Delete func for %v", deployment.ID)
	log.Infof("device %v decommissioned, awaiting garbage collection", id)
	return nil
}

// Shutdown holds a context which may contain a timeout and passes it to the
// stores state manager, where it forces a garbage collection.
func (store DeploymentStore) Shutdown(ctx context.Context) {
	store.shutdown <- ctx
}

func setup(garbageCollectionInterval time.Duration) (chan<- uuid.UUID, <-chan Deployment, chan<- Deployment, chan<- context.Context) {
	out := make(chan Deployment)
	in := make(chan Deployment)
	deploymentIDChannel := make(chan uuid.UUID)
	shutdown := make(chan context.Context)
	activeDeployments := make(map[uuid.UUID]Deployment)
	ticker := time.NewTicker(garbageCollectionInterval)
	go func() {
		for {
			select {
			case <-ticker.C:
				log.Trace("running garbage collection")
				if err := garbageCollector(context.Background(), activeDeployments); err != nil {
					log.Error(err)
				}
			case id := <-deploymentIDChannel:
				log.Tracef("request for %v", id)
				out <- activeDeployments[id]
			case deployment := <-in:
				log.Tracef("updating deployment %v", deployment.ID)
				deploymentsTotal.Set(float64(len(activeDeployments)))
				activeDeployments[deployment.ID] = deployment
			case ctx := <-shutdown:
				for k, v := range activeDeployments {
					v.State = pb.State_STATE_DECOMMISSIONED
					activeDeployments[k] = v
				}
				if err := garbageCollector(ctx, activeDeployments); err != nil {
					log.Error(err)
				}
			}
		}
	}()
	return deploymentIDChannel, out, in, shutdown
}

func garbageCollector(ctx context.Context, activeDeployments map[uuid.UUID]Deployment) error {
	done := make(chan time.Duration)
	go func() {
		start := time.Now()
		for k, v := range activeDeployments {
			if v.State == pb.State_STATE_DECOMMISSIONED {
				delete(activeDeployments, k)
				deploymentsTotal.Set(float64(len(activeDeployments)))
				if err := os.RemoveAll(k.String()); err != nil {
					log.Error(err)
				}
				log.WithFields(log.Fields{
					"id": k,
				}).Debug("deployment garbage collected")
			}
		}
		done <- time.Since(start)
	}()
	select {
	case duration := <-done:
		gcDurationSeconds.Observe(duration.Seconds())
		gcDurationSecondsTotal.Add(duration.Seconds())
		return nil
	case <-ctx.Done():
		gcTimeoutsTotal.Inc()
		return fmt.Errorf("garbage collection timed out")
	}
}
