package main

import (
	"context"
	"database/sql"
	"fmt"
	"os"
	"time"

	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"github.com/teambition/rrule-go"
	"google.golang.org/grpc"

	"github.com/chef/automate/lib/cereal"
	grpccereal "github.com/chef/automate/lib/cereal/grpc"
	"github.com/chef/automate/lib/cereal/postgres"
	"github.com/chef/automate/lib/platform/pg"
)

var opts struct {
	Debug    bool
	Endpoint string
}

var startManagerOpts struct {
	TaskExecutorCount     int
	DequeueWorkerCount    int
	SkipScheduleExecutors bool
	SkipEchoExecutors     bool
}

var enqueueWorkflowOpts struct {
	Name      string
	TaskCount int
}

var scheduleOpts struct {
	Name    string
	RecRule string
}

var listInstanceOpts struct {
	IsRunning    string
	WorkflowName string
	InstanceName string
}

func main() {
	cmd := &cobra.Command{
		Use:           "workflow-scaffold",
		Short:         "Simple tool to play with the PG workflow experiment",
		SilenceUsage:  true,
		SilenceErrors: true,
		Args:          cobra.ExactArgs(1),
		PersistentPreRun: func(cmd *cobra.Command, args []string) {
			logrus.SetFormatter(&logrus.TextFormatter{
				FullTimestamp: true,
			})
			if opts.Debug {
				logrus.SetLevel(logrus.DebugLevel)
			}
		},
	}
	cmd.PersistentFlags().BoolVarP(
		&opts.Debug,
		"debug",
		"d",
		false,
		"Enabled debug output")
	cmd.PersistentFlags().StringVarP(
		&opts.Endpoint,
		"endpoint",
		"e",
		"",
		"grpc endpoint")

	enqueueWorkflowCmd := &cobra.Command{
		Use:           "enqueue-echo-workflow",
		Short:         "Enqueue an instance of EchoWorkflow",
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE:          enqueueEchoWorkflow,
	}

	enqueueWorkflowCmd.PersistentFlags().StringVar(
		&enqueueWorkflowOpts.Name,
		"name",
		"",
		"Name for the enqueued workflow. Generated by default")

	enqueueWorkflowCmd.PersistentFlags().IntVar(
		&enqueueWorkflowOpts.TaskCount,
		"task-count",
		20,
		"Number of tasks to enqueue")

	startManagerCmd := &cobra.Command{
		Use:           "start-manager",
		Short:         "Start a cereal manager",
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE:          startManager,
	}

	startManagerCmd.PersistentFlags().IntVar(
		&startManagerOpts.TaskExecutorCount,
		"task-worker-count",
		10,
		"Number of task workers for each of the task executors")

	startManagerCmd.PersistentFlags().IntVar(
		&startManagerOpts.DequeueWorkerCount,
		"dequeue-worker-count",
		10,
		"Number of task dequeue workers to allow")

	startManagerCmd.PersistentFlags().BoolVar(
		&startManagerOpts.SkipScheduleExecutors,
		"skip-schedule-executors",
		false,
		"Skip registering the schedule-test-workflow and schedule-test-task executors")

	startManagerCmd.PersistentFlags().BoolVar(
		&startManagerOpts.SkipEchoExecutors,
		"skip-echo-executors",
		false,
		"Skip registering the echo-workflow and echo-task executors")

	resetDBCmd := &cobra.Command{
		Use:           "reset-db DATABASE",
		Short:         "DROP and CREATE the named DB",
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE:          runResetDB,
	}

	scheduleCmd := &cobra.Command{
		Use:           "schedule-workflow",
		Short:         "Add a scheduled job",
		SilenceUsage:  true,
		SilenceErrors: true,
		RunE:          runScheduleTest,
	}

	scheduleCmd.PersistentFlags().StringVar(
		&scheduleOpts.Name,
		"name",
		"",
		"Name to use for the scheduled workflow",
	)

	scheduleCmd.PersistentFlags().StringVar(
		&scheduleOpts.RecRule,
		"recurrence-rule",
		"",
		"Name to use for the scheduled workflow",
	)

	listInstancesCmd := &cobra.Command{
		Use:  "list-instances",
		RunE: runListInstances,
	}

	listInstancesCmd.PersistentFlags().StringVar(&listInstanceOpts.IsRunning, "is-running", "", "true or false")
	listInstancesCmd.PersistentFlags().StringVar(&listInstanceOpts.WorkflowName, "workflow-name", "", "the name of the workflow")
	listInstancesCmd.PersistentFlags().StringVar(&listInstanceOpts.InstanceName, "instance-name", "", "the name of the instance")

	cmd.AddCommand(startManagerCmd)
	cmd.AddCommand(enqueueWorkflowCmd)

	cmd.AddCommand(resetDBCmd)
	cmd.AddCommand(scheduleCmd)
	cmd.AddCommand(listInstancesCmd)

	err := cmd.Execute()
	if err != nil {
		logrus.Fatal(err)
	}
}

