package server

import (
	"context"
	"crypto/tls"
	"fmt"
	"net/http"
	"time"

	"github.com/pingcap/errcode"

	"github.com/tidbcloud/tiinf/sdk/cors"

	"github.com/juju/errors"
	"github.com/patrickmn/go-cache"
	"github.com/tidbcloud/account/commons/database"
	adminmodel "github.com/tidbcloud/account/internel/svc/admin"
	"github.com/tidbcloud/account/internel/svc/admin/grafana"
	"github.com/tidbcloud/account/internel/svc/auditlog"
	"github.com/tidbcloud/account/internel/svc/authn"
	tenantmodel "github.com/tidbcloud/account/internel/svc/org/tenantctx"
	"github.com/tidbcloud/account/internel/svc/orgsso"
	token2 "github.com/tidbcloud/account/internel/svc/token"
	"github.com/tidbcloud/account/thirdparty/account"
	"github.com/tidbcloud/account/thirdparty/salesforce"
	"github.com/tidbcloud/pkgv2/auth/auth0"
	"github.com/tidbcloud/pkgv2/auth/util"
	"github.com/tidbcloud/pkgv2/zendesk"
	"go.uber.org/zap"
	"gopkg.in/auth0.v4/management"

	"google.golang.org/grpc"
	"google.golang.org/grpc/health"
	"google.golang.org/grpc/health/grpc_health_v1"

	"github.com/go-redis/redis/v8"
	grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
	grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
	grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator"
	"github.com/tidbcloud/account/central/jobqueue"
	"github.com/tidbcloud/account/central/jobs"
	"github.com/tidbcloud/account/central/utils"
	"github.com/tidbcloud/account/commons/config"

	"github.com/tidbcloud/account/pkg/auth/token"
	grpc_errcode "github.com/tidbcloud/account/pkg/grpc/middleware/errcode"
	grpc_log "github.com/tidbcloud/account/pkg/grpc/middleware/log"
	grpc_ratelimit "github.com/tidbcloud/account/pkg/grpc/middleware/ratelimit"
	grpc_server "github.com/tidbcloud/account/pkg/grpc/server"

	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/tiinf"
	tigrpc "github.com/tidbcloud/tiinf/transport/tigrpc/server"
)

// Server represents the central Server instance
// NOTE: Many fields in this struct are public, and act as global variables,
// which is not good practice. The solution is to avoid unnecessary dependencies
// in various layers, and/or introduce DI system to replace global dependencies.
// Currently we use public fields as a workaround to make the code clear.
type Server struct {
	Session *token2.Session

	Auth0TokenValidator *token.Validator
	Auth0Management     *management.Management
	GetClaimFunc        auth0.IntoGetClaim

	zendeskClient    *zendesk.Client
	salesforceClient *salesforce.Client

	TiInf            tiinf.TiInf
	HTTPGateway      *ServeGateway
	ProxyMiddlewares []Middleware

	AdminModel  *adminmodel.Model
	TenantModel *tenantmodel.Model
}

