// A jira event listener and notification sender
package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"

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

	"github.com/tidbcloud/jiraport/pkg/clients/clinic"
	"github.com/tidbcloud/jiraport/pkg/clients/devops"
	"github.com/tidbcloud/jiraport/pkg/clients/engineer"
	"github.com/tidbcloud/jiraport/pkg/clients/jira"
	lambdacli "github.com/tidbcloud/jiraport/pkg/clients/lambda"
	"github.com/tidbcloud/jiraport/pkg/clients/lark"
	"github.com/tidbcloud/jiraport/pkg/clients/oncall"
	"github.com/tidbcloud/jiraport/pkg/db"
	"github.com/tidbcloud/jiraport/pkg/resolver"
	ackHandler "github.com/tidbcloud/jiraport/pkg/resolver/handler/ack"
	actionHandler "github.com/tidbcloud/jiraport/pkg/resolver/handler/action"
	hookHandler "github.com/tidbcloud/jiraport/pkg/resolver/handler/hooks"
	jiraHandler "github.com/tidbcloud/jiraport/pkg/resolver/handler/jira"
	ntf "github.com/tidbcloud/jiraport/pkg/resolver/notification"
)

var (
	configPath = os.Getenv("CONFIG_PATH")
	logLevel   = os.Getenv("LOG_LEVEL")
)

// Enterpoint of the whole Jiraport.
// 1. Read config and set some default value.
// 2. Build program from services and resolver.
// 3. Start Gin to accept data from Jira and other external api caller.
// 4. Start resolver to handle event from services
func main() {
	// parse flag and load config
	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)
	}

	slogLevel := slog.LevelInfo
	if strings.ToLower(logLevel) == "debug" {
		slogLevel = slog.LevelDebug
	}
	opts := &slog.HandlerOptions{
		AddSource: true,
		Level:     slogLevel,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.SourceKey {
				s := a.Value.Any().(*slog.Source)
				s.File = path.Base(s.File)
			}
			return a
		},
	}
	loggerHandler := slog.NewTextHandler(os.Stdout, opts)
	logger := slog.New(loggerHandler)
	gormLoggerLevel := glogger.Warn

	// policyDB for escalation, is read-only
	policyDB, err := gorm.Open(mysql.Open(viper.GetString("policy-db-dsn")), &gorm.Config{
		PrepareStmt: true,
		Logger:      glogger.Default.LogMode(gormLoggerLevel),
	})
	if err != nil {
		logger.Error("policy db init failed", "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 {
		logger.Error("data db init failed", "err", err)
		os.Exit(1)
	}

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

	dbClient, err := db.New(dataDB)
	if err != nil {
		logger.Error("db client init failed", "err", err)
		os.Exit(1)
	}

	var lambdaClient lambdacli.Interface
	if resolverURL := viper.GetString("resolver-url"); resolverURL != "" {
		lambdaClient = lambdacli.NewHTTPClient(logger, resolverURL)
	} else if viper.GetBool("is-local-dev") {
		// lambdaClient, err = lambdacli.NewLocalClient(logger, viper.GetString("resolver-function-name"))
		// ...
		logger.Error("is-local-dev requires resolver-url to be set for http mode")
		os.Exit(1)
	} else {
		lambdaClient, err = lambdacli.New(logger, viper.GetString("resolver-function-name"))
		if err != nil {
			logger.Error("lambda client init failed", "err", err)
			os.Exit(1)
		}
	}

	engineerClient, err := engineer.New(policyDB)
	if err != nil {
		logger.Error("engineer client init failed", "err", err)
		os.Exit(1)
	}

	// clinic service provide cluster info.
	clinicClient, err := clinic.New(
		infoDB,
		policyDB,
		viper.GetString("clinic-addr"),
		viper.GetString("clinic-token"),
	)
	if err != nil {
		logger.Error("clinic client init failed", "err", err)
		os.Exit(1)
	}

	devopsClient, err := devops.New(
		viper.GetString("devops-addr"),
		viper.GetString("devops-apikey"),
	)
	if err != nil {
		logger.Error("devops client init failed", "err", err)
		os.Exit(1)
	}

	// oncall client is optional: an empty oncall-addr returns a nil client, in
	// which case O11Y assignment falls back to the matcher-based logic.
	oncallClient, err := oncall.New(
		viper.GetString("oncall-addr"),
		viper.GetString("oncall-apikey"),
	)
	if err != nil {
		logger.Error("oncall client init failed", "err", err)
		os.Exit(1)
	}

	larkClient, err := lark.NewClient()
	if err != nil {
		logger.Error("lark client init failed", "err", err)
		os.Exit(1)
	}

	jiraClient, err := jira.NewClient(logger, engineerClient)
	if err != nil {
		logger.Error("jira client init failed", "err", err)
		os.Exit(1)
	}

	// notification service provide notification related operation
	notificationSvc, err := ntf.NewNotificationService(logger, dbClient, lambdaClient)
	if err != nil {
		logger.Error("notification svc init failed", "err", err)
		os.Exit(1)
	}

	// jira handler handle jira webhook event
	jiraHandler, err := jiraHandler.New(logger, dbClient, notificationSvc, engineerClient)
	if err != nil {
		logger.Error("jira handler init failed", "err", err)
		os.Exit(1)
	}

	// ack handler handle ack event
	ackHandler, err := ackHandler.New(logger, dbClient, notificationSvc, jiraClient)
	if err != nil {
		logger.Error("ack handler init failed", "err", err)
		os.Exit(1)
	}

	// hook handler handle hook event
	hookHandler, err := hookHandler.New(logger, jiraClient)
	if err != nil {
		logger.Error("hook handler init failed", "err", err)
		os.Exit(1)
	}

	// action handler handle action execution event
	actionHandler, err := actionHandler.New(
		logger,
		dbClient,
		clinicClient,
		jiraClient,
		larkClient,
		engineerClient,
		devopsClient,
		oncallClient,
		notificationSvc,
	)
	if err != nil {
		logger.Error("action handler init failed", "err", err)
		os.Exit(1)
	}

	// resolver handle event from queue
	rs, err := resolver.New()
	if err != nil {
		logger.Error("resolver init failed", "err", err)
		os.Exit(1)
	}
	logger.Info("start to resolve")
	// register those handler, resolver will dispatch event by event name
	rs.Register("JiraIssue", jiraHandler)
	rs.Register("NotificationAck", ackHandler)
	rs.Register("NotificationAction", actionHandler)
	rs.Register("HookEvent", hookHandler)

	handler := func(ctx context.Context, event lambdacli.Event) error {
		logger.Info("receive event", "event", event.Name)
		return rs.Run(ctx, event)
	}

	if viper.GetBool("server-mode") {
		gin.SetMode(viper.GetString("mode"))
		r := gin.Default()
		r.GET("/healthz", func(ctx *gin.Context) { ctx.Status(http.StatusOK) })
		r.POST("/", func(c *gin.Context) {
			var event lambdacli.Event
			if err := c.ShouldBindJSON(&event); err != nil {
				logger.Error("failed to bind json", "err", err)
				c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
				return
			}
			// Use a background context or the request context.
			// Since we want to emulate async lambda invocation (fire and forget from caller perspective if we want, or wait).
			// The Lambda InvokeAsync waits for the response?
			// The interface says `InvokeAsync`, which implies we might not wait for the result.
			// But the `lambda.Invoke` with `InvocationTypeEvent` (Async) returns immediately.
			// Here, if we process it synchronously in the handler, the caller waits.
			// To truly support "async", we should push to a channel/goroutine, but then we lose reliability if pod crashes.
			// For now, let's just process it synchronously. The caller (API) will wait.
			// If we need async behavior, the caller can use a go routine or we can return immediately here.
			// However, `pkg/clients/lambda` implementation `InvokeAsync` uses `InvocationTypeEvent`, which is async.
			// The caller expects to return quickly.
			// So we should spawn a goroutine here, but we process synchronously and return 202 Accepted.
			// Using synchronous processing limits unbounded concurrency because the HTTP server limits it naturally,
			// or we can just process in request context.

			go func(ev lambdacli.Event) {
				err := handler(context.Background(), ev)
				if err != nil {
					logger.Error("handler execution failed", "err", err)
				}
			}(event)

			c.Status(http.StatusAccepted)
		})

		addr := viper.GetString("listen-address")
		if addr == "debug" {
			addr = ":8081" // Default resolver port
		}
		logger.Info("starting resolver server", "addr", addr)
		if err := r.Run(addr); err != nil {
			logger.Error("server run failed", "err", err)
			os.Exit(1)
		}
	} else {
		lambda.Start(handler)
	}
}

// load config from configPath and set default value of some key
func LoadConfig(configPath string) error {
	viper.SetDefault("mode", "debug")
	viper.SetDefault("is-local-dev", false)
	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("oncall-assign-matcher-file", "") // empty = oncall assign disabled
	viper.SetDefault("l2-matcher-file", "")            // empty = L2/SEE assign disabled
	viper.SetDefault("oncall-addr", "")                // empty = oncall service disabled
	viper.SetDefault("oncall-apikey", "")              // bearer token for the oncall service
	viper.SetDefault("fbowner-matcher-file", "fbowner.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.SetDefault("fbowner-field-name", "customfield_10634")
	viper.SetDefault("resolver-url", "")
	viper.SetDefault("server-mode", false)

	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
	viper.AutomaticEnv()

	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)

	}

	// update those file path config to absolute path
	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")))
	if sf := viper.GetString("oncall-assign-matcher-file"); sf != "" {
		viper.Set("oncall-assign-matcher-file", resolveRelPath(sf))
	}
	if sf := viper.GetString("l2-matcher-file"); sf != "" {
		viper.Set("l2-matcher-file", resolveRelPath(sf))
	}
	viper.Set("fbowner-matcher-file", resolveRelPath(viper.GetString("fbowner-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
}