const (
	defaultDatabaseName = "workflow"
	defaultAdminDBName  = "template1"
)

func defaultConnURIForDatabase() string {
	if os.Getenv("PG_URL") != "" {
		return os.Getenv("PG_URL")
	}
	connInfo := pg.A2ConnInfo{
		Host:  "localhost",
		Port:  5432,
		User:  "automate",
		Certs: pg.A2SuperuserCerts,
	}
	return connInfo.ConnURI(defaultDatabaseName)
}

func defaultAdminConnURI() string {
	if os.Getenv("PG_ADMIN_URL") != "" {
		return os.Getenv("PG_ADMIN_URL")
	}
	connInfo := pg.A2ConnInfo{
		Host:  "localhost",
		Port:  5432,
		User:  "automate",
		Certs: pg.A2SuperuserCerts,
	}
	return connInfo.ConnURI(defaultAdminDBName)
}

func runResetDB(_ *cobra.Command, args []string) error {
	dbName := defaultDatabaseName
	if len(args) > 0 {
		dbName = args[0]
	}

	db, err := sql.Open("postgres", defaultAdminConnURI())
	if err != nil {
		return errors.Wrap(err, "could not initialize db connection")
	}
	_, err = db.Exec(pg.DropDatabaseQuery(dbName))
	if err != nil {
		return errors.Wrap(err, "could not drop database")
	}
	_, err = db.Exec(pg.CreateDatabaseQuery(dbName))
	if err != nil {
		return errors.Wrap(err, "could not create database")
	}
	return nil
}

func getBackend() cereal.Driver {
	if opts.Endpoint != "" {
		conn, err := grpc.Dial(opts.Endpoint, grpc.WithInsecure(), grpc.WithMaxMsgSize(64*1024*1024)) // nosemgrep: go.grpc.security.grpc-client-insecure-connection.grpc-client-insecure-connection
		if err != nil {
			panic(err)
		}
		grpcBackend := grpccereal.NewGrpcBackendFromConn("test", conn)
		return grpcBackend
	}
	return postgres.NewPostgresBackend(defaultConnURIForDatabase())
}

var echoWorkflowStartTime *time.Time

var (
	EchoWorkflowName         = cereal.NewWorkflowName("echo-workflow")
	EchoTaskName             = cereal.NewTaskName("echo-task")
	ScheduleTestWorkflowName = cereal.NewWorkflowName("schedule-test")
	ScheduleTestTaskName     = cereal.NewTaskName("test task")
)

func startManager(_ *cobra.Command, args []string) error {
	b := getBackend()
	manager, err := cereal.NewManager(b, cereal.WithOnWorkflowCompleteCallback(
		func(w *cereal.WorkflowEvent) {
			if w.Instance.WorkflowName == EchoWorkflowName.String() && echoWorkflowStartTime != nil {
				logrus.Infof("echo workflow total runtime: %s", time.Since(*echoWorkflowStartTime))
			}
		}),
		cereal.WithTaskDequeueWorkers(startManagerOpts.DequeueWorkerCount),
		cereal.WithTaskPollInterval(1*time.Second),
		cereal.WithWorkflowPollInterval(1*time.Second),
	)
	if err != nil {
		return err
	}
	defer manager.Stop() // nolint: errcheck

	if !startManagerOpts.SkipScheduleExecutors {
		err = manager.RegisterWorkflowExecutor(ScheduleTestWorkflowName, &ScheduleTestWorkflow{})
		if err != nil {
			return err
		}
		err = manager.RegisterTaskExecutor(ScheduleTestTaskName, &ScheduleTestTask{}, cereal.TaskExecutorOpts{
			Workers: startManagerOpts.TaskExecutorCount,
		})
		if err != nil {
			return err
		}
	}

	if !startManagerOpts.SkipEchoExecutors {
		err = manager.RegisterWorkflowExecutor(EchoWorkflowName, &EchoWorkflow{})
		if err != nil {
			return err
		}
		err = manager.RegisterTaskExecutor(EchoTaskName, &EchoTask{}, cereal.TaskExecutorOpts{
			Workers: startManagerOpts.TaskExecutorCount,
		})
		if err != nil {
			return err
		}

	}

	err = manager.Start(context.Background())
	if err != nil {
		return errors.Wrap(err, "could not start cereal manager")
	}

	for {
		time.Sleep(1 * time.Second)
	}
}

func enqueueEchoWorkflow(_ *cobra.Command, args []string) error {
	var instanceName string
	if enqueueWorkflowOpts.Name == "" {
		instanceName = fmt.Sprintf("echo-workflow-%s", time.Now())
	} else {
		instanceName = enqueueWorkflowOpts.Name
	}

	b := getBackend()
	manager, err := cereal.NewManager(b)
	if err != nil {
		return err
	}
	defer manager.Stop() // nolint: errcheck

	params := EchoWorkflowParameters{
		Count: enqueueWorkflowOpts.TaskCount,
	}

	err = manager.EnqueueWorkflow(context.TODO(),
		EchoWorkflowName, instanceName, &params)
	if err != nil {
		logrus.WithError(err).Error("Unexpected error enqueueing workflow")
		return err
	}
	return nil
}