// NewServer initializes new server struct, used by both central and acl services
func NewServer(configPath string, grpcAuthType GRPCAuthType, errorFormat grpc_errcode.ErrorFormat) (*Server, error) {
	auth0Config := config.Global().Auth0Config
	grafanaConfig := config.Global().GrafanaConfig
	serverConfig := config.Global().ServerConfig

	centralAuth0 := auth0.Conf{
		Audience: auth0Config.Auth0Audience,
		Secret:   []byte(auth0Config.Auth0Secret),
		Domain:   auth0Config.Auth0Domain,
	}
	portalAuth0 := auth0.Conf{
		Audience:                 auth0Config.ManagementPortalAuth0Audience,
		Secret:                   []byte(auth0Config.ManagementPortalAuth0Secret),
		Domain:                   auth0Config.ManagementPortalAuth0Domain,
		ClientCredentialAudience: auth0Config.ManagementPortalAuth0ClientAudience,
	}

	grafanaClient := grafana.NewClient(grafanaConfig.Server, grafanaConfig.APIKey, grafana.DefaultHTTPClient)

	session, err := token2.GetSession()
	if err != nil {
		return nil, errors.Annotate(err, "initialize session failed")
	}

	err = initMySQLClient()
	if err != nil {
		return nil, errors.Annotate(err, "initialize mysql client failed")
	}

	_, err = initRedisClient()
	if err != nil {
		return nil, errors.Annotate(err, "initialize redis client failed")
	}

	zendeskClient, err := initZendeskClient()
	if err != nil {
		return nil, errors.Annotate(err, "initialize zendesk client failed")
	}

	salesforceClient, err := initSalesforceClient()
	if err != nil {
		return nil, errors.Annotate(err, "initialize salesforce client failed")
	}

	auth0TokenValidator, err := initAuth0TokenValidator(centralAuth0, portalAuth0)
	if err != nil {
		return nil, errors.Annotate(err, "initialize auth0 token validator failed")
	}
	orgsso.Auth0TokenValidator = auth0TokenValidator

	err = initDefaultJobQueue()
	if err != nil {
		return nil, errors.Annotate(err, "initialize default job queue failed")
	}

	auditlog.SetupAuditLogger()

	var auth0Management *management.Management
	var auth0UserManagement tenantmodel.Auth0UserManagementHandler
	if grpcAuthType == GRPCAuthTypeAuth0 {
		auth0Management, err = management.New(auth0Config.Auth0Domain, auth0Config.Auth0ManagementClientID, auth0Config.Auth0ManagementClientSecret)
		if err != nil {
			return nil, errors.Trace(err)
		}
		auth0UserManagement = auth0Management.User
		orgsso.Auth0UserManagement = auth0Management.User
	}

	tenantModel := tenantmodel.NewModel(tilog.WithContextL(context.Background()), session, auth0UserManagement, zendeskClient, salesforceClient, auth0TokenValidator, account.GetHandler())
	adminModel := adminmodel.NewModel(tilog.WithContextL(context.Background()), grafanaClient, salesforceClient, tenantModel, account.GetHandler())

	var authFunc grpc_auth.AuthFunc
	authFuncAuth0 := authn.MakeAuthGRPC(auth0TokenValidator.RawTokenValidator)

	switch grpcAuthType {
	case GRPCAuthTypeAuth0:
		authFunc = authFuncAuth0
	case GRPCAuthTypeOpenAPI:
		authFunc = AuthFuncOpenAPI
	default:
		authFunc = AuthFuncDumb
	}

	opts := []grpc_recovery.Option{
		grpc_recovery.WithRecoveryHandlerContext(utils.PanicHandler),
	}
	inf := tiinf.NewInf()
	err = inf.Init(
		tiinf.WithConfigFile(configPath),
		tiinf.WithGrpcOptions(
			tigrpc.WithUnaryInterceptors(
				grpc_middleware.ChainUnaryServer(
					errs.UnaryServerInterceptor(),
					grpc_errcode.UnaryServerInterceptor(errorFormat),
					grpc_validator.UnaryServerInterceptor(),
					grpc_auth.UnaryServerInterceptor(authFunc),
					rbacUnaryInterceptor(grpcAuthType),
					grpc_log.LogUnaryServerInterceptor(),
					grpc_ratelimit.UnaryServerInterceptor(grpc_ratelimit.GlobalLimiters),
					grpc_recovery.UnaryServerInterceptor(opts...),
				),
			),
			tigrpc.WithStreamInterceptors(
				grpc_middleware.ChainStreamServer(
					grpc_errcode.StreamServerInterceptor(errorFormat),
					grpc_validator.StreamServerInterceptor(),
					grpc_auth.StreamServerInterceptor(authFunc),
					rbacStreamInterceptor(grpcAuthType),
					grpc_recovery.StreamServerInterceptor(opts...),
				),
			),
		),
	)
	if err != nil {
		return nil, errors.Annotate(err, "init tiinf failed")
	}
	// Enable health check endpoint by default
	grpc_health_v1.RegisterHealthServer(inf.GrpcServer(), &grpc_server.HealthServerNoAuth{
		HealthServer: health.NewServer(),
	})

	httpGateway, err := newHTTPGateway(inf, fmt.Sprintf("0.0.0.0:%d", serverConfig.GrpcPort), ProdGatewayOptions, errorFormat)
	if err != nil {
		return nil, errors.Trace(err)
	}

	s := &Server{
		Session: session,

		Auth0Management:     auth0Management,
		Auth0TokenValidator: auth0TokenValidator,

		zendeskClient:    zendeskClient,
		salesforceClient: salesforceClient,

		HTTPGateway: httpGateway,
		TiInf:       inf,

		AdminModel:  &adminModel,
		TenantModel: &tenantModel,
	}

	return s, nil
}

