package gosdn

import (
	"context"
	"net"
	"net/http"
	"os"
	"os/signal"
	"sync"
	"syscall"

	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/viper"
	"google.golang.org/grpc"

	pb "code.fbi.h-da.de/danet/api/go/gosdn/core"
	cpb "code.fbi.h-da.de/danet/api/go/gosdn/csbi"
	ppb "code.fbi.h-da.de/danet/api/go/gosdn/pnd"
	spb "code.fbi.h-da.de/danet/api/go/gosdn/southbound"
	"code.fbi.h-da.de/danet/gosdn/interfaces/southbound"
	nbi "code.fbi.h-da.de/danet/gosdn/northbound/server"

	"code.fbi.h-da.de/danet/gosdn/nucleus"
)

var coreLock sync.RWMutex
var coreOnce sync.Once

// Core is the representation of the controller's core
type Core struct {
	pndc       *nucleus.PndStore
	httpServer *http.Server
	grpcServer *grpc.Server
	nbi        *nbi.NorthboundInterface
	stopChan   chan os.Signal

	csbiClient cpb.CsbiClient
}

var c *Core

func init() {
	c = &Core{
		pndc:     nucleus.NewPndStore(),
		stopChan: make(chan os.Signal, 1),
	}

	// Setting up signal capturing
	signal.Notify(c.stopChan, os.Interrupt, syscall.SIGTERM)
}

// initialize does start-up housekeeping like reading controller config files
func initialize() error {
	if err := startGrpc(); err != nil {
		return err
	}

	coreLock.Lock()
	startHttpServer()
	coreLock.Unlock()

	config := Config{}
	err := config.InitializeConfig()
	if err != nil {
		return err
	}

	return createSouthboundInterfaces(config)
}

func startGrpc() error {
	sock := viper.GetString("socket")
	lis, err := net.Listen("tcp", sock)
	if err != nil {
		return err
	}
	log.Infof("listening to %v", lis.Addr())
	c.grpcServer = grpc.NewServer()
	c.nbi = nbi.NewNBI(c.pndc)
	pb.RegisterCoreServer(c.grpcServer, c.nbi.Core)
	ppb.RegisterPndServer(c.grpcServer, c.nbi.Pnd)
	cpb.RegisterCsbiServer(c.grpcServer, c.nbi.Csbi)
	go func() {
		if err := c.grpcServer.Serve(lis); err != nil {
			log.Fatal(err)
		}
	}()

	orchestrator := viper.GetString("csbi-orchestrator")
	conn, err := grpc.Dial(orchestrator, grpc.WithInsecure())
	c.csbiClient = cpb.NewCsbiClient(conn)
	return nil
}

// createSouthboundInterfaces initializes the controller with its supported SBIs
func createSouthboundInterfaces(config Config) error {
	sbi := nucleus.NewSBI(spb.Type(config.BaseSouthBoundType), config.BaseSouthBoundUUID)
	return createPrincipalNetworkDomain(sbi, config)
}

// createPrincipalNetworkDomain initializes the controller with an initial PND
func createPrincipalNetworkDomain(s southbound.SouthboundInterface, config Config) error {
	pnd, err := nucleus.NewPND("base", "gosdn base pnd", config.BasePndUUID, s, c.csbiClient, callback)
	if err != nil {
		return err
	}
	err = c.pndc.Add(pnd)
	if err != nil {
		return err
	}
	return nil
}

// Run calls initialize to start the controller
func Run(ctx context.Context) error {
	var initError error
	coreOnce.Do(func() {
		initError = initialize()
	})
	if initError != nil {
		log.WithFields(log.Fields{}).Error(initError)
		return initError
	}
	log.WithFields(log.Fields{}).Info("initialisation finished")
	for {
		select {
		case <-c.stopChan:
			return shutdown()
		case <-ctx.Done():
			return shutdown()
		}
	}
}

func shutdown() error {
	log.Info("shutting down controller")
	coreLock.Lock()
	defer coreLock.Unlock()
	c.grpcServer.GracefulStop()
	return stopHttpServer()
}

func callback(id uuid.UUID, ch chan nucleus.DeviceDetails) {
	if ch != nil {
		c.pndc.AddPendingChannel(id, ch)
		log.Infof("pending channel %v added", id)
	} else {
		c.pndc.RemovePendingChannel(id)
		log.Infof("pending channel %v removed", id)
	}
}