package auditlog

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/tidbcloud/pkgv2/auth/kong"
	"github.com/tidbcloud/tiinf/transport/tieventbus"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
	"google.golang.org/grpc/metadata"

	"github.com/tidbcloud/tiinf/monitor/tilog"
)

var (
	errTokenNotFound          = errors.New("token not found")
	errHeaderNotFound         = errors.New("header not found")
	errMetadataNotFound       = errors.New("metadata not found")
	errIPAddrNotFound         = errors.New("remote real addr is empty")
	errResultNotSet           = errors.New("result not set")
	errDetailsNotSet          = errors.New("details not set")
	errEventbusNotInitialized = errors.New("audit log sdk is not initialized")
)

type EventBuilder struct {
	ctx context.Context
	e   Event
}

func ParseOperatorIP(header string) string {
	if header == "" {
		return ""
	}

	// Remove proxy IPs from Forwarded-For header
	// https://en.wikipedia.org/wiki/X-Forwarded-For
	rips := strings.Split(header, ",")
	if len(rips) == 0 {
		return ""
	}

	return rips[0]
}

func getOperatorIP(ctx context.Context) (string, error) {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", errMetadataNotFound
	}

	rips := md.Get(HeaderXForwardedFor)
	if len(rips) == 0 || rips[0] == "" {
		return "", errIPAddrNotFound
	}

	return ParseOperatorIP(rips[0]), nil
}

func tryGetFirstHeader(ctx context.Context, header string) (string, error) {
	headers, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return "", errMetadataNotFound
	}
	headerTexts := headers[strings.ToLower(header)]
	if len(headerTexts) < 1 {
		return "", errHeaderNotFound
	}

	return headerTexts[0], nil
}

func getOpenAPIPublicKey(ctx context.Context) (string, error) {
	authType, err := tryGetFirstHeader(ctx, HeaderXAuthType)
	if err != nil {
		return "", err
	}

	if authType != string(kong.AuthTypeOpenAPI) {
		return "", fmt.Errorf("unexpected auth type: %s", authType)
	}

	authContent, err := tryGetFirstHeader(ctx, HeaderXAuthContent)
	if err != nil {
		return "", err
	}

	result := new(kong.OpenAPIAuthResult)
	err = json.Unmarshal([]byte(authContent), result)
	if err != nil {
		return "", fmt.Errorf("failed to unmarshal: auth content: %s, error: %s", authContent, err)
	}
	publicKey := result.PublicKey
	if publicKey == "" {
		return "", fmt.Errorf("failed to retrieve public_key: auth content: %s", authContent)
	}
	return publicKey, nil
}

func getUserToken(ctx context.Context) (string, error) {
	bearerToken, err := tryGetFirstHeader(ctx, HeaderAuthorizationGRPC)
	if err != nil {
		return "", err
	}

	token := ParseBearerToken(bearerToken)
	if token == "" {
		return "", errTokenNotFound
	}

	return token, nil
}

func getAccessToken(ctx context.Context) (string, error) {
	authMethod, err := tryGetFirstHeader(ctx, HeaderXAuthMethod)
	if err != nil {
		return "", err
	}

	if authMethod != "bear" {
		return "", fmt.Errorf("unexpected auth method: %s", authMethod)
	}

	accessToken, err := tryGetFirstHeader(ctx, HeaderXAuthRaw)
	if err != nil {
		return "", err
	}

	token := ParseBearerToken(accessToken)
	if token == "" {
		return "", errTokenNotFound
	}

	return token, nil
}

func ParseBearerToken(header string) string {
	if header != "" {
		tokenTexts := strings.Split(header, " ")
		if len(tokenTexts) != 2 || strings.ToLower(tokenTexts[0]) != TokenTypeBearer {
			return ""
		}

		return tokenTexts[1]
	}

	return ""
}

func getTraceID(ctx context.Context) string {
	spanCtx := trace.SpanContextFromContext(ctx)
	if spanCtx.HasTraceID() {
		return spanCtx.TraceID().String()
	}
	return ""
}

func NewEvent(ctx context.Context, eventType EventType) *EventBuilder {
	operatorIP, err := getOperatorIP(ctx)
	if err != nil {
		tilog.Info(ctx, "failed to get operator ip from context", zap.Error(err))
	}

	userToken, err := getUserToken(ctx)
	if err != nil {
		tilog.Info(ctx, "failed to get user token from context", zap.Error(err))
	}

	accessToken, err := getAccessToken(ctx)
	if err != nil {
		tilog.Info(ctx, "failed to get access token from context", zap.Error(err))
	}

	pubKey, err := getOpenAPIPublicKey(ctx)
	if err != nil {
		tilog.Info(ctx, "failed to get api token from context", zap.Error(err))
	}

	traceID := getTraceID(ctx)

	return &EventBuilder{
		ctx: ctx,
		e: Event{
			Type:        eventType,
			EndsAt:      time.Now(),
			OperatorIP:  operatorIP,
			UserToken:   userToken,
			AccessToken: accessToken,
			APIPubKey:   pubKey,
			TraceID:     traceID,
		},
	}
}

