/*
 * Copyright 2018-present Open Networking Foundation

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at

 * http://www.apache.org/licenses/LICENSE-2.0

 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 model

import (
	"context"
	"crypto/md5"
	"errors"
	"fmt"
	"github.com/google/uuid"
	"github.com/opencord/voltha-go/common/log"
	"reflect"
	"runtime"
	"strings"
	"sync"
)

// OperationContext holds details on the information used during an operation
type OperationContext struct {
	Path      string
	Data      interface{}
	FieldName string
	ChildKey  string
}

// NewOperationContext instantiates a new OperationContext structure
func NewOperationContext(path string, data interface{}, fieldName string, childKey string) *OperationContext {
	oc := &OperationContext{
		Path:      path,
		Data:      data,
		FieldName: fieldName,
		ChildKey:  childKey,
	}
	return oc
}

// Update applies new data to the context structure
func (oc *OperationContext) Update(data interface{}) *OperationContext {
	oc.Data = data
	return oc
}

// Proxy holds the information for a specific location with the data model
type Proxy struct {
	mutex      sync.RWMutex
	Root       *root
	Node       *node
	ParentNode *node
	Path       string
	FullPath   string
	Exclusive  bool
	Callbacks  map[CallbackType]map[string]*CallbackTuple
	operation  ProxyOperation
}

// NewProxy instantiates a new proxy to a specific location
func NewProxy(root *root, node *node, parentNode *node, path string, fullPath string, exclusive bool) *Proxy {
	callbacks := make(map[CallbackType]map[string]*CallbackTuple)
	if fullPath == "/" {
		fullPath = ""
	}
	p := &Proxy{
		Root:       root,
		Node:       node,
		ParentNode: parentNode,
		Exclusive:  exclusive,
		Path:       path,
		FullPath:   fullPath,
		Callbacks:  callbacks,
	}
	return p
}

// GetRoot returns the root attribute of the proxy
func (p *Proxy) GetRoot() *root {
	return p.Root
}

// getPath returns the path attribute of the proxy
func (p *Proxy) getPath() string {
	return p.Path
}

// getFullPath returns the full path attribute of the proxy
func (p *Proxy) getFullPath() string {
	return p.FullPath
}

// getCallbacks returns the full list of callbacks associated to the proxy
func (p *Proxy) getCallbacks(callbackType CallbackType) map[string]*CallbackTuple {
	p.mutex.RLock()
	defer p.mutex.RUnlock()

	if p != nil {
		if cb, exists := p.Callbacks[callbackType]; exists {
			return cb
		}
	} else {
		log.Debugw("proxy-is-nil", log.Fields{"callback-type": callbackType.String()})
	}
	return nil
}

// getCallback returns a specific callback matching the type and function hash
func (p *Proxy) getCallback(callbackType CallbackType, funcHash string) *CallbackTuple {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	if tuple, exists := p.Callbacks[callbackType][funcHash]; exists {
		return tuple
	}
	return nil
}

// setCallbacks applies a callbacks list to a type
func (p *Proxy) setCallbacks(callbackType CallbackType, callbacks map[string]*CallbackTuple) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	p.Callbacks[callbackType] = callbacks
}

// setCallback applies a callback to a type and hash value
func (p *Proxy) setCallback(callbackType CallbackType, funcHash string, tuple *CallbackTuple) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	p.Callbacks[callbackType][funcHash] = tuple
}

// DeleteCallback removes a callback matching the type and hash
func (p *Proxy) DeleteCallback(callbackType CallbackType, funcHash string) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	delete(p.Callbacks[callbackType], funcHash)
}

// CallbackType is an enumerated value to express when a callback should be executed
type ProxyOperation uint8

// Enumerated list of callback types
const (
	PROXY_NONE ProxyOperation = iota
	PROXY_GET
	PROXY_LIST
	PROXY_ADD
	PROXY_UPDATE
	PROXY_REMOVE
	PROXY_CREATE
	PROXY_WATCH
)

var proxyOperationTypes = []string{
	"PROXY_NONE",
	"PROXY_GET",
	"PROXY_LIST",
	"PROXY_ADD",
	"PROXY_UPDATE",
	"PROXY_REMOVE",
	"PROXY_CREATE",
	"PROXY_WATCH",
}

func (t ProxyOperation) String() string {
	return proxyOperationTypes[t]
}

func (p *Proxy) GetOperation() ProxyOperation {
	p.mutex.RLock()
	defer p.mutex.RUnlock()
	return p.operation
}

func (p *Proxy) SetOperation(operation ProxyOperation) {
	p.mutex.Lock()
	defer p.mutex.Unlock()
	p.operation = operation
}

// parseForControlledPath verifies if a proxy path matches a pattern
// for locations that need to be access controlled.
func (p *Proxy) parseForControlledPath(path string) (pathLock string, controlled bool) {
	// TODO: Add other path prefixes that may need control
	if strings.HasPrefix(path, "/devices") ||
		strings.HasPrefix(path, "/logical_devices") ||
		strings.HasPrefix(path, "/adapters") {

		split := strings.SplitN(path, "/", -1)
		switch len(split) {
		case 2:
			controlled = false
			pathLock = ""
			break
		case 3:
			fallthrough
		default:
			pathLock = fmt.Sprintf("%s/%s", split[1], split[2])
			controlled = true
		}
	}
	return pathLock, controlled
}

// List will retrieve information from the data model at the specified path location
// A list operation will force access to persistence storage
func (p *Proxy) List(ctx context.Context, path string, depth int, deep bool, txid string) interface{} {
	var effectivePath string
	if path == "/" {
		effectivePath = p.getFullPath()
	} else {
		effectivePath = p.getFullPath() + path
	}

	pathLock, controlled := p.parseForControlledPath(effectivePath)

	p.SetOperation(PROXY_LIST)
	defer p.SetOperation(PROXY_NONE)

	log.Debugw("proxy-list", log.Fields{
		"path":       path,
		"effective":  effectivePath,
		"pathLock":   pathLock,
		"controlled": controlled,
		"operation":  p.GetOperation(),
	})

	rv := p.GetRoot().List(ctx, path, "", depth, deep, txid)

	return rv
}

// Get will retrieve information from the data model at the specified path location
func (p *Proxy) Get(ctx context.Context, path string, depth int, deep bool, txid string) interface{} {
	var effectivePath string
	if path == "/" {
		effectivePath = p.getFullPath()
	} else {
		effectivePath = p.getFullPath() + path
	}

	pathLock, controlled := p.parseForControlledPath(effectivePath)

	p.SetOperation(PROXY_GET)
	defer p.SetOperation(PROXY_NONE)

	log.Debugw("proxy-get", log.Fields{
		"path":       path,
		"effective":  effectivePath,
		"pathLock":   pathLock,
		"controlled": controlled,
		"operation":  p.GetOperation(),
	})

	rv := p.GetRoot().Get(ctx, path, "", depth, deep, txid)

	return rv
}

// Update will modify information in the data model at the specified location with the provided data
func (p *Proxy) Update(ctx context.Context, path string, data interface{}, strict bool, txid string) interface{} {
	if !strings.HasPrefix(path, "/") {
		log.Errorf("invalid path: %s", path)
		return nil
	}
	var fullPath string
	var effectivePath string
	if path == "/" {
		fullPath = p.getPath()
		effectivePath = p.getFullPath()
	} else {
		fullPath = p.getPath() + path
		effectivePath = p.getFullPath() + path
	}

	pathLock, controlled := p.parseForControlledPath(effectivePath)

	p.SetOperation(PROXY_UPDATE)
	defer p.SetOperation(PROXY_NONE)

	log.Debugw("proxy-update", log.Fields{
		"path":       path,
		"effective":  effectivePath,
		"full":       fullPath,
		"pathLock":   pathLock,
		"controlled": controlled,
		"operation":  p.GetOperation(),
	})

	if p.GetRoot().KvStore != nil {
		p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
		defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
	}

	result := p.GetRoot().Update(ctx, fullPath, data, strict, txid, nil)

	if result != nil {
		return result.GetData()
	}

	return nil
}

// AddWithID will insert new data at specified location.
// This method also allows the user to specify the ID of the data entry to ensure
// that access control is active while inserting the information.
func (p *Proxy) AddWithID(ctx context.Context, path string, id string, data interface{}, txid string) interface{} {
	if !strings.HasPrefix(path, "/") {
		log.Errorf("invalid path: %s", path)
		return nil
	}
	var fullPath string
	var effectivePath string
	if path == "/" {
		fullPath = p.getPath()
		effectivePath = p.getFullPath()
	} else {
		fullPath = p.getPath() + path
		effectivePath = p.getFullPath() + path + "/" + id
	}

	pathLock, controlled := p.parseForControlledPath(effectivePath)

	p.SetOperation(PROXY_ADD)
	defer p.SetOperation(PROXY_NONE)

	log.Debugw("proxy-add-with-id", log.Fields{
		"path":       path,
		"effective":  effectivePath,
		"full":       fullPath,
		"pathLock":   pathLock,
		"controlled": controlled,
		"operation":  p.GetOperation(),
	})

	if p.GetRoot().KvStore != nil {
		p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
		defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
	}

	result := p.GetRoot().Add(ctx, fullPath, data, txid, nil)

	if result != nil {
		return result.GetData()
	}

	return nil
}

// Add will insert new data at specified location.
func (p *Proxy) Add(ctx context.Context, path string, data interface{}, txid string) interface{} {
	if !strings.HasPrefix(path, "/") {
		log.Errorf("invalid path: %s", path)
		return nil
	}
	var fullPath string
	var effectivePath string
	if path == "/" {
		fullPath = p.getPath()
		effectivePath = p.getFullPath()
	} else {
		fullPath = p.getPath() + path
		effectivePath = p.getFullPath() + path
	}

	pathLock, controlled := p.parseForControlledPath(effectivePath)

	p.SetOperation(PROXY_ADD)
	defer p.SetOperation(PROXY_NONE)

	log.Debugw("proxy-add", log.Fields{
		"path":       path,
		"effective":  effectivePath,
		"full":       fullPath,
		"pathLock":   pathLock,
		"controlled": controlled,
		"operation":  p.GetOperation(),
	})

	if p.GetRoot().KvStore != nil {
		p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
		defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
	}

	result := p.GetRoot().Add(ctx, fullPath, data, txid, nil)

	if result != nil {
		return result.GetData()
	}

	return nil
}

// Remove will delete an entry at the specified location
func (p *Proxy) Remove(ctx context.Context, path string, txid string) interface{} {
	if !strings.HasPrefix(path, "/") {
		log.Errorf("invalid path: %s", path)
		return nil
	}
	var fullPath string
	var effectivePath string
	if path == "/" {
		fullPath = p.getPath()
		effectivePath = p.getFullPath()
	} else {
		fullPath = p.getPath() + path
		effectivePath = p.getFullPath() + path
	}

	pathLock, controlled := p.parseForControlledPath(effectivePath)

	p.SetOperation(PROXY_REMOVE)
	defer p.SetOperation(PROXY_NONE)

	log.Debugw("proxy-remove", log.Fields{
		"path":       path,
		"effective":  effectivePath,
		"full":       fullPath,
		"pathLock":   pathLock,
		"controlled": controlled,
		"operation":  p.GetOperation(),
	})

	if p.GetRoot().KvStore != nil {
		p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
		defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
	}

	result := p.GetRoot().Remove(ctx, fullPath, txid, nil)

	if result != nil {
		return result.GetData()
	}

	return nil
}

// CreateProxy to interact with specific path directly
func (p *Proxy) CreateProxy(ctx context.Context, path string, exclusive bool) *Proxy {
	if !strings.HasPrefix(path, "/") {
		log.Errorf("invalid path: %s", path)
		return nil
	}

	var fullPath string
	var effectivePath string
	if path == "/" {
		fullPath = p.getPath()
		effectivePath = p.getFullPath()
	} else {
		fullPath = p.getPath() + path
		effectivePath = p.getFullPath() + path
	}

	pathLock, controlled := p.parseForControlledPath(effectivePath)

	p.SetOperation(PROXY_CREATE)
	defer p.SetOperation(PROXY_NONE)

	log.Debugw("proxy-create", log.Fields{
		"path":       path,
		"effective":  effectivePath,
		"full":       fullPath,
		"pathLock":   pathLock,
		"controlled": controlled,
		"operation":  p.GetOperation(),
	})

	if p.GetRoot().KvStore != nil {
		p.GetRoot().KvStore.Client.Reserve(pathLock+"_", uuid.New().String(), ReservationTTL)
		defer p.GetRoot().KvStore.Client.ReleaseReservation(pathLock + "_")
	}

	return p.GetRoot().CreateProxy(ctx, fullPath, exclusive)
}

// OpenTransaction creates a new transaction branch to isolate operations made to the data model
func (p *Proxy) OpenTransaction() *Transaction {
	txid := p.GetRoot().MakeTxBranch()
	return NewTransaction(p, txid)
}

// commitTransaction will apply and merge modifications made in the transaction branch to the data model
func (p *Proxy) commitTransaction(txid string) {
	p.GetRoot().FoldTxBranch(txid)
}

// cancelTransaction will terminate a transaction branch along will all changes within it
func (p *Proxy) cancelTransaction(txid string) {
	p.GetRoot().DeleteTxBranch(txid)
}

// CallbackFunction is a type used to define callback functions
type CallbackFunction func(args ...interface{}) interface{}

// CallbackTuple holds the function and arguments details of a callback
type CallbackTuple struct {
	callback CallbackFunction
	args     []interface{}
}

// Execute will process the a callback with its provided arguments
func (tuple *CallbackTuple) Execute(contextArgs []interface{}) interface{} {
	args := []interface{}{}

	for _, ta := range tuple.args {
		args = append(args, ta)
	}

	if contextArgs != nil {
		for _, ca := range contextArgs {
			args = append(args, ca)
		}
	}

	return tuple.callback(args...)
}

// RegisterCallback associates a callback to the proxy
func (p *Proxy) RegisterCallback(callbackType CallbackType, callback CallbackFunction, args ...interface{}) {
	if p.getCallbacks(callbackType) == nil {
		p.setCallbacks(callbackType, make(map[string]*CallbackTuple))
	}
	funcName := runtime.FuncForPC(reflect.ValueOf(callback).Pointer()).Name()
	log.Debugf("value of function: %s", funcName)
	funcHash := fmt.Sprintf("%x", md5.Sum([]byte(funcName)))[:12]

	p.setCallback(callbackType, funcHash, &CallbackTuple{callback, args})
}

// UnregisterCallback removes references to a callback within a proxy
func (p *Proxy) UnregisterCallback(callbackType CallbackType, callback CallbackFunction, args ...interface{}) {
	if p.getCallbacks(callbackType) == nil {
		log.Errorf("no such callback type - %s", callbackType.String())
		return
	}

	funcName := runtime.FuncForPC(reflect.ValueOf(callback).Pointer()).Name()
	funcHash := fmt.Sprintf("%x", md5.Sum([]byte(funcName)))[:12]

	log.Debugf("value of function: %s", funcName)

	if p.getCallback(callbackType, funcHash) == nil {
		log.Errorf("function with hash value: '%s' not registered with callback type: '%s'", funcHash, callbackType)
		return
	}

	p.DeleteCallback(callbackType, funcHash)
}

func (p *Proxy) invoke(callback *CallbackTuple, context []interface{}) (result interface{}, err error) {
	defer func() {
		if r := recover(); r != nil {
			errStr := fmt.Sprintf("callback error occurred: %+v", r)
			err = errors.New(errStr)
			log.Error(errStr)
		}
	}()

	result = callback.Execute(context)

	return result, err
}

// InvokeCallbacks executes all callbacks associated to a specific type
func (p *Proxy) InvokeCallbacks(args ...interface{}) (result interface{}) {
	callbackType := args[0].(CallbackType)
	proceedOnError := args[1].(bool)
	context := args[2:]

	var err error

	if callbacks := p.getCallbacks(callbackType); callbacks != nil {
		p.mutex.Lock()
		for _, callback := range callbacks {
			if result, err = p.invoke(callback, context); err != nil {
				if !proceedOnError {
					log.Info("An error occurred.  Stopping callback invocation")
					break
				}
				log.Info("An error occurred.  Invoking next callback")
			}
		}
		p.mutex.Unlock()
	}

	return result
}
