package ibclient

import (
	"fmt"
	"math/rand"
	"time"

	"github.com/sirupsen/logrus"
)

const (
	timeout     int32  = 60 // in seconds
	freeLockVal string = "Available"
)

type Lock interface {
	Lock() error
	UnLock(force bool) error
}

type NetworkViewLock struct {
	Name          string
	ObjMgr        *ObjectManager
	LockEA        string
	LockTimeoutEA string
}

func (l *NetworkViewLock) createLockRequest() *MultiRequest {

	req := NewMultiRequest(
		[]*RequestBody{
			&RequestBody{
				Method: "GET",
				Object: "networkview",
				Data: map[string]interface{}{
					"name":         l.Name,
					"*" + l.LockEA: freeLockVal,
				},
				Args: map[string]string{
					"_return_fields": "extattrs",
				},
				AssignState: map[string]string{
					"NET_VIEW_REF": "_ref",
				},
				Discard: true,
			},
			&RequestBody{
				Method: "PUT",
				Object: "##STATE:NET_VIEW_REF:##",
				Data: map[string]interface{}{
					"extattrs+": map[string]interface{}{
						l.LockEA: map[string]string{
							"value": l.ObjMgr.tenantID,
						},
						l.LockTimeoutEA: map[string]int32{
							"value": int32(time.Now().Unix()),
						},
					},
				},
				EnableSubstitution: true,
				Discard:            true,
			},
			&RequestBody{
				Method: "GET",
				Object: "##STATE:NET_VIEW_REF:##",
				Args: map[string]string{
					"_return_fields": "extattrs",
				},
				AssignState: map[string]string{
					"DOCKER-ID": "*" + l.LockEA,
				},
				EnableSubstitution: true,
				Discard:            true,
			},
			&RequestBody{
				Method: "STATE:DISPLAY",
			},
		},
	)

	return req
}

func (l *NetworkViewLock) createUnlockRequest(force bool) *MultiRequest {

	getData := map[string]interface{}{"name": l.Name}
	if !force {
		getData["*"+l.LockEA] = l.ObjMgr.tenantID
	}

	req := NewMultiRequest(
		[]*RequestBody{
			&RequestBody{
				Method: "GET",
				Object: "networkview",
				Data:   getData,
				Args: map[string]string{
					"_return_fields": "extattrs",
				},
				AssignState: map[string]string{
					"NET_VIEW_REF": "_ref",
				},
				Discard: true,
			},
			&RequestBody{
				Method: "PUT",
				Object: "##STATE:NET_VIEW_REF:##",
				Data: map[string]interface{}{
					"extattrs+": map[string]interface{}{
						l.LockEA: map[string]string{
							"value": freeLockVal,
						},
					},
				},
				EnableSubstitution: true,
				Discard:            true,
			},
			&RequestBody{
				Method: "PUT",
				Object: "##STATE:NET_VIEW_REF:##",
				Data: map[string]interface{}{
					"extattrs-": map[string]interface{}{
						l.LockTimeoutEA: map[string]interface{}{},
					},
				},
				EnableSubstitution: true,
				Discard:            true,
			},
			&RequestBody{
				Method: "GET",
				Object: "##STATE:NET_VIEW_REF:##",
				Args: map[string]string{
					"_return_fields": "extattrs",
				},
				AssignState: map[string]string{
					"DOCKER-ID": "*" + l.LockEA,
				},
				EnableSubstitution: true,
				Discard:            true,
			},
			&RequestBody{
				Method: "STATE:DISPLAY",
			},
		},
	)

	return req
}

func (l *NetworkViewLock) getLock() bool {
	logrus.Debugf("Creating lock on network niew %s\n", l.Name)
	req := l.createLockRequest()
	res, err := l.ObjMgr.CreateMultiObject(req)

	if err != nil {
		logrus.Debugf("Failed to create lock on network view %s: %s\n", l.Name, err)

		//Check for Lock Timeout
		nw, err := l.ObjMgr.GetNetworkView(l.Name)
		if err != nil {
			logrus.Debugf("Failed to get the network view object for %s : %s\n", l.Name, err)
			return false
		}

		if t, ok := nw.Ea[l.LockTimeoutEA]; ok {
			if int32(time.Now().Unix())-int32(t.(int)) > timeout {
				logrus.Debugln("Lock is timed out. Forcefully acquiring it.")
				//remove the lock forcefully and acquire it
				l.UnLock(true)
				// try to get lock again
				return l.getLock()
			}
		}
		return false
	}

	dockerID := res[0]["DOCKER-ID"]
	if dockerID == l.ObjMgr.tenantID {
		logrus.Debugln("Got the lock !!!")
		return true
	}

	return false
}

func (l *NetworkViewLock) Lock() error {

	// verify if network view exists and has EA for the lock
	nw, err := l.ObjMgr.GetNetworkView(l.Name)
	if err != nil {
		msg := fmt.Sprintf("Failed to get the network view object for %s : %s\n", l.Name, err)
		logrus.Debugf(msg)
		return fmt.Errorf(msg)
	}

	if _, ok := nw.Ea[l.LockEA]; !ok {
		nw.Ea[l.LockEA] = freeLockVal
		_, err = l.ObjMgr.UpdateNetworkView(nw.Ref, "", "", nw.Ea)
		if err != nil {
			return fmt.Errorf("Failed to Update Network view with Lock EA")
		}
	}

	retryCount := 0
	for {
		// Get lock on the network view
		lock := l.getLock()
		if lock == true {
			// Got the lock.
			logrus.Debugf("Got the lock on Network View %s\n", l.Name)
			return nil
		}

		// Lock is held by some other agent. Wait for some time and retry it again
		if retryCount >= 10 {
			return fmt.Errorf("Failed to get Lock on Network View %s", l.Name)
		}

		retryCount++
		logrus.Debugf("Lock on Network View %s not free. Retrying again %d out of 10.\n", l.Name, retryCount)
		// sleep for random time (between 1 - 10 seconds) to reduce collisions
		time.Sleep(time.Duration(rand.Intn(9)+1) * time.Second)
		continue
	}
}

func (l *NetworkViewLock) UnLock(force bool) error {
	// To unlock set the Docker-Plugin-Lock EA of network view to Available and
	// remove the Docker-Plugin-Lock-Time EA
	req := l.createUnlockRequest(force)
	res, err := l.ObjMgr.CreateMultiObject(req)

	if err != nil {
		msg := fmt.Sprintf("Failed to release lock from Network View %s: %s\n", l.Name, err)
		logrus.Errorf(msg)
		return fmt.Errorf(msg)
	}

	dockerID := res[0]["DOCKER-ID"]
	if dockerID == freeLockVal {
		logrus.Debugln("Removed the lock!")
		return nil
	}

	msg := fmt.Sprintf("Failed to release lock from Network View %s\n", l.Name)
	logrus.Errorf(msg)
	return fmt.Errorf(msg)
}
