// A jira event listener and notification sender
package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"sync"
	"syscall"

	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	glogger "gorm.io/gorm/logger"

	"jiraport/pkg/log"
	"jiraport/pkg/queue"
	"jiraport/resolver"
	ackHandler "jiraport/resolver/handler/ack"
	actionHandler "jiraport/resolver/handler/action"
	jiraHandler "jiraport/resolver/handler/jira"
	"jiraport/service/jira"
	"jiraport/service/lark"
	ntf "jiraport/service/notification"
	"jiraport/service/timer"
)

var (
	configPath = flag.String("config-path", "etc", "App config search path")
)

func main() {
	flag.Parse()
	err := LoadConfig(*configPath)
	if err != nil {
		// log have not inited, use fmt.Fprintf
		fmt.Fprintf(os.Stderr, "load config error: %s\n", err.Error())
		os.Exit(1)
	}

	if viper.GetBool("verbose") {
		// "verbose" implicitly set debug mode
		log.Init("debug")
	} else {
		log.Init(viper.GetString("mode"))
	}

	gormLoggerLevel := glogger.Warn
	if viper.GetString("mode") == "debug" {
		gormLoggerLevel = glogger.Info
	}

	// policyDB for escalation
	policyDB, err := gorm.Open(mysql.Open(viper.GetString("policy-db-dsn")), &gorm.Config{
		PrepareStmt: true,
		Logger:      glogger.Default.LogMode(gormLoggerLevel),
	})
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}

	// dataDB for data storage
	dataDB, err := gorm.Open(mysql.Open(viper.GetString("data-db-dsn")), &gorm.Config{
		PrepareStmt: false,
		Logger:      glogger.Default.LogMode(gormLoggerLevel),
	})
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}

	gin.SetMode(viper.GetString("mode"))
	r := gin.Default()

	// create queue with max capacity
	tx, rx := queue.New(16)

	// event source and utils
	// send event to resolver, and provide methods as services
	j, err := jira.New(tx, r.Group("/jira"))
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}
	l, err := lark.New(tx, r.Group("/lark"))
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}
	tm, err := timer.New(tx)
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}
	ns, err := ntf.New(tx, dataDB)
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}

	startup := make(chan bool)
	// try to complete unfinished actions after startup
	go func() {
		if viper.GetBool("retry-on-startup") {
			// will send unfinished actions to resolver
			err := ns.RetryUnfinishedActions()
			if err != nil {
				log.Error("msg", "retry unfinished actions error", "err", err)
			}
		}
		startup <- true
	}()

	// resolver handle event from queue
	rs, err := resolver.New(rx)
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}
	jiraHandler, err := jiraHandler.New(l, tm, ns)
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}
	ackHandler, err := ackHandler.New(ns)
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}
	actionHandler, err := actionHandler.New(policyDB, j, l, tm, ns)
	if err != nil {
		log.Error("err", err)
		os.Exit(1)
	}
	// dispatch event by event name
	rs.Register("JiraIssue", jiraHandler)
	rs.Register("NotificationAck", ackHandler)
	rs.Register("NotificationAction", actionHandler)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// two main group, resolver and http server
	var wg sync.WaitGroup
	wg.Add(2)

	srv := &http.Server{
		Addr:    viper.GetString("listen-address"),
		Handler: r,
	}
	go func() {
		// wait for startup
		<-startup

		log.Info("msg", "listening", "address", viper.GetString("listen-address"))
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Error("msg", "HTTP server err", "err", err)
		}
		wg.Done()
	}()

	go func() {
		// should not wait startup
		rs.Run(ctx)
		wg.Done()
	}()

	quit := make(chan os.Signal, 8)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	// stop resolver
	cancel()
	// stop http server
	if err := srv.Shutdown(context.Background()); err != nil {
		log.Error("server shutdown", err)
	}
	wg.Wait()
	log.Info("stop jiraport", "success")
}

// load config from configPath and set default value of some key
func LoadConfig(configPath string) error {
	viper.SetDefault("mode", "debug")
	viper.SetDefault("verbose", true)
	viper.SetDefault("listen-address", "debug")
	viper.SetDefault("policy-file", "policy.yml")
	viper.SetDefault("template-file", "jiraport.tmpl")
	viper.SetDefault("need-assign", false)
	viper.SetDefault("receivers", make(map[string]interface{}))
	viper.SetDefault("assign-matcher-file", "assign.yml")
	viper.SetDefault("retry-on-startup", false)
	viper.SetDefault("policy-matcher-file", "policy-route.yml")
	viper.SetDefault("event-matcher-file", "event-matcher.yml")
	viper.SetDefault("region-receivers", make(map[string]interface{}))

	viper.SetConfigName("config")
	viper.SetConfigType("yaml")
	viper.AddConfigPath(configPath)
	err := viper.ReadInConfig()
	if err != nil {
		return err
	}

	err = CheckConfig()
	if err != nil {
		return err
	}

	// resolve relative path, use config path as parent
	resolveRelPath := func(p string) string {
		if filepath.IsAbs(p) {
			return p
		}
		curConfigPath, _ := filepath.Split(viper.ConfigFileUsed())
		return filepath.Join(curConfigPath, p)

	}

	viper.Set("policy-file", resolveRelPath(viper.GetString("policy-file")))
	viper.Set("template-file", resolveRelPath(viper.GetString("template-file")))
	viper.Set("assign-matcher-file", resolveRelPath(viper.GetString("assign-matcher-file")))
	viper.Set("policy-matcher-file", resolveRelPath(viper.GetString("policy-matcher-file")))
	viper.Set("event-matcher-file", resolveRelPath(viper.GetString("event-matcher-file")))

	// set default dsn parameters
	policyDSN := viper.GetString("policy-db-dsn")
	if !strings.ContainsRune(policyDSN, '?') {
		viper.Set("policy-db-dsn", policyDSN+"?charset=utf8mb4&parseTime=True&loc=Local")
	}
	dataDSN := viper.GetString("data-db-dsn")
	if !strings.ContainsRune(dataDSN, '?') {
		viper.Set("data-db-dsn", dataDSN+"?charset=utf8mb4&parseTime=True&loc=Local")
	}

	return nil
}

// check some required key and value
func CheckConfig() error {
	if viper.GetString("notify-address") == "" {
		return errors.New("require valid notify address")
	}
	if viper.GetString("policy-db-dsn") == "" {
		return errors.New("require valid policy DSN")
	}
	if viper.GetString("data-db-dsn") == "" {
		return errors.New("require valid data DSN")
	}
	if viper.GetBool("need-assign") {
		if viper.GetString("jira-base-url") == "" {
			return errors.New("require valid jira base url")
		}
		if viper.GetString("jira-token") == "" {
			return errors.New("require valid jira token")
		}
	}
	return nil

}
