package api

import (
	"context"
	"dataservices/commons/config"
	"dataservices/commons/constants"
	"dataservices/commons/ginparser"
	"dataservices/internal/entity"
	service "dataservices/internal/service/central"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"
	"google.golang.org/genproto/googleapis/rpc/errdetails"
	spb "google.golang.org/genproto/googleapis/rpc/status"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

const resourceURI = "dataservice/adminapi/orgs/{org_id}/all"

type AdminApiError struct {
	Code    int                   `json:"code"`
	Message string                `json:"message"`
	Details []AdminApiErrorDetail `json:"details"`
}

type AdminApiErrorDetail struct {
	Reason string `json:"reason"`
}

// Check if the input api key is authorized to visit the resource
func CheckApiKeyAppPermission(c *gin.Context) {
	ctx := c.Request.Context()
	params := ginparser.ParseAdminApiPathParameters(ctx, c.Request.RequestURI)
	var orgIDStr string
	if params.ExternalAppId != "" {
		if params.ExternalAppId != entity.DataAppAdminAPICreateOrList {
			appInfo, err := service.GetDataAppByExtId(ctx, params.ExternalAppId)
			if err != nil {
				tilog.Error(ctx, "get data app failed", zap.Error(err))
				c.JSON(http.StatusNotFound, AdminApiError{Code: http.StatusNotFound, Message: "Data app not found", Details: []AdminApiErrorDetail{}})
				c.Abort()
				return

			}

			if params.EndpointId > 0 {
				if endpoint, err := service.GetEndpoint(ctx, params.EndpointId); err != nil {
					tilog.Error(ctx, "get endpoint info failed", zap.Error(err))
					c.JSON(http.StatusNotFound, AdminApiError{Code: http.StatusNotFound, Message: "Endpoint not found", Details: []AdminApiErrorDetail{}})
					c.Abort()
					return
				} else if endpoint.DataappId != appInfo.ID {
					c.JSON(http.StatusForbidden, AdminApiError{Code: http.StatusForbidden, Message: "Unauthorized access to endpoint", Details: []AdminApiErrorDetail{}})
					c.Abort()
					return
				}
			}

			c.Request.Header.Add(constants.RequestHeaderInnerAppId, fmt.Sprintf("%d", appInfo.ID))
			orgIDStr = fmt.Sprintf("%d", appInfo.OrgId)
		} else {
			// create dataapp or list dataapp
			orgId, err := ginparser.ParseOrgIdByCreateDataAppOrListDataApp(c, ctx)
			if err != nil {
				tilog.Error(ctx, "parse org id failed", zap.Error(err))
				c.JSON(http.StatusForbidden, AdminApiError{Code: http.StatusForbidden, Message: "Request error, projectId not exist", Details: []AdminApiErrorDetail{}})
				c.Abort()
				return
			}
			tilog.Info(ctx, "middleware-adminapi-orgId", zap.String("orgId", orgId))
			c.Request.Header.Add(constants.RequestHeaderInnerOrgId, orgId)

			orgIDStr = orgId
		}

		hasPermission := false
		msg := ""
		if config.IsLocalMode() {
			hasPermission = true
		} else {
			idInfo, err := auth.GetIdentityInfoFromGinContext(c)
			if err != nil {
				tilog.Error(ctx, "check permission failed", zap.Error(err), zap.String("msg", msg))
				c.JSON(http.StatusForbidden, AdminApiError{Code: http.StatusForbidden, Message: "Permission denied", Details: []AdminApiErrorDetail{}})
				c.Abort()
				return
			}

			// set constants.RequestHeaderIdentityInfoKey, pass it to grpc gateway in header
			idInfoStr, _ := json.Marshal(idInfo)
			c.Request.Header.Add(constants.RequestHeaderIdentityInfoKey, string(idInfoStr))

			hasPermission = fmt.Sprintf("%d", idInfo.OrgID) == orgIDStr
		}
		if !hasPermission {
			tilog.Error(ctx, "api_key/user has no permission to visit this resource", zap.String("msg", msg))
			c.JSON(http.StatusForbidden, AdminApiError{Code: http.StatusForbidden, Message: "Permission denied", Details: []AdminApiErrorDetail{}})
			c.Abort()
			return
		}
	} else {
		tilog.Info(ctx, "Invalid dataapp id", zap.String("ExternalAppId", params.ExternalAppId))
		c.JSON(http.StatusNotFound, AdminApiError{Code: http.StatusNotFound, Message: "Data app not found", Details: []AdminApiErrorDetail{}})
		c.Abort()
		return
	}

	c.Next()
}

func ErrorConvertUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
		resp, err := handler(ctx, req)
		if err != nil {
			tilog.Error(ctx, "grpc failed with raw error", zap.Error(err))
			e, ok := errs.AsTiError(err)
			if !ok {
				return resp, err
			} else {
				if e.HTTPCode() >= 500 {
					// if code >= 500, update error message to avoid confusing user
					s := status.FromProto(&spb.Status{
						Code:    int32(e.HTTPCode()),
						Message: "Internal server error, please try again later",
					})
					ss, err := s.WithDetails(&errdetails.ErrorInfo{
						Reason: "INTERNAL",
						Metadata: map[string]string{
							"extraInfo": e.ErrMsg(),
						},
					})
					if err != nil {
						return resp, s.Err()
					}
					return resp, ss.Err()
				}
				// update grpc code to tierror http code if return type is tierror
				return resp, status.Error(codes.Code(e.HTTPCode()), e.ErrMsg())
			}
		}
		return resp, nil
	}
}

func CustomGrpcGatewayHTTPErrorHandler(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
	s := status.Convert(err)
	if s.Code() >= 400 && s.Code() < 600 {
		// return type is tierror, update http code to grpc code
		newerr := runtime.HTTPStatusError{
			HTTPStatus: int(s.Code()),
			Err:        err,
		}
		w.WriteHeader(int(s.Code()))
		runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, &newerr)
	} else {
		// return type from grpc framework, update grpc code to http code
		st := runtime.HTTPStatusFromCode(s.Code())
		newerr := runtime.HTTPStatusError{
			HTTPStatus: st,
			Err: status.ErrorProto(&spb.Status{
				Code:    int32(st),
				Message: s.Message(),
			}),
		}
		runtime.DefaultHTTPErrorHandler(ctx, mux, marshaler, w, r, &newerr)
	}
}

func CustomGrpcGatewayHeaderMatcher(key string) (string, bool) {
	switch strings.ToLower(key) {
	case constants.RequestHeaderAuthContentKey:
		return key, true
	case constants.RequestHeaderIdentityInfoKey:
		return key, true
	case constants.RequestHeaderInnerAppId:
		return key, true
	case constants.RequestHeaderInnerOrgId:
		return key, true
	default:
		return runtime.DefaultHeaderMatcher(key)
	}
}
