package controller

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/tidbcloud/auditlog/configs"
	"github.com/tidbcloud/auditlog/consts"
	"github.com/tidbcloud/auditlog/internal/entity"
	"github.com/tidbcloud/auditlog/types"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"github.com/tidbcloud/tiinf/sdk/auth"
	"go.uber.org/zap"
	"gorm.io/gorm"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// spec: https://pingcap.feishu.cn/wiki/ExOYwpxUfihxoYkVzQvcmJvEnue

const (
	authenticateErrorCode   = "AuthenticateError"
	paramInvalidErrorCode   = "ParamInvalidError"
	internalServerErrorCode = "InternalServerError"
)

var openApiPathPattern2ResourceMap = map[string]string{
	"/orgs/(\\d+)/v1beta1/auditLogs:checkStatus": "account/openapi/orgs/{org_id}/audit_log/check_status",
	"/orgs/(\\d+)/v1beta1/auditLogs:enable":      "account/openapi/orgs/{org_id}/audit_log/enable_status",
	"/orgs/(\\d+)/v1beta1/auditLogs:disable":     "account/openapi/orgs/{org_id}/audit_log/disable_status",
	"/orgs/(\\d+)/v1beta1/auditLogs":             "account/openapi/orgs/{org_id}/audit_log/query",
}

// create swag doc command: project root directory exec the command
// swag init -d internal/controller/ -g open_api.go --tags "AuditLog" --output docs/openapi

// @title Account System OPENAPI
// @version v1beta1
// @description This is an account system server.
// @host iam.tidbapi.com
// @BasePath /v1beta1
func RegisterOpenApiRouter(engine *gin.Engine) {
	openApiGroup := engine.Group("/v1beta1/auditLogs")
	auth.InitAccountClientByEndpoint(configs.GetConfig().AccountSvcAddr, 0)
	openApiGroup.Use(
		auth.GinGetIdentityInfoInterceptor(&auth.IdentityAuthOption{
			EnableLog: true,
		}),
		auth.GinPermissionCheckInterceptor(&auth.PermissionCheckerCommonParam{
			IdentityF:               auth.GinGetIdentityFunc,
			PathPattern2ResourceMap: openApiPathPattern2ResourceMap,
			GenAccessParam4GinF:     auth.GenAccessParam4GinWithPrefixOrgId,
			EnableLog:               true,
		}),
	)

	openApiGroup.GET("", HandleQueryAuditLogs)
	//openApiGroup.GET(":checkStatus", HandleCheckAuditLogStatus)
	//openApiGroup.POST(":enable", HandleEnableAuditLog)
	//openApiGroup.POST(":disable", HandleDisableAuditLog)
}

// @Tags
// @Summary Check Audit Log Status
// @Description Check current console audit logging status.
// @Schemes
// @Accept json
// @Produce json
// @Success 200 {object} orgAuditLogStatus
// @Failure 400  {object}  openApiError
// @Failure 500  {object}  openApiError
// @Router /auditLogs:checkStatus [GET]
func HandleCheckAuditLogStatus(c *gin.Context) {
	ctx := c.Request.Context()
	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      authenticateErrorCode,
			Status:    http.StatusBadRequest,
			MsgPrefix: "get org_id from auth info error",
		})
		return
	}

	settings, err := auditSvc.QueryOrgSettings(ctx, orgId)
	if err != nil && err != gorm.ErrRecordNotFound {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      internalServerErrorCode,
			Status:    http.StatusInternalServerError,
			MsgPrefix: "get org settings error",
		})
		return
	}
	status := "false"
	if settings != nil && settings.Status == consts.SettingsOn {
		status = "true"
	}

	openApiWriteResult(c.Writer, &orgAuditLogStatus{
		Enabled: status,
	})
}