func (b *EventBuilder) ParseGIN(c *gin.Context) *EventBuilder {
	header := c.GetHeader(HeaderAuthorizationGIN)
	token := ParseBearerToken(header)
	if token == "" {
		tilog.Info(b.ctx, "failed to get token from gin")
	} else {
		b.SetUserToken(token)
	}

	forwarded := c.GetHeader(HeaderXForwardedFor)
	operatorIP := ParseOperatorIP(forwarded)
	if operatorIP == "" {
		tilog.Info(b.ctx, "failed to get operator ip from gin")
	} else {
		b.SetOperatorIP(operatorIP)
	}

	return b
}

func (b *EventBuilder) SetEndsAt(t time.Time) *EventBuilder {
	b.e.EndsAt = t
	return b
}

func (b *EventBuilder) SetUserToken(userToken string) *EventBuilder {
	b.e.UserToken = userToken
	return b
}

func (b *EventBuilder) SetAccessToken(accessToken string) *EventBuilder {
	b.e.AccessToken = accessToken
	return b
}

func (b *EventBuilder) SetAPIPubKey(pubKey string) *EventBuilder {
	b.e.APIPubKey = pubKey
	return b
}

func (b *EventBuilder) SetOperatorIP(ip string) *EventBuilder {
	b.e.OperatorIP = ip
	return b
}

func (b *EventBuilder) SetOrgID(orgID uint64) *EventBuilder {
	b.e.OrgID = orgID
	return b
}

func (b *EventBuilder) SetProjectID(projectID uint64) *EventBuilder {
	b.e.ProjectID = projectID
	return b
}

func (b *EventBuilder) SetClusterID(clusterID uint64) *EventBuilder {
	b.e.ClusterID = clusterID
	return b
}

func (b *EventBuilder) SetTraceID(traceID string) *EventBuilder {
	b.e.TraceID = traceID
	return b
}

func (b *EventBuilder) Succeed() *EventBuilder {
	b.e.Result = ResultSuccess
	return b
}

func (b *EventBuilder) Failed() *EventBuilder {
	b.e.Result = ResultFailed
	return b
}

func (b *EventBuilder) SetDetails(details map[string]string) *EventBuilder {
	v, err := json.Marshal(details)
	if err != nil {
		tilog.Error(b.ctx, "failed to marshal details", zap.Error(err))
	} else {
		b.e.Details = string(v)
	}

	return b
}

func (b *EventBuilder) SetOperatorType(t OperatorType) *EventBuilder {
	b.e.OperatorType = t
	return b
}

func (b *EventBuilder) SetOperatorID(id uint64) *EventBuilder {
	b.e.OperatorID = id
	return b
}

func (b *EventBuilder) SetOperatorEmail(email string) *EventBuilder {
	b.e.OperatorEmail = email
	return b
}

func (b *EventBuilder) SetOperatorLoginMethod(method string) *EventBuilder {
	b.e.OperatorLoginMethod = method
	return b
}

func (b *EventBuilder) SetOrgName(name string) *EventBuilder {
	b.e.OrgName = name
	return b
}

func (b *EventBuilder) SetProjectName(name string) *EventBuilder {
	b.e.ProjectName = name
	return b
}

func (b *EventBuilder) SetClusterName(name string) *EventBuilder {
	b.e.ClusterName = name
	return b
}

func (b *EventBuilder) SetResult(t bool) *EventBuilder {
	if t {
		return b.Succeed()
	} else {
		return b.Failed()
	}
}

func (b *EventBuilder) SetClusterType(t ClusterType) *EventBuilder {
	b.e.ClusterType = t
	return b
}

func (b *EventBuilder) checkRequiredFields() error {
	if b.e.UserToken == "" && b.e.APIPubKey == "" && b.e.AccessToken == "" && b.e.OperatorID == 0 {
		return errTokenNotFound
	}

	if b.e.OperatorIP == "" {
		return errIPAddrNotFound
	}

	if b.e.Result == ResultUnknown {
		return errResultNotSet
	}

	// trace id: will check it in stage 2

	if b.e.Details == "" {
		return errDetailsNotSet
	}

	return nil
}

func (b *EventBuilder) Submit() error {
	var eventID = b.e.TraceID
	if eventID == "" {
		eventID = uuid.New().String()
	}

	err := b.checkRequiredFields()
	if err != nil {
		tilog.Error(b.ctx, "not a valid audit log event, reject to submit", zap.Error(err), zap.Any("event", b.e), zap.String("tag", TagAuditLogRejected))
		return err
	}
	if eventbus == nil {
		tilog.Error(b.ctx, "failed to publish event", zap.Error(errEventbusNotInitialized), zap.Any("event", b.e), zap.String("tag", TagAuditLogSubmitFailed))
		return errEventbusNotInitialized
	}

	// NOTE: eventbus does not support set a timeout right now
	err = eventbus.Publish(&tieventbus.Event{
		ID:   eventID,
		Type: EventBusEventType,
		Data: &b.e,
	})
	if err != nil {
		tilog.Error(b.ctx, "failed to publish event", zap.Error(err), zap.Any("event", b.e), zap.String("tag", TagAuditLogSubmitFailed))
		return err
	}

	return nil
}