func rbacUnaryInterceptor(grpcAuthType GRPCAuthType) grpc.UnaryServerInterceptor {
	// just intercept central
	if grpcAuthType == GRPCAuthTypeAuth0 {
		//return rbacsdk.UnaryServerInterceptor()
		return UnaryServerAccessInterceptor()

	}
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		return handler(ctx, req)
	}
}

func rbacStreamInterceptor(grpcAuthType GRPCAuthType) grpc.StreamServerInterceptor {
	// just intercept central
	if grpcAuthType == GRPCAuthTypeAuth0 {
		//return rbacsdk.StreamServerInterceptor()
		return StreamServerAccessInterceptor()

	}
	return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
		return handler(srv, ss)
	}
}

func initDefaultJobQueue() error {
	jobQueueConfig := config.Global().JobQueueConfig

	if err := jobqueue.InitDefaultJobQueue(
		context.Background(),
		jobQueueConfig.JobQueueURL,
		jobQueueConfig.JobQueueDLURL,
		jobQueueConfig.JobQueueRegion,
		jobQueueConfig.JobQueueTimeout,
	); err != nil {
		return errors.Annotate(err, "initialize default job queue failed")
	}

	err := jobs.InitDeps()
	if err != nil {
		return errors.Annotate(err, "initialize job queue deps failed")
	}

	return nil
}

func initMySQLClient() error {
	// Initialize global database.
	ctx := context.Background()
	mysqlConfig := config.Global().MysqlConfig
	tilog.Info(ctx, "Initializing global database connection", zap.String("user", mysqlConfig.User))
	_, err := database.Initialize(ctx, mysqlConfig)
	if err != nil {
		return errors.Annotate(err, "initialize global database connection failed")
	}

	return nil
}

func initRedisClient() (*redis.Client, error) {
	gatewayConfig := config.Global().OpenAPIGatewayConfig
	ctx := context.Background()

	tilog.Info(ctx, "Initializing global redis connection")
	client := redis.NewClient(&redis.Options{
		Addr:     gatewayConfig.OpenAPIGatewayRedisMasterAddress,
		Password: gatewayConfig.OpenAPIGatewayRedisPassword,
		TLSConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	})

	return client, nil
}

func initSalesforceClient() (*salesforce.Client, error) {
	var salesforceClient *salesforce.Client
	var err error
	ctx := context.Background()

	tilog.Info(ctx, "Initializing salesforce client")
	salesforceConfig := config.Global().SalesforceConfig
	if salesforceConfig.EnableSalesforce {

		accessTokenCache := cache.New(20*time.Minute, 10*time.Minute)
		salesforceCfg := &salesforce.Config{
			SalesforceDomain:       salesforceConfig.SalesforceDomain,
			SalesforceClientID:     salesforceConfig.SalesforceClientID,
			SalesforceClientSecret: salesforceConfig.SalesforceClientSecret,
			SalesforceUsername:     salesforceConfig.SalesforceUsername,
			SalesforcePassword:     salesforceConfig.SalesforcePassword,
		}
		salesforceClient, err = salesforce.NewClient(accessTokenCache, salesforceCfg)
		if err != nil {
			return nil, errors.Annotate(err, "create salesforce client failed")
		}
	}
	return salesforceClient, err
}

