package middleware

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/ecosystem-service/api/auth"
	"github.com/tidbcloud/ecosystem-service/internal/service/grpc/mgmt"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	tiinf_auth "github.com/tidbcloud/tiinf/sdk/auth"
	"github.com/tidbcloud/tiinf/sdk/cors"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/tidbcloud/pkgv2/bizerr"
	"go.uber.org/zap"
)

func init() {
	tiinf_auth.InitAccountClientByEndpoint("account-provider-grpc:10000", 0)
}

var (
	devAllowOrigins = []string{
		"https://tidbcloud.com",
		"https://*.tidbcloud.com",
		"http://localhost:*",
		"https://*.dev.tidbcloud.com",
		"https://*-dbaas-dev.netlify.app",
		"https://dev.auth.tidbapis.com",
		"https://*partner-dev.netlify.app",
		"https://partner-console.dev.tidbcloud.com",
		"https://dev.console.tidb.io",

		// ops
		"https://*.ops-dev.tidbcloud.com",
		"https://*-management-portal.netlify.app",
	}

	stagingAllowOrigins = []string{
		"https://staging.tidbcloud.com",
		"https://*.staging.tidbcloud.com",
		"https://*staging-tidbcloud.netlify.app",
		"https://*partner-staging.netlify.app",
		"https://partner-console.staging.tidbcloud.com",
		"http://localhost:*",
		"https://staging.console.tidb.io",

		// ops
		"https://ops-staging.tidbcloud.com",
		"https://*-nostalgic-varahamihira-62ba08.netlify.app",
	}

	prodAllowOrigins = []string{
		"https://*.tidbcloud.com",
		"https://tidbcloud.com",
		"https://partner-console.tidbcloud.com",
		"https://auth.tidbapi.com",
		"https://*prod-tidbcloud.netlify.app",
		"https://console.tidb.io",

		// ops
		"https://ops.tidbcloud.com",
		"http://localhost:3000",
		"https://debug.tidbcloud.com",
	}
)

func GetCorsAllowOrigins(project string) []string {
	tilog.Info(context.Background(), fmt.Sprintf("GetCorsAllowOrigins get env: %s", project))
	switch project {
	case "nightly":
		return devAllowOrigins
	case "staging":
		return stagingAllowOrigins
	case "prod":
		return prodAllowOrigins
	default:
		return []string{"*"}
	}
}

func GetCorsAllowMethods(project string) []string {
	return []string{
		http.MethodHead,
		http.MethodGet,
		http.MethodPost,
		http.MethodPut,
		http.MethodPatch,
		http.MethodDelete,
	}
}

func GetCorsAllowHeaders(project string) []string {
	return []string{"*"}
}

func GetCorsOptions(project string) cors.Options {
	return cors.Options{
		AllowedOrigins: GetCorsAllowOrigins(project),
		AllowedMethods: GetCorsAllowMethods(project),
		AllowedHeaders: GetCorsAllowHeaders(project),
	}
}

func AuthenticationHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		path := c.FullPath()
		// Ignore methods that needs no auth
		if val, ok := auth.PathsMapNoAuth[path]; ok && val {
			c.Next()
			return
		}
		interceptor := tiinf_auth.GinGetIdentityInfoInterceptor(nil)
		interceptor(c)
	}
}