// @Tags
// @Summary Enable Audit Log
// @Description Enables console audit logging.
// @Schemes
// @Accept json
// @Produce json
// @Success 200 {object} orgAuditLogStatus
// @Failure 400  {object}  openApiError
// @Failure 500  {object}  openApiError
// @Router /auditLogs:enable [POST]
func HandleEnableAuditLog(c *gin.Context) {
	ctx := c.Request.Context()
	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      authenticateErrorCode,
			Status:    http.StatusBadRequest,
			MsgPrefix: "get org_id from auth info error",
		})
		return
	}
	err = auditSvc.UpdateOrgSettings(ctx, orgId, consts.SettingsOn)

	details := auditlog.Details{
		auditlog.KeyEnableOrDisable: "enable",
		auditlog.KeyError:           err,
	}
	auditlog.NewEvent(ctx, auditlog.EventEnableConsoleAuditLog).ParseGIN(c).SetDetails(details.Build()).SetResult(err == nil).SetOrgID(orgId).Submit() //nolint:errcheck

	if err != nil {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      internalServerErrorCode,
			Status:    http.StatusInternalServerError,
			MsgPrefix: "update org settings error",
		})
	} else {
		openApiWriteResult(c.Writer, &orgAuditLogStatus{
			Enabled: "true",
		})
	}
}

// @Tags
// @Summary Disable Audit Log
// @Description Disables console audit logging.
// @Schemes
// @Accept json
// @Produce json
// @Success 200 {object} orgAuditLogStatus
// @Failure 400  {object}  openApiError
// @Failure 500  {object}  openApiError
// @Router /auditLogs:disable [POST]
func HandleDisableAuditLog(c *gin.Context) {
	ctx := c.Request.Context()
	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      authenticateErrorCode,
			Status:    http.StatusBadRequest,
			MsgPrefix: "get org_id from auth info error",
		})
		return
	}
	err = auditSvc.UpdateOrgSettings(ctx, orgId, consts.SettingsOff)

	details := auditlog.Details{
		auditlog.KeyEnableOrDisable: "disable",
		auditlog.KeyError:           err,
	}
	auditlog.NewEvent(ctx, auditlog.EventEnableConsoleAuditLog).ParseGIN(c).SetDetails(details.Build()).SetResult(err == nil).SetOrgID(orgId).Submit() //nolint:errcheck

	if err != nil {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      internalServerErrorCode,
			Status:    http.StatusInternalServerError,
			MsgPrefix: "update org settings error",
		})
	} else {
		openApiWriteResult(c.Writer, &orgAuditLogStatus{
			Enabled: "false",
		})
	}
}

// @Tags AuditLog
// @Summary List Audit Logs
// @Description Lists all console audit logs from current organization.
// @Schemes
// @Accept json
// @Produce json
// @Param pageToken query string false "The pagination token received from a previous List Audit Logs request. Use this token to retrieve the next page of results."
// @Param pageSize query int false "The maximum number of audit logs to return. If not specified, at most 100 audit logs will be returned."
// @Param startTime query int false "The start timestamp to list audit logs, in the ISO 8601 format."
// @Param endTime query int false "The end timestamp to list audit logs, in the ISO 8601 format."
// @Param auditEventTypes query string false "The auditing event types to include in the audit log list. Refer Console audit event types for more information."
// @Param keyword query string false "The search keyword to include in the audit log list."
// @Success 200 {object} queryAuditLogRsp
// @Failure 400  {object}  openApiError
// @Failure 500  {object}  openApiError
// @Router /auditLogs [GET]
func HandleQueryAuditLogs(c *gin.Context) {
	ctx := c.Request.Context()
	var req queryAuditLogReq
	if err := c.ShouldBindQuery(&req); err != nil {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      paramInvalidErrorCode,
			Status:    http.StatusBadRequest,
			MsgPrefix: err.Error(),
		})
		return
	}
	tilog.Info(ctx, "HandleQueryAuditLogs request", zap.Any("req", req))
	var startTime, endTime int64 = 0, 0
	if req.StartTime != "" {
		t, err := time.Parse(time.RFC3339, req.StartTime)
		if err != nil {
			openApiWriteError(ctx, c.Writer, &openApiError{
				Code:      paramInvalidErrorCode,
				Status:    http.StatusBadRequest,
				MsgPrefix: err.Error(),
			})
			return
		}
		startTime = t.Unix()
	}
	if req.EndTime != "" {
		t, err := time.Parse(time.RFC3339, req.EndTime)
		if err != nil {
			openApiWriteError(ctx, c.Writer, &openApiError{
				Code:      paramInvalidErrorCode,
				Status:    http.StatusBadRequest,
				MsgPrefix: err.Error(),
			})
			return
		}
		endTime = t.Unix()
	}
	if startTime != 0 && endTime != 0 && startTime > endTime {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      paramInvalidErrorCode,
			Status:    http.StatusBadRequest,
			MsgPrefix: "startTime must be less than endTime",
		})
		return
	}
	queryEventTypes := ""
	if len(req.AuditEventTypes) > 0 {
		var tempEventTypes []string
		codeMapper := flipEventCodeMapper()
		for _, t := range req.AuditEventTypes {
			eventType, ok := codeMapper[t]
			if !ok {
				openApiWriteError(ctx, c.Writer, &openApiError{
					Code:      paramInvalidErrorCode,
					Status:    http.StatusBadRequest,
					MsgPrefix: fmt.Sprintf("exist invalid auditEventType: %s", t),
				})
				return
			}
			tempEventTypes = append(tempEventTypes, fmt.Sprintf("%d", eventType))
		}
		queryEventTypes = strings.Join(tempEventTypes, ",")
	}
	pageSize := req.PageSize
	if pageSize <= 0 || pageSize > 100 {
		pageSize = 100
	}
	prevID, err := parsePageToken(req.PageToken)
	if err != nil {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      paramInvalidErrorCode,
			Status:    http.StatusBadRequest,
			MsgPrefix: err.Error(),
		})
		return
	}
	orgId, err := getOrgIdFromAuthInfo(c)
	if err != nil {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      authenticateErrorCode,
			Status:    http.StatusBadRequest,
			MsgPrefix: "get org_id from auth info error",
		})
		return
	}

	var payload = types.QueryAuditLogFilter{
		OpenApiQuery: true,
		OrgID:        orgId,
		EventTypes:   queryEventTypes,
		StartedAt:    startTime,
		EndedAt:      endTime,
		Search:       req.Keyword,
		PrevID:       prevID,
		PerPage:      pageSize,
		AscOrder:     true,
	}
	tilog.Info(ctx, "HandleQueryAuditLogs", zap.Any("payload", payload))
	events, total, err := auditSvc.QueryEvents(ctx, payload)
	if err != nil {
		openApiWriteError(ctx, c.Writer, &openApiError{
			Code:      internalServerErrorCode,
			Status:    http.StatusInternalServerError,
			MsgPrefix: err.Error(),
		})
		return
	}

	var hasMore = true
	if len(events) < pageSize {
		hasMore = false
	}
	var lastID uint
	var retAuditLogs []orgAuditLog
	for _, e := range events {
		retAuditLogs = append(retAuditLogs, convertAuditLog(e))
		lastID = e.ID
	}

	rsp := queryAuditLogRsp{
		TotalSize:     total,
		NextPageToken: createNextPageToken(hasMore, lastID),
		AuditLogs:     retAuditLogs,
	}
	openApiWriteResult(c.Writer, &rsp)
}

func flipEventCodeMapper() map[string]auditlog.EventType {
	m := map[string]auditlog.EventType{}
	for i, s := range consts.EventCodeMapper {
		m[s] = i
	}
	return m
}

func convertAuditLog(e entity.Event) orgAuditLog {
	return orgAuditLog{
		ID:                  fmt.Sprintf("%d", e.ID),
		EventType:           consts.GetEventTypeName(e.Type),
		EndTime:             e.EndsAt.Format(time.RFC3339),
		OperatorType:        consts.GetOperatorTypeName(e.OperatorType),
		OperatorID:          fmt.Sprintf("%d", e.OperatorID),
		OperatorEmail:       e.OperatorEmail,
		OperatorIP:          e.OperatorIP,
		OperatorLoginMethod: e.OperatorLoginMethod,
		OrgID:               fmt.Sprintf("%d", e.OrgID),
		OrgName:             e.OrgName,
		ProjectID:           fmt.Sprintf("%d", e.ProjectID),
		ProjectName:         e.ProjectName,
		ClusterID:           fmt.Sprintf("%d", e.ClusterID),
		ClusterName:         e.ClusterName,
		TraceID:             e.TraceID,
		Result:              consts.GetResultName(e.Result),
		Details:             e.Details,
		ClusterType:         consts.GetClusterTypeName(e.ClusterType),
	}
}

func createNextPageToken(hasMore bool, prevID uint) string {
	if !hasMore {
		return ""
	}
	return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("prev:%d", prevID)))
}

