package shared

import (
	"errors"
	"io"

	"golang.org/x/net/context"

	mnepb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/networkelement"
	pb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/plugin"
	gpb "github.com/openconfig/gnmi/proto/gnmi"
	"github.com/sirupsen/logrus"
)

// DeviceModelClient is the implementation of a DeviceModel that can
// communicate via gRPC. The main purpose it to act as the gRPC client for the
// DeviceModelPlugin. A hashicorp client can consume and use it. The
// DeviceModelClient has to satisfy the DeviceModel interface and therefore
// implements all the needed methods.
type DeviceModelClient struct{ client pb.PluginClient }

// Unmarshal calls the Unmarshal method of the DeviceModelClients client. A
// request is created with the given json and path. If the operation failed an
// error is returned.
func (m *DeviceModelClient) Unmarshal(json []byte, path *gpb.Path) error {
	_, err := m.client.Unmarshal(context.Background(), &pb.UnmarshalRequest{
		Json: json,
		Path: path,
	})

	return err
}

// SetNode calls the SetNode method of the DeviceModelClients client. A request
// is created with the given path and value. If the operation failed an error is
// returned.
func (m *DeviceModelClient) SetNode(path *gpb.Path, value *gpb.TypedValue) error {
	_, err := m.client.SetNode(context.Background(), &pb.SetNodeRequest{
		Path:  path,
		Value: value,
	})

	return err
}

// GetNode calls the GetNode method of the DeviceModelClients client. A request
// is created with the given path and the provided boolean that decides if only
// the intended state should be requested. The response is returned as a slice
// of notifications. If the operation failed an error is returned.
func (m *DeviceModelClient) GetNode(path *gpb.Path, requestForIntendedState bool) ([]*gpb.Notification, error) {
	resp, err := m.client.GetNode(context.Background(), &pb.GetNodeRequest{
		Path:                    path,
		RequestForIntendedState: requestForIntendedState,
	})
	if err != nil {
		return nil, err
	}

	return resp.GetNodes(), nil
}

// DeleteNode calls the DeleteNode method of the DeviceModelClients client. A
// request is created with the given path. If the operation failed an error is
// returned.
func (m *DeviceModelClient) DeleteNode(path *gpb.Path) error {
	_, err := m.client.DeleteNode(context.Background(), &pb.DeleteNodeRequest{
		Path: path,
	})

	return err
}

// Model calls the Model method of the DeviceModelClients client. A request is
// created with the given boolean that decides if read-only nodes should be
// filtered. The response returns the model (filtered or not) as json. If the
// operation failed an error is returned.
func (m *DeviceModelClient) Model(filterReadOnly bool) ([]byte, error) {
	resp, err := m.client.Model(context.Background(), &pb.ModelRequest{
		FilterReadOnly: filterReadOnly,
	})
	if err != nil {
		return nil, err
	}

	return resp.Json, nil
}

// Diff calls the Diff method of the DeviceModelClients client. A request is
// created with the given original and modified model. The response returns the
// diff as a gnmi notifications. If the operation failed an error is returned.
func (m *DeviceModelClient) Diff(original, modified []byte) (*gpb.Notification, error) {
	resp, err := m.client.Diff(context.Background(), &pb.DiffRequest{
		Original: original,
		Modified: modified,
	})
	if err != nil {
		return nil, err
	}

	return resp.GetNotification(), nil
}

// ValidateChange calls the ValidateChange method of the DeviceModelClients
// client. A request is created with the given operation, path and value (as
// gnmi.TypedValue). The response returns the model as json. If the operation
// failed an error is returned.
func (m *DeviceModelClient) ValidateChange(operation mnepb.ApiOperation, path *gpb.Path, value *gpb.TypedValue) ([]byte, error) {
	resp, err := m.client.ValidateChange(context.Background(), &pb.ValidateChangeRequest{
		Operation: operation,
		Path:      path,
		Value:     value,
	})
	if err != nil {
		return nil, err
	}

	return resp.GetModel(), nil
}

// PruneConfigFalse calls the PruneConfigFalse method of the DeviceModelClients
// client. A request is created with the given model value (json as byte[]).
// The response returns the model with the pruned config false nodes. If the
// operation failed an error is returned.
func (m *DeviceModelClient) PruneConfigFalse(value []byte) ([]byte, error) {
	resp, err := m.client.PruneConfigFalse(context.Background(), &pb.PruneConfigFalseRequest{
		Value: value,
	})
	if err != nil {
		return nil, err
	}

	return resp.GetModel(), err
}

// SchemaTree calls the SchemaTree method of the DeviceModelClients client. The
// response stream is packed into a slice of bytes and returned.
func (m *DeviceModelClient) SchemaTreeGzip() ([]byte, error) {
	schemaTreeGzipClient, err := m.client.SchemaTreeGzip(context.Background(), &pb.SchemaTreeGzipRequest{})
	if err != nil {
		return nil, err
	}

	sTreeBytes := []byte{}

	for {
		payload, err := schemaTreeGzipClient.Recv()
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			logrus.Error(err)

			closeErr := schemaTreeGzipClient.CloseSend()
			if closeErr != nil {
				return nil, err
			}

			return nil, err
		}
		sTreeBytes = append(sTreeBytes, payload.Chunk...)
	}

	return sTreeBytes, nil
}