package sdk

import (
	"sync"
	"time"

	mnepb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
	"code.fbi.h-da.de/danet/gosdn/controller/customerrs"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	gpb "github.com/openconfig/gnmi/proto/gnmi"
	"github.com/openconfig/ygot/ygot"
	"github.com/openconfig/ygot/ytypes"
	log "github.com/sirupsen/logrus"
)

// Device model satisfies the DeviceModel interface of a plugin defined in
// "code.fbi.h-da.de/danet/gosdn/controller/plugin/shared".
type DeviceModel struct {
	mu                         sync.RWMutex
	model                      ygot.ValidatedGoStruct
	schema                     *ytypes.Schema
	generatedUnmarshalFn       func([]byte, ygot.GoStruct, ...ytypes.UnmarshalOpt) error
	genereatedSchemaTreeGzipFn func() []byte
}

// NewDeviceModel creates a new DeviceModel.
func NewDeviceModel(generatedSchemaFn func() (*ytypes.Schema, error), generatedUnmarshalFn func([]byte, ygot.GoStruct, ...ytypes.UnmarshalOpt) error, genereatedSchemaTreeGzipFn func() []byte) (*DeviceModel, error) {
	schema, err := generatedSchemaFn()
	if err != nil {
		return nil, err
	}

	validatedCopy, err := createValidatedCopy(schema.Root)
	if err != nil {
		return nil, err
	}

	return &DeviceModel{
		model:                      validatedCopy,
		schema:                     schema,
		generatedUnmarshalFn:       generatedUnmarshalFn,
		genereatedSchemaTreeGzipFn: genereatedSchemaTreeGzipFn,
	}, nil
}

// Unmarshal takes a JSON as []byte and parses it into the DeviceModels GoStruct.
func (d *DeviceModel) Unmarshal(json []byte, path *gpb.Path) error {
	opts := []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}}

	d.mu.Lock()
	defer d.mu.Unlock()

	return unmarshal(d.schema, d.generatedUnmarshalFn, json, path, d.model, opts...)
}

// unmarshal parses a gNMI response to a go struct.
func unmarshal(
	schema *ytypes.Schema,
	generatedUnmarshalFn func([]byte, ygot.GoStruct, ...ytypes.UnmarshalOpt) error,
	bytes []byte,
	path *gpb.Path,
	goStruct ygot.GoStruct,
	opt ...ytypes.UnmarshalOpt) error {
	defer func() {
		if r := recover(); r != nil {
			log.Error(r.(error))
		}
	}()

	validatedDeepCopy, err := createValidatedCopy(schema.Root)
	if err != nil {
		return err
	}

	// returns the node we want to fill with the data contained in 'bytes',
	// using the specified 'path'.
	createdNode, entry, err := ytypes.GetOrCreateNode(schema.RootSchema(), validatedDeepCopy, path)
	if err != nil {
		return err
	}

	if entry.IsDir() {
		validatedCreatedNode, ok := createdNode.(ygot.ValidatedGoStruct)
		if !ok {
			return &customerrs.InvalidTypeAssertionError{
				Value: createdNode,
				Type:  (*ygot.ValidatedGoStruct)(nil),
			}
		}

		if err := generatedUnmarshalFn(bytes, validatedCreatedNode, opt...); err != nil {
			return err
		}
	} else {
		if err := generatedUnmarshalFn(bytes, validatedDeepCopy, opt...); err != nil {
			return err
		}
	}

	opts := []ygot.MergeOpt{&ygot.MergeOverwriteExistingFields{}}
	return ygot.MergeStructInto(goStruct, validatedDeepCopy, opts...)
}

// SetNode sets the current Node within the given ygot.GoStruct with the value
// provided at the given path.
func (d *DeviceModel) SetNode(path *gpb.Path, value *gpb.TypedValue) error {
	opts := []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}, &ytypes.TolerateJSONInconsistencies{}, &ytypes.IgnoreExtraFields{}}

	d.mu.Lock()
	defer d.mu.Unlock()

	return ytypes.SetNode(d.schema.RootSchema(), d.model, path, value, opts...)
}

// DeleteNode deletes the current Node within the given ygot.GoStruct at the
// given path.
func (d *DeviceModel) DeleteNode(path *gpb.Path) error {
	opts := []ytypes.DelNodeOpt{}

	d.mu.Lock()
	defer d.mu.Unlock()

	return ytypes.DeleteNode(d.schema.RootSchema(), d.model, path, opts...)
}

// GetNode gets the node for the given path.
func (d *DeviceModel) GetNode(path *gpb.Path, requestForIntendedState bool) ([]*gpb.Notification, error) {
	opts := []ytypes.GetNodeOpt{&ytypes.GetHandleWildcards{}, &ytypes.GetPartialKeyMatch{}}
	d.mu.RLock()
	nodes, err := ytypes.GetNode(d.schema.RootSchema(), d.model, path, opts...)
	if err != nil {
		return nil, err
	}
	d.mu.RUnlock()

	notifications := make([]*gpb.Notification, len(nodes))
	for i, node := range nodes {
		if requestForIntendedState && node.Schema.ReadOnly() {
			return nil, status.Errorf(codes.Aborted, "Error: request for intended state not allowed on read-only field")
		}

		var nodeStruct interface{} = node.Data
		if requestForIntendedState && !node.Schema.IsLeaf() {
			nodeStruct, ok := nodeStruct.(ygot.GoStruct)
			if !ok {
				return nil, status.Errorf(codes.Aborted, "Error: asserting ytypes.TreeNode to GoStruct")
			}

			err = ygot.PruneConfigFalse(node.Schema, nodeStruct)
			if err != nil {
				return nil, err
			}
		}

		generatedNotifictaions, err := genGnmiNotification(path, nodeStruct)
		if err != nil {
			return nil, status.Errorf(codes.Aborted, "%v", err)
		}

		notifications[i] = generatedNotifictaions
	}

	return notifications, nil
}

