package csbi

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"time"

	pb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/csbi"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	log "github.com/sirupsen/logrus"
	codes "google.golang.org/grpc/codes"
	status "google.golang.org/grpc/status"
)

type byteSize float64

// constants representing human friendly data sizes as per https://www.socketloop.com/tutorials/golang-how-to-declare-kilobyte-megabyte-gigabyte-terabyte-and-so-on
const (
	_           = iota // ignore first value by assigning to blank identifier
	KB byteSize = 1 << (10 * iota)
	MB
	GB
	TB
	PB
	EB
	ZB
	YB
)

const gostructName, manifestFileName, gostructAdditionsName string = "gostructs.go", "plugin.yml", "csbiAdditions.go"

type server struct {
	pb.UnimplementedCsbiServiceServer
	orchestrator Orchestrator
}

// Get returns a set of the requested deployments.
func (s server) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) {
	labels := prometheus.Labels{"rpc": "get"}
	start := time.Now()
	defer promEndHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	deployments := make([]*pb.Deployment, len(req.Did))
	for i, id := range req.Did {
		deploymentID, _ := uuid.Parse(id)
		dep, err := s.orchestrator.Get(deploymentID)
		if err != nil {
			return nil, handleRPCError(labels, err)
		}
		deployments[i] = &pb.Deployment{
			Id:    dep.ID.String(),
			Name:  dep.Name,
			State: dep.State,
		}
	}

	return &pb.GetResponse{
		Timestamp:   time.Now().UnixNano(),
		Deployments: deployments,
	}, nil
}

// Create creates a new cSBI; this includes generating a deployment, building
// the container, generating gostructs from the devices capabilities.
func (s server) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) {
	labels := prometheus.Labels{"rpc": "create"}
	start := promStartHook(labels, grpcRequestsTotal)
	defer promEndHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	deployments := make([]*pb.Deployment, len(req.TransportOption))
	for i, opt := range req.TransportOption {
		model, err := Discover(ctx, opt)
		if err != nil {
			return nil, handleRPCError(labels, err)
		}
		ctx = context.WithValue(ctx, "target-address", opt.Address) //nolint
		d, err := s.orchestrator.Build(ctx, model)
		if err != nil {
			return nil, handleRPCError(labels, err)
		}
		deployments[i] = &pb.Deployment{
			Id:    d.ID.String(),
			Name:  d.Name,
			State: d.State,
		}
	}
	return &pb.CreateResponse{
		Timestamp:   time.Now().UnixNano(),
		Deployments: deployments,
	}, nil
}

// GetFile sends files based on the requested file name. Currently only
// `gostructs.go`, `plugin.yml` and `csbiAdditions.go` are allowed to be
// requested.
func (s server) GetFile(req *pb.GetPayloadRequest, stream pb.CsbiService_GetFileServer) (err error) {
	log.Info("started GetGoStruct")
	labels := prometheus.Labels{"rpc": "get_go_struct"}
	start := promStartHook(labels, grpcRequestsTotal)
	defer promEndHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	var file *os.File
	defer func() {
		if ferr := file.Close(); ferr != nil {
			fErrString := ferr.Error()
			err = fmt.Errorf("InternalError=%w DeferError=%+s", err, fErrString)
		}
	}()

	switch fn := req.GetFile(); {
	case fn == gostructName:
		file, err = os.Open(filepath.Join(req.GetDid(), gostructName))
		if err != nil {
			return handleRPCError(labels, err)
		}
	case fn == manifestFileName:
		file, err = os.Open(filepath.Join(req.GetDid(), manifestFileName))
		if err != nil {
			return handleRPCError(labels, err)
		}
	case fn == gostructAdditionsName:
		file, err = os.Open(filepath.Join(req.GetDid(), gostructAdditionsName))
		if err != nil {
			return handleRPCError(labels, err)
		}
	default:
		return handleRPCError(labels, fmt.Errorf("the requested file name: %s could not be found or is not allowed to be requested", fn))
	}

	buffer := make([]byte, int(KB))

	for {
		n, err := file.Read(buffer)
		if err != nil {
			if errors.Is(err, io.EOF) {
				fmt.Println(err)
			}
			break
		}
		log.WithField("n", n).Trace("read bytes")
		payload := &pb.Payload{Chunk: buffer[:n]}
		err = stream.Send(payload)
		if err != nil {
			return handleRPCError(labels, err)
		}
	}
	return nil
}

// CreateGoStruct generates a deployment and the corresponding ygot
// `gostruct.go` file from the YANG files discovered by requesting the devices
// capabilities.
func (s server) CreateGoStruct(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) {
	labels := prometheus.Labels{"rpc": "create_gostruct"}
	start := promStartHook(labels, grpcRequestsTotal)
	defer promEndHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	deployments := make([]*pb.Deployment, len(req.TransportOption))
	for i, opt := range req.TransportOption {
		model, err := Discover(ctx, opt)
		if err != nil {
			return nil, handleRPCError(labels, err)
		}
		d, err := Generate(ctx, model, s.orchestrator.Repository(), opt.Type)
		if err != nil {
			return nil, handleRPCError(labels, err)
		}
		deployments[i] = &pb.Deployment{
			Id:    d.ID.String(),
			Name:  d.Name,
			State: d.State,
		}
	}
	return &pb.CreateResponse{
		Timestamp:   time.Now().UnixNano(),
		Deployments: deployments,
	}, nil
}

// Delete deletes the provided deployment.
func (s server) Delete(ctx context.Context, req *pb.DeleteRequest) (*pb.DeleteResponse, error) {
	labels := prometheus.Labels{"rpc": "delete"}
	start := promStartHook(labels, grpcRequestsTotal)
	defer promEndHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	for _, id := range req.Did {
		log.Infof("processing deletion request for %v", id)
		deploymentID, _ := uuid.Parse(id)
		if err := s.orchestrator.Destroy(ctx, deploymentID); err != nil {
			return nil, handleRPCError(labels, err)
		}
	}
	return &pb.DeleteResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    pb.DeleteResponse_STATUS_OK,
	}, nil
}

func handleRPCError(labels prometheus.Labels, err error) error {
	return status.Errorf(codes.Aborted, "%v", promHandleError(labels, err, grpcAPIErrorsTotal))
}