func AuthorizationHandler(mgmtSvc mgmt.MgmtSvc) gin.HandlerFunc {
	return func(c *gin.Context) {
		path := c.FullPath()
		// Ignore methods that needs no auth
		if val, ok := auth.PathsMapNoAuth[path]; ok && val {
			tilog.Info(c.Request.Context(), "ignore method")
			c.Next()
			return
		}

		identityInfo, err := tiinf_auth.GetIdentityInfoFromGinContext(c)
		if err != nil {
			log.Error("Failed to get identity info from gin context", zap.Error(err))
			c.JSON(http.StatusUnauthorized, gin.H{"error": "permission check failed"})
			c.Abort()
			return
		}
		if strings.HasPrefix(path, "/ecosystem/api/internal") {
			// Open API authentication, including API key and access token
			if identityInfo.IdentityType != tiinf_auth.AuthnIdentityTypeApiKeyAK && identityInfo.IdentityType != tiinf_auth.AuthnIdentityTypeAccessToken {
				log.Error("Invalid identity type", zap.String("path", c.FullPath()), zap.Any("identityType", identityInfo.IdentityType))
				c.JSON(http.StatusUnauthorized, gin.H{"error": "permission check failed"})
				c.Abort()
				return
			}
		} else {
			// Web console user token authentication
			if identityInfo.IdentityType != tiinf_auth.AuthnIdentityTypeUserToken {
				log.Error("Invalid identity type", zap.String("path", c.FullPath()), zap.Any("identityType", identityInfo.IdentityType))
				c.JSON(http.StatusUnauthorized, gin.H{"error": "permission check failed"})
				c.Abort()
				return
			}
			if identityInfo.UserTokenRsp == nil {
				log.Error("user token rsp is nil", zap.String("path", c.FullPath()))
				c.JSON(http.StatusUnauthorized, gin.H{"error": "permission check failed"})
				c.Abort()
				return
			}
			// RBAC only check org and project permission. We need to determine whether the cluster belongs to the org and the project.
			if err := auth.CheckPathIDs(c, mgmtSvc); err != nil {
				log.Error("Failed to check path IDs", zap.Error(err), zap.String("path", c.FullPath()))
				c.JSON(http.StatusForbidden, gin.H{"error": "cluster not belong to the org or project"})
				c.Abort()
				return
			}
		}

		ctx := c.Request.Context()
		checker, err := tiinf_auth.GetGinPermissionChecker(&tiinf_auth.PermissionCheckerCommonParam{
			IdentityF:               tiinf_auth.GinGetIdentityFunc,
			PathPattern2ResourceMap: auth.DefaultRBACPathPattern2ResourceMap,
			PathWhitelist:           auth.RBACPathWhitelist,
		})
		if err != nil {
			tilog.Warn(ctx, "GetGinPermissionChecker error", zap.Any("err", err))
			c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
			c.Abort()
			return
		}

		accessParam := tiinf_auth.GenAccessParam4GinByDefault(c)
		accessParam.Metadata[tiinf_auth.AccessMetadataResourceMapKey] = auth.DefaultRBACPathPattern2ResourceMap
		accessParam.Metadata[tiinf_auth.AccessMetadataRequestWhitelistKey] = auth.RBACPathWhitelist
		result, err := checker.Handle(ctx, accessParam)
		if !defaultPermissionCheckResultHandleFunc(c, result, err) {
			c.Abort()
			return
		}

		c.Next()
	}
}

func defaultPermissionCheckResultHandleFunc(c *gin.Context, result *tiinf_auth.AccessResult, err error) (continueRun bool) {
	tilog.Info(c.Request.Context(), "access check result", zap.Any("result", result), zap.Any("err", err))
	if err != nil {
		c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
		return false

	}
	if result.Code != tiinf_auth.AccessRetRelease {
		c.JSON(http.StatusForbidden, gin.H{"message": "no permission to access the resource"})
		return false
	}
	return true
}

type bodyLogWriter struct {
	gin.ResponseWriter
	body *bytes.Buffer
}

func (w bodyLogWriter) Write(b []byte) (int, error) {
	w.body.Write(b)
	return w.ResponseWriter.Write(b)
}

type Resp struct {
	ErrorCode    int64  `json:"err_code"`
	ErrorMessage string `json:"err_msg"`
}

type BaseResp struct {
	BaseResp *Resp `json:"base_resp"`
}

func GinMetricsHandler(urlLabelMappingFn func(c *gin.Context) string, durationHistogram *prometheus.HistogramVec, counter *prometheus.CounterVec) gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
		c.Writer = blw

		c.Next()

		url := urlLabelMappingFn(c)
		status := c.Writer.Status()
		elapsed := float64(time.Since(start)) / float64(time.Second)
		respBody := &BaseResp{}
		// ignore unmarshal error because some response body is not json
		_ = json.Unmarshal(blw.body.Bytes(), respBody)
		if respBody.BaseResp != nil && respBody.BaseResp.ErrorCode != 0 {
			switch {
			case bizerr.IsRequestErr(bizerr.ErrCode(respBody.BaseResp.ErrorCode)):
				status = http.StatusBadRequest
			case bizerr.IsInternalErr(bizerr.ErrCode(respBody.BaseResp.ErrorCode)):
				status = http.StatusInternalServerError
			case bizerr.IsBusinessErr(bizerr.ErrCode(respBody.BaseResp.ErrorCode)):
				status = http.StatusBadRequest
			default:
				status = http.StatusInternalServerError
			}
			tilog.Error(c.Request.Context(), "handle request error", zap.String("url", url),
				zap.String("method", c.Request.Method), zap.Int("status", status), zap.Any("resp", respBody))
		}

		durationHistogram.WithLabelValues(strconv.FormatInt(int64(status), 10), c.Request.Method, c.HandlerName(), c.Request.Host, url).Observe(elapsed)
		counter.WithLabelValues(strconv.FormatInt(int64(status), 10), c.Request.Method, c.HandlerName(), c.Request.Host, url).Inc()
	}
}