func runScheduleTest(_ *cobra.Command, args []string) error {
	var scheduleName string
	if scheduleOpts.Name == "" {
		scheduleName = fmt.Sprintf("scheduled-workflow-%s", time.Now())
	} else {
		scheduleName = scheduleOpts.Name
	}

	b := getBackend()
	manager, err := cereal.NewManager(b)
	if err != nil {
		return err
	}
	defer manager.Stop() // nolint: errcheck

	var recRule *rrule.RRule
	if scheduleOpts.RecRule == "" {
		recRule, err = rrule.NewRRule(rrule.ROption{
			Freq:    rrule.MINUTELY,
			Dtstart: time.Now().AddDate(0, 0, -1).Add(-55 * time.Second), // This will be due in 5 seconds
		})
		if err != nil {
			return errors.Wrap(err, "could not create rrule")
		}
	} else {
		recRule, err = rrule.StrToRRule(scheduleOpts.RecRule)
		if err != nil {
			return errors.Wrap(err, "could not create rrule")
		}
	}

	err = manager.CreateWorkflowSchedule(
		context.Background(),
		scheduleName, ScheduleTestWorkflowName, nil, true, recRule)
	if err != nil {
		if err == cereal.ErrWorkflowScheduleExists {
			logrus.Info("workflow schedule exists, updating it")
			err = manager.UpdateWorkflowScheduleByName(
				context.Background(),
				scheduleName,
				ScheduleTestWorkflowName,
				cereal.UpdateRecurrence(recRule),
				cereal.UpdateEnabled(true),
			)
			if err != nil {
				return err
			}

		} else {
			return err
		}
	}
	return nil
}

func runListInstances(_ *cobra.Command, args []string) error {
	b := getBackend()
	if err := b.Init(); err != nil {
		return err
	}

	opts := cereal.ListWorkflowOpts{}
	if listInstanceOpts.IsRunning == "true" {
		m := true
		opts.IsRunning = &m
	} else if listInstanceOpts.IsRunning == "false" {
		m := false
		opts.IsRunning = &m
	}

	if listInstanceOpts.InstanceName != "" {
		opts.InstanceName = &listInstanceOpts.InstanceName
	}

	if listInstanceOpts.WorkflowName != "" {
		opts.WorkflowName = &listInstanceOpts.WorkflowName
	}

	instances, err := b.ListWorkflowInstances(context.Background(), opts)
	if err != nil {
		return err
	}
	for _, instance := range instances {
		fmt.Printf("%13s: %s\n", "Workflow Name", instance.WorkflowName)
		fmt.Printf("%13s: %s\n", "Instance Name", instance.InstanceName)
		fmt.Printf("%13s: %s\n", "Status", string(instance.Status))
		fmt.Printf("%13s: %s\n", "Parameters", string(instance.Parameters))
		fmt.Printf("%13s: %s\n", "Payload", string(instance.Payload))
		fmt.Printf("%13s: %s\n", "Result", string(instance.Result))
		fmt.Printf("%13s: %v\n", "Err", instance.Err)
		fmt.Println("-----------------------------------")
	}

	return nil
}

type ScheduleTestTask struct{}

func (t *ScheduleTestTask) Run(ctx context.Context, _ cereal.Task) (interface{}, error) {
	logrus.Info("Running schedule test task")
	return nil, nil
}

type ScheduleTestWorkflow struct{}

func (p *ScheduleTestWorkflow) OnStart(w cereal.WorkflowInstance,
	ev cereal.StartEvent) cereal.Decision {
	var params string
	err := w.GetParameters(&params)
	if err != nil {
		return w.Fail(errors.Wrap(err, "failed to get parameters"))
	}
	logrus.WithField("params", params).Info("Doing OnStart")
	err = w.EnqueueTask(ScheduleTestTaskName, "asdf")
	if err != nil {
		return w.Fail(errors.Wrap(err, "could not enqueue task"))
	}
	return w.Continue(0)
}

func (p *ScheduleTestWorkflow) OnTaskComplete(w cereal.WorkflowInstance,
	ev cereal.TaskCompleteEvent) cereal.Decision {

	logrus.WithFields(logrus.Fields{
		"task_name": ev.TaskName,
		"enqueued":  w.TotalEnqueuedTasks(),
		"completed": w.TotalCompletedTasks(),
	}).Info("ScheduleTestWorkflow got Task Completed")
	return w.Complete()
}

func (p *ScheduleTestWorkflow) OnCancel(w cereal.WorkflowInstance,
	ev cereal.CancelEvent) cereal.Decision {
	return w.Complete()
}