func genGnmiNotification(path *gpb.Path, val any) (*gpb.Notification, error) {
	typedVal, err := ygot.EncodeTypedValue(val, gpb.Encoding_JSON_IETF)
	if err != nil {
		return nil, err
	}
	return &gpb.Notification{
		Timestamp: time.Now().UnixNano(),
		Update: []*gpb.Update{
			{
				Path: &gpb.Path{
					Elem: path.GetElem(),
				},
				Val: typedVal,
			},
		},
	}, nil
}

// Model returns the current model as byte slice representing a JSON.
func (d *DeviceModel) Model(filterReadOnly bool) ([]byte, error) {
	d.mu.RLock()
	modelCopy, err := createValidatedCopy(d.model)
	if err != nil {
		return nil, err
	}
	d.mu.RUnlock()

	if filterReadOnly {
		if err := ygot.PruneConfigFalse(d.schema.RootSchema(), modelCopy); err != nil {
			return nil, err
		}
	}

	return ygot.Marshal7951(modelCopy, getYgotMarshal7951Config(), ygot.JSONIndent(""))
}

// SchemaTree returns the gzipped version of the SchemaTree as byte slice.
func (d *DeviceModel) SchemaTreeGzip() ([]byte, error) {
	return d.genereatedSchemaTreeGzipFn(), nil
}

// Diff returns the difference of two DeviceModels based on their JSON
// representation.
func (d *DeviceModel) Diff(original, modified []byte) (*gpb.Notification, error) {
	opts := []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}}

	originalAsValidatedCopy, err := createValidatedCopy(d.schema.Root)
	if err != nil {
		return nil, err
	}
	modifiedAsValidatedCopy, err := createValidatedCopy(d.schema.Root)
	if err != nil {
		return nil, err
	}

	if err := d.generatedUnmarshalFn(original, originalAsValidatedCopy, opts...); err != nil {
		return nil, err
	}
	if err := d.generatedUnmarshalFn(modified, modifiedAsValidatedCopy, opts...); err != nil {
		return nil, err
	}

	diffOpts := []ygot.DiffOpt{}
	return ygot.Diff(originalAsValidatedCopy, modifiedAsValidatedCopy, diffOpts...)
}

// ValidateChange validates that the given value can be set at the given path.
func (d *DeviceModel) ValidateChange(operation mnepb.ApiOperation, path *gpb.Path, value *gpb.TypedValue) ([]byte, error) {
	d.mu.RLock()
	modelCopy, err := createValidatedCopy(d.model)
	if err != nil {
		return nil, err
	}
	d.mu.RUnlock()

	switch operation {
	case mnepb.ApiOperation_API_OPERATION_DELETE:
		if err := ytypes.DeleteNode(d.schema.RootSchema(), modelCopy, path); err != nil {
			return nil, err
		}
	case mnepb.ApiOperation_API_OPERATION_REPLACE:
		if err := ytypes.DeleteNode(d.schema.RootSchema(), modelCopy, path); err != nil {
			return nil, err
		}
		opts := []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}, &ytypes.TolerateJSONInconsistencies{}, &ytypes.IgnoreExtraFields{}}
		if err := ytypes.SetNode(d.schema.RootSchema(), modelCopy, path, value, opts...); err != nil {
			return nil, err
		}
	case mnepb.ApiOperation_API_OPERATION_UPDATE:
		opts := []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}, &ytypes.TolerateJSONInconsistencies{}, &ytypes.IgnoreExtraFields{}}
		if err := ytypes.SetNode(d.schema.RootSchema(), modelCopy, path, value, opts...); err != nil {
			return nil, err
		}
	default:
		return nil, &customerrs.OperationNotSupportedError{Op: operation}
	}

	ygot.PruneEmptyBranches(modelCopy)

	return ygot.Marshal7951(modelCopy, getYgotMarshal7951Config(), ygot.JSONIndent(""))
}

// PruneConfigFalse removes all config false elements from the given model.
func (d *DeviceModel) PruneConfigFalse(value []byte) ([]byte, error) {
	validatedCopy, err := createValidatedCopy(d.schema.Root)
	if err != nil {
		return nil, err
	}
	opts := []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}}
	if err := d.generatedUnmarshalFn(value, validatedCopy, opts...); err != nil {
		return nil, err
	}
	if err := ygot.PruneConfigFalse(d.schema.RootSchema(), validatedCopy); err != nil {
		return nil, err
	}

	return ygot.Marshal7951(validatedCopy, getYgotMarshal7951Config(), ygot.JSONIndent(""))
}

// createValidatedCopy is a helper function which returns a validated
// copy of the `Device` struct.
func createValidatedCopy(toCopy ygot.GoStruct) (ygot.ValidatedGoStruct, error) {
	// create a deep copy of the schema's root
	schemaRootCopy, err := ygot.DeepCopy(toCopy)
	if err != nil {
		return nil, err
	}

	validatedCopy, ok := schemaRootCopy.(ygot.ValidatedGoStruct)
	if !ok {
		return nil, customerrs.InvalidTypeAssertionError{
			Value: validatedCopy,
			Type:  (*ygot.ValidatedGoStruct)(nil),
		}
	}

	return validatedCopy, nil
}

func getYgotMarshal7951Config() *ygot.RFC7951JSONConfig {
	return &ygot.RFC7951JSONConfig{
		AppendModuleName: true,
	}
}