// A jira event listener and notification sender
package main

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

	"github.com/carlmjohnson/versioninfo"
	"github.com/mitchellh/mapstructure"
	"github.com/tidbcloud/jiraport/pkg/api/hooks"
	"github.com/tidbcloud/jiraport/pkg/api/jira"
	"github.com/tidbcloud/jiraport/pkg/api/lark"
	lambdacli "github.com/tidbcloud/jiraport/pkg/clients/lambda"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"
)

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

// 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,
	}
	loggerHandler := slog.NewTextHandler(os.Stdout, opts)
	logger := slog.New(loggerHandler)

	logger.Info("starting jiraport api", "version", Version)

	// get default gin router, all http api exposed can use this router
	gin.SetMode(viper.GetString("mode"))
	r := gin.Default()
	// health check
	r.GET("/healthz", func(ctx *gin.Context) { ctx.Status(http.StatusOK) })
	r.GET("/version", func(ctx *gin.Context) { ctx.String(http.StatusOK, Version) })

	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"))
		// if err != nil {
		// 	logger.Error("lambda client init failed", "err", err)
		// 	os.Exit(1)
		// }
		// The original local client logic is removed or commented out as per the new design
		// Use HTTP client for local dev as well if possible, or adapt if needed.
		// For now let's assume if is-local-dev is true, we might want to use HTTP too if we run resolver locally.
		// But let's stick to the requested changes.
		// Since I replaced NewLocalClient with NewHTTPClient in the package, I should just use NewHTTPClient.
		// However, the original code had NewLocalClient. My previous edit REPLACED NewLocalClient with NewHTTPClient.
		// So I should use NewHTTPClient here.
		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)
		}
	}

	// jira service provide jira related api, for input from jira webhook and output to jira software.
	// jira service's exposed api will under gin route group "/jira".
	_, err = jira.New(r.Group("/jira"), logger, lambdaClient, viper.GetString("secret"))
	if err != nil {
		logger.Error("init jira client failed", "err", err)
		os.Exit(1)
	}

	// lark service provide lark related api.
	// lark service's exposed api will under gin route group "/lark".
	_, err = lark.New(r.Group("/lark"), logger, viper.GetString("notify-address"), lambdaClient)
	if err != nil {
		logger.Error("init lark client failed", "err", err)
		os.Exit(1)
	}

	// hook service provide hook related api.
	// hook service's exposed api will under gin route group "/hook".
	_, err = hooks.New(r.Group("/hook"), logger, lambdaClient, viper.GetString("secret"))
	if err != nil {
		logger.Error("init hook client failed", "err", err)
		os.Exit(1)
	}

	logRequest := func(ctx context.Context, ev map[string]interface{}) (interface{}, error) {
		if _, ok := ev["routeKey"]; ok {
			var gatewayEventV2 events.APIGatewayV2HTTPRequest
			if err := mapstructure.Decode(ev, &gatewayEventV2); err == nil {
				logger.Info("received APIGatewayV2HTTPRequest", "route key", gatewayEventV2.RouteKey, "raw path", gatewayEventV2.RawPath)
				result, err := ginadapter.NewV2(r).ProxyWithContext(ctx, gatewayEventV2)
				logger.Info("response", "result", result, "err", err)
				return result, err
			}
		}

		var albEvent events.ALBTargetGroupRequest
		if err := mapstructure.Decode(ev, &albEvent); err == nil {
			logger.Info("received ALBTargetGroupRequest", "path", albEvent.Path, "method", albEvent.HTTPMethod)
			result, err := ginadapter.NewALB(r).ProxyWithContext(ctx, albEvent)
			logger.Info("response", "result", result, "err", err)
			return result, err
		}

		logger.Info("unknown event", "event", ev)
		return nil, errors.New("unknown event")
	}

	if viper.GetBool("server-mode") {
		addr := viper.GetString("listen-address")
		if addr == "debug" {
			addr = ":8080"
		}
		logger.Info("starting server", "addr", addr)
		if err := r.Run(addr); err != nil {
			logger.Error("server run failed", "err", err)
			os.Exit(1)
		}
	} else {
		lambda.Start(logRequest)
	}
}

// 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("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")))
	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

}