func parsePageToken(pageToken string) (int64, error) {
	if pageToken == "" {
		return -1, nil
	}
	decoded, err := base64.StdEncoding.DecodeString(pageToken)
	if err != nil {
		return 0, err
	}
	parts := strings.Split(string(decoded), ":")
	if len(parts) != 2 {
		return 0, errs.NewBadRequest("invalid page token length", nil)
	} else if parts[0] != "prev" {
		return 0, errs.NewBadRequest("invalid page token prev", nil)
	}
	prevID, err := strconv.ParseInt(parts[1], 10, 64)
	if err != nil {
		return 0, errs.NewBadRequest("invalid page token id", nil)
	}
	return prevID, nil
}

func getOrgIdFromAuthInfo(c *gin.Context) (uint64, error) {
	authInfo, err := auth.GetIdentityInfoFromGinContext(c)
	if err != nil {
		return 0, err
	}
	if authInfo.IdentityType != auth.AuthnIdentityTypeApiKeyAK {
		return 0, errs.NewBadRequest("only support api key access", nil)
	}
	apiKeyRsp := authInfo.ApiKeyRsp
	return auth.GetOrgIdFromApiKeyRsp(c.Request.Context(), apiKeyRsp)
}

func openApiWriteError(ctx context.Context, rw http.ResponseWriter, err *openApiError) {
	tilog.Info(ctx, "openapi v1beta1 error response", zap.Any("err", err))
	rw.Header().Set("Content-Type", "application/json")
	rw.WriteHeader(err.Status)

	raw, _ := json.Marshal(err)
	_, _ = rw.Write(raw)
}

func openApiWriteResult(rw http.ResponseWriter, ret interface{}) {
	rw.Header().Set("Content-Type", "application/json")
	rw.WriteHeader(http.StatusOK)
	raw, _ := json.Marshal(ret)
	_, _ = rw.Write(raw)
}

type openApiError struct {
	error
	Code      string `json:"code"`
	Status    int    `json:"status"`
	MsgPrefix string `json:"msgPrefix"`
}

type orgAuditLogStatus struct {
	Enabled string `json:"enabled"`
}

type queryAuditLogReq struct {
	PageToken       string   `form:"pageToken"`
	PageSize        int      `form:"pageSize"`
	StartTime       string   `form:"startTime"`
	EndTime         string   `form:"endTime"`
	AuditEventTypes []string `form:"auditEventTypes"`
	Keyword         string   `form:"keyword"`
}

type queryAuditLogRsp struct {
	// The total number of console audit logs that match the query.
	TotalSize int64 `json:"totalSize"`
	// The token to retrieve the next page of results. Use this value as the pageToken parameter in the next request. This field is empty when there are no more pages.
	NextPageToken string `json:"nextPageToken"`
	// A list of console audit logs that match the query.
	AuditLogs []orgAuditLog `json:"auditLogs"`
}

type orgAuditLog struct {
	// The audit event id
	ID string `json:"id"`
	// The audit event type
	EventType string `json:"eventType"`
	// The audit event end time, in the ISO 8601 format.
	EndTime string `json:"endTime,omitempty"`
	// The operation type, indicating the operator is user or api.
	OperatorType string `json:"operationType,omitempty"`
	// The operation id.
	OperatorID string `json:"operationId,string,omitempty"`
	// The operation email.
	OperatorEmail string `json:"operationEmail,omitempty"`
	// The operation IP.
	OperatorIP string `json:"operationIp,omitempty"`
	// The operation login method.
	OperatorLoginMethod string `json:"operationLoginMethod,omitempty"`
	// The organization id.
	OrgID string `json:"organizationId,string,omitempty" gorm:"index"`
	// The organization name.
	OrgName string `json:"organizationName,omitempty"`
	// The project id.
	ProjectID string `json:"projectId,omitempty,string"`
	// The project name.
	ProjectName string `json:"projectName,omitempty"`
	// The cluster ID.
	ClusterID string `json:"clusterId,omitempty,string"`
	// The cluster type.
	ClusterType string `json:"clusterType,omitempty"`
	// The cluster name.
	ClusterName string `json:"clusterName,omitempty"`
	// The audit trace ID.
	TraceID string `json:"tranceId,omitempty"`
	// The restult of the event, indicating Success or Failure.
	Result string `json:"result,omitempty"`
	// The detail information of the event.
	Details string `json:"details,omitempty"`
}
