package csbi

import (
	"context"
	"fmt"
	"strings"
	"sync"

	"github.com/openconfig/goyang/pkg/yang"
	"github.com/prometheus/client_golang/prometheus"

	pb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/csbi"
	spb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/southbound"
	"github.com/google/uuid"
	gpb "github.com/openconfig/gnmi/proto/gnmi"
	"github.com/openconfig/ygot/gogen"
	"github.com/openconfig/ygot/ygen"
	log "github.com/sirupsen/logrus"
)

func init() {
	log.SetReportCaller(false)
}

var lock sync.Mutex

// Generate takes gnmi.ModelData, a Repository, and an southbound.Type
// It searches for the model contained in the models slice in the provided
// Repository. Assuming all necessary models are found Go code is generated
// and written to Disk. Depending on the southbound.Type additional files
// for either containerised or plugin mode are created.
func Generate(ctx context.Context, models []*gpb.ModelData, repository Repository, sbiType spb.Type) (Deployment, error) {
	labels := prometheus.Labels{"type": sbiType.String()}
	start := promStartHook(labels, codeGenerationsTotal)
	defer promEndHook(labels, start, codeGenerationDurationSecondsTotal, codeGenerationDurationSeconds)
	codeGenerationNumberOfModels.With(labels).Observe(float64(len(models)))
	id := uuid.New()

	if models == nil {
		models = ModelData
		id = uuid.Nil
	}

	yangFiles, errs := repository.FindYANGFiles(models)
	for _, err := range errs {
		log.Error(promHandleError(labels, err, codeGenerationErrorsTotal))
	}
	if len(yangFiles) == 0 {
		return Deployment{}, fmt.Errorf("no yang files found, too many errors")
	} else if len(yangFiles) != len(models) {
		log.Warn("could not find all models")
	}

	generator := gogen.New("GoCodeGenerator", ygen.IROptions{
		ParseOptions: ygen.ParseOpts{
			YANGParseOptions: yang.Options{
				IgnoreSubmoduleCircularDependencies: true,
				StoreUses:                           false,
			},
		},
		TransformationOptions: ygen.TransformationOpts{
			GenerateFakeRoot: true,
			FakeRootName:     "Device",
		},
	}, gogen.GoOpts{
		PackageName:          "main",
		GenerateJSONSchema:   true,
		IncludeModelData:     true,
		GenerateSimpleUnions: true,
		ValidateFunctionName: "Validate",
	})

	lock.Lock()

	searchpath, err := repository.YANGPathsWithSuffix()
	if err != nil {
		return Deployment{}, promHandleError(labels, err, codeGenerationErrorsTotal)
	}

	code, errs := generator.Generate(yangFiles, searchpath)
	for _, e := range errs {
		if strings.Contains(e.Error(), "duplicate entry interfaces at the root") {
			if strings.Contains(e.Error(), "ietf-interfaces") {
				generator.IROptions.ParseOptions.ExcludeModules = append(generator.IROptions.ParseOptions.ExcludeModules, "ietf-interfaces")
			} else {
				splitted := strings.SplitAfter(e.Error(), "new: ")
				model := strings.Split(splitted[0], "/")[1]
				generator.IROptions.ParseOptions.ExcludeModules = append(generator.IROptions.ParseOptions.ExcludeModules, model)
			}
		}
		log.Warnf("error during first round %v", promHandleError(labels, e, codeGenerationErrorsTotal))
	}

	log.Infof("excluded models: %v", generator.IROptions.ParseOptions.ExcludeModules)

	if code == nil {
		log.Info("running second round")
		code, errs = generator.Generate(yangFiles, searchpath)
	}

	lock.Unlock()

	if len(errs) != 0 {
		var handledErrs []error
		n := len(errs)
		log.Errorf("%v errors during code generation", n)
		for _, err := range errs {
			handledErrs = append(handledErrs, promHandleError(labels, err, codeGenerationErrorsTotal))
		}
		log.Errorf("error details after handling: \n%v\n", handledErrs)
	}

	if code == nil {
		return Deployment{}, promHandleError(labels, fmt.Errorf("code generation failed"), codeGenerationErrorsTotal)
	}

	if err := write(ctx, code, id.String(), sbiType); err != nil {
		return Deployment{}, promHandleError(labels, err, codeGenerationErrorsTotal)
	}

	return Deployment{
		State: pb.State_STATE_ANNOUNCED,
		ID:    id,
	}, nil
}
