//go:build unix

package dev

import (
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"

	"github.com/cirruslabs/orchard/internal/config"
	"github.com/cirruslabs/orchard/internal/controller"
	"github.com/cirruslabs/orchard/internal/netconstants"
	"github.com/cirruslabs/orchard/internal/worker"
	"github.com/cirruslabs/orchard/pkg/client"
	v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
	"github.com/spf13/cobra"
	"go.uber.org/zap"
)

var ErrFailed = errors.New("failed to run development controller and worker")

var devDataDirPath string
var apiPrefix string
var stringToStringResources map[string]string
var experimentalRPCV2 bool

func NewCommand() *cobra.Command {
	command := &cobra.Command{
		Use:   "dev",
		Short: "Run a controller and a worker for development purposes",
		RunE:  runDev,
	}

	command.Flags().StringVarP(&devDataDirPath, "data-dir", "d", ".dev-data",
		"path to persist data between runs")
	command.Flags().StringVar(&apiPrefix, "api-prefix", "",
		"prefix to prepend to all Orchard Controller API endpoints; useful when exposing Orchard Controller "+
			"behind an HTTP proxy together with other services")
	command.Flags().StringToStringVar(&stringToStringResources, "resources", map[string]string{},
		"resources that the development worker will provide")
	command.Flags().BoolVar(&experimentalRPCV2, "experimental-rpc-v2", false,
		"enable experimental RPC v2 (https://github.com/cirruslabs/orchard/issues/235)")

	return command
}

func runDev(cmd *cobra.Command, args []string) error {
	if !filepath.IsAbs(devDataDirPath) {
		pwd, err := os.Getwd()
		if err != nil {
			return err
		}
		devDataDirPath = path.Join(pwd, devDataDirPath)
	}

	// Convert resources
	resources, err := v1.NewResourcesFromStringToString(stringToStringResources)
	if err != nil {
		return fmt.Errorf("%w: %v", ErrFailed, err)
	}

	var additionalControllerOpts []controller.Option

	if apiPrefix != "" {
		additionalControllerOpts = append(additionalControllerOpts, controller.WithAPIPrefix(apiPrefix))
	}

	if experimentalRPCV2 {
		additionalControllerOpts = append(additionalControllerOpts, controller.WithExperimentalRPCV2())
	}

	devController, devWorker, err := CreateDevControllerAndWorker(devDataDirPath,
		fmt.Sprintf(":%d", netconstants.DefaultControllerPort), resources,
		additionalControllerOpts, nil)
	if err != nil {
		return err
	}
	defer devWorker.Close()

	errChan := make(chan error, 2)

	go func() {
		if err := devController.Run(cmd.Context()); err != nil {
			errChan <- err
		}
	}()

	go func() {
		if err := devWorker.Run(cmd.Context()); err != nil {
			errChan <- err
		}
	}()

	return <-errChan
}

func CreateDevControllerAndWorker(
	devDataDirPath string,
	controllerListenAddr string,
	resources v1.Resources,
	additionalControllerOpts []controller.Option,
	additionalWorkerOpts []worker.Option,
) (*controller.Controller, *worker.Worker, error) {
	// Initialize the logger
	logger, err := zap.NewDevelopment()
	if err != nil {
		return nil, nil, err
	}
	defer func() {
		if syncErr := logger.Sync(); syncErr != nil && err == nil {
			err = syncErr
		}
	}()

	dataDir, err := controller.NewDataDir(devDataDirPath)
	if err != nil {
		return nil, nil, err
	}

	controllerOpts := []controller.Option{
		controller.WithDataDir(dataDir),
		controller.WithListenAddr(controllerListenAddr),
		controller.WithInsecureAuthDisabled(),
		controller.WithSwaggerDocs(),
		controller.WithLogger(logger),
	}

	controllerOpts = append(controllerOpts, additionalControllerOpts...)

	devController, err := controller.New(controllerOpts...)
	if err != nil {
		return nil, nil, err
	}

	defaultClient, err := client.New(client.WithAddress(devController.Address()))
	if err != nil {
		return nil, nil, err
	}

	workerOpts := []worker.Option{
		worker.WithResources(resources),
		worker.WithLogger(logger),
	}

	workerOpts = append(workerOpts, additionalWorkerOpts...)

	devWorker, err := worker.New(defaultClient, workerOpts...)
	if err != nil {
		return nil, nil, err
	}

	// set local-dev context as active
	configHandle, err := config.NewHandle()
	if err != nil {
		return nil, nil, err
	}
	localContext := config.Context{URL: devController.Address()}
	err = configHandle.CreateContext("local-dev", localContext, true)
	if err != nil {
		return nil, nil, err
	}
	err = configHandle.SetDefaultContext("local-dev")
	if err != nil {
		return nil, nil, err
	}

	return devController, devWorker, nil
}