func initZendeskClient() (*zendesk.Client, error) {
	var zendeskClient *zendesk.Client
	var err error
	ctx := context.Background()

	tilog.Info(ctx, "Initializing zendesk client")
	zendeskConfig := config.Global().ZendeskConfig
	tilog.Info(ctx, "Initializing zendesk config", zap.Any("zendesk_config", zendeskConfig))
	if zendeskConfig.EnableZendesk {
		zendeskClient, err = zendesk.NewClient(zendeskConfig.ZendeskDomain, zendeskConfig.ZendeskEmail, zendeskConfig.ZendeskSecret)
		if err != nil {
			return nil, errors.Annotate(err, "create zendesk client failed")
		}

	}
	return zendeskClient, nil
}

func initAuth0TokenValidator(centralAuth0, portalAuth0 auth0.Conf) (*token.Validator, error) {
	jwks, err := auth0.GetJWKS(centralAuth0.Domain)
	if err != nil {
		return nil, errors.Trace(err)
	}
	managementPortalJWKs, err := auth0.GetJWKS(portalAuth0.Domain)
	if err != nil {
		return nil, errors.Trace(err)
	}

	tokenValidator := auth0.NewRawTokenValidator().
		RegisterAuth(auth0.AuthValidatorIDCentral, centralAuth0, jwks).
		RegisterAuth(auth0.AuthValidatorIDManagement, portalAuth0, managementPortalJWKs)

	return token.NewValidator(tokenValidator), nil
}

func InitProxyMiddlewares(tokenValidator *auth0.RawTokenValidator, getClaim auth0.GetClaim, testTokenModel token2.TestTokenSvc) []Middleware {
	ctx := context.Background()
	tilog.Info(ctx, "Initializing proxy middlewares")

	mwb := auth0.NewHTTPMiddlewareBuilder(tokenValidator, getClaim).
		AcceptToken(auth0.TokenRawFromHTTPHeader).
		AcceptToken(auth0.TokenFromParameter("jwt")).
		AcceptToken(auth0.TokenFromCookie).
		AcceptValidator(auth0.AuthValidatorIDCentral).
		AcceptValidator(auth0.AuthValidatorIDManagement) // proxy interface accept both central and management access
	return []Middleware{
		TryAuth,
		mwb.MWTryAuth,
		util.MWRequireAuth,
		cors.New(config.GetCorsOptions()).Handler,
	}
}

func (s *Server) Run() error {
	return s.TiInf.Run()
}

func TryAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Don't authenticate an OPTIONS request
		if r.Method == http.MethodOptions {
			next.ServeHTTP(w, r)
			return
		}

		if r.Context().Value(util.AuthProcessedByKey) != nil {
			// Processed by other auth middlewares, skip.
			next.ServeHTTP(w, r)
			return
		}

		tokenExtractors := []auth0.TokenExtractor{auth0.TokenRawFromHTTPHeader, auth0.TokenFromParameter("jwt"), auth0.TokenFromCookie}
		rawToken, err := auth0.TokenFromFirst(tokenExtractors...)(r)
		if err != nil {
			next.ServeHTTP(w, r)
			return
		}
		path := r.URL.Path
		orgID, orgFound := utils.ParseOrgIDFromPath(path)
		user, userFound, err := authn.TryGetTestUserFromToken(r.Context(), rawToken, orgID, !orgFound)
		if err != nil {
			// HACK(iven): Return errors directly when GetUser failed.
			// Normally errors should be returned from MWRequireAuth, but as billing proxy is being removed from
			// account before 2023, we use this hack method instead.
			code := errcode.CodeChain(err)
			http.Error(w, err.Error(), code.Code().HTTPCode())
			return
		}
		if !userFound {
			next.ServeHTTP(w, r)
			return
		}
		newCtx := context.WithValue(r.Context(), auth0.AuthHttpMethod, r.Method)
		newCtx = context.WithValue(newCtx, auth0.AuthValidatorIDKey, auth0.AuthValidatorIDCentral)
		newCtx = context.WithValue(newCtx, auth0.RawTokenKey, rawToken)
		newCtx = context.WithValue(newCtx, auth0.AuthTokenClaimKey, user)
		newCtx = context.WithValue(newCtx, util.AuthProcessedByKey, auth0.AuthByID)

		newRequest := r.WithContext(newCtx)
		*r = *newRequest
		next.ServeHTTP(w, r)
	})
}
