package service

import (
	"context"
	"time"

	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"gorm.io/gorm"

	"github.com/tidbcloud/auditlog/consts"
	"github.com/tidbcloud/auditlog/internal/dao"
	"github.com/tidbcloud/auditlog/internal/dto"
	"github.com/tidbcloud/auditlog/internal/entity"
	"github.com/tidbcloud/auditlog/third_party/account"
	"github.com/tidbcloud/auditlog/third_party/acl"
	"github.com/tidbcloud/auditlog/third_party/cluster_management"
	"github.com/tidbcloud/auditlog/types"
)

type AuditSvc interface {
	CreateEvent(ctx context.Context, event *auditlog.Event, trace string) error
	DeleteEventsOlderThan(ctx context.Context, createdAt time.Time) error
	QueryEvents(ctx context.Context, filter types.QueryAuditLogFilter) ([]entity.Event, int64, error)
	ExportEvents(ctx context.Context, filter types.ExportAuditLogReq) ([]entity.Event, error)
	QueryOrgSettings(ctx context.Context, orgID uint64) (*entity.OrgAuditLogSettings, error)
	UpdateOrgSettings(ctx context.Context, orgID uint64, status int) error
}

func NewAuditSvc(
	db *gorm.DB, auditDao dao.AuditDaoIface,
	accountSvc account.AccountSvcIface, cmSvc cluster_management.ClusterManageIface, aclSvc acl.AclSvcIface,
) AuditSvc {
	return &auditSvc{
		db:         db,
		auditDao:   auditDao,
		accountSvc: accountSvc,
		cmSvc:      cmSvc,
		aclSvc:     aclSvc,
	}
}

type auditSvc struct {
	db         *gorm.DB
	auditDao   dao.AuditDaoIface
	accountSvc account.AccountSvcIface
	cmSvc      cluster_management.ClusterManageIface
	aclSvc     acl.AclSvcIface
}

func (a *auditSvc) QueryOrgSettings(ctx context.Context, orgID uint64) (*entity.OrgAuditLogSettings, error) {
	return a.auditDao.QueryOrgSettings(ctx, a.db, orgID)
}

func (a *auditSvc) UpdateOrgSettings(ctx context.Context, orgID uint64, status int) error {
	return a.auditDao.UpdateOrgSettings(ctx, a.db, orgID, status)
}

func (a *auditSvc) extendEvent(ctx context.Context, event *auditlog.Event) (*auditlog.Event, error) {
	if event.UserToken != "" {
		userResp, err := a.accountSvc.GetUserByToken(ctx, event.UserToken)
		if err != nil {
			return event, err
		}
		event.OperatorID = userResp.User.Id
		event.OperatorLoginMethod = userResp.TokenType
		event.OperatorEmail = userResp.User.Email
		event.OperatorType = auditlog.OperatorTypeUser
	}

	if event.APIPubKey != "" {
		aclResp, err := a.aclSvc.GetOpenAPIKeyByToken(ctx, event.APIPubKey)
		if err != nil {
			return event, err
		}

		event.OperatorID = aclResp.Id
		event.OperatorLoginMethod = consts.LoginMethodAPIKey
		event.OperatorEmail = aclResp.CreatorEmail
		event.OperatorType = auditlog.OperatorTypeAPIKey
	}

	if event.AccessToken != "" {
		id, err := a.accountSvc.GetIdentityByAccessToken(ctx, event.AccessToken)
		if err != nil {
			return event, err
		}
		event.OperatorID = id.AccessTokenIdentity.UserId
		event.OperatorLoginMethod = consts.LoginMethodAccessToken
		event.OperatorEmail = id.AccessTokenIdentity.UserEmail
		event.OperatorType = auditlog.OperatorTypeAccessToken
	}

	if event.OperatorID == 0 {
		return event, errs.NewBadRequest("operator_id is required, but failed to get or parse", nil)
	}

	if event.OrgID != 0 && event.OrgName == "" {
		orgResp, err := a.accountSvc.GetOrgByID(ctx, event.OrgID)
		if err != nil {
			return event, err
		}

		event.OrgName = orgResp.Org.Name
	}

	if event.ProjectID != 0 && event.ProjectName == "" {
		projectResp, err := a.accountSvc.GetProjectByID(ctx, event.ProjectID)
		if err != nil {
			tilog.Warn(ctx, "failed to query project name by project id", zap.Uint64("projectID", event.ProjectID), zap.Error(err))
		} else {
			event.ProjectName = projectResp.Project.Name
		}
	}

	if event.ClusterID != 0 && (event.ClusterName == "" || event.ClusterType == auditlog.ClusterTypeUnknown) {
		cluster, err := a.cmSvc.GetClusterByID(ctx, event.ClusterID)
		if err != nil {
			tilog.Warn(ctx, "failed to query cluster name by cluster id", zap.Uint64("clusterID", event.ClusterID), zap.Error(err))
		} else {
			if event.ClusterName == "" {
				event.ClusterName = cluster.Name
			}
			if event.ClusterType == auditlog.ClusterTypeUnknown {
				event.ClusterType = consts.GetClusterType(cluster.IsDevTier)
			}
		}
	}

	return event, nil
}

func checkIfEventValid(event *auditlog.Event, isInternal bool) bool {
	if event == nil {
		return false
	}

	if event.Type == auditlog.EventUnknown {
		return false
	}

	if event.EndsAt.IsZero() {
		return false
	}

	if event.OperatorType == auditlog.OperatorTypeUnknown {
		return false
	}

	if event.OperatorID == 0 {
		return false
	}

	if event.OperatorEmail == "" {
		return false
	}

	if event.OperatorIP == "" {
		return false
	}

	if event.Details == "" {
		return false
	}

	if !isInternal && event.OrgID == 0 {
		return false
	}

	return true
}

func (a *auditSvc) CreateEvent(ctx context.Context, event *auditlog.Event, trace string) error {
	fullEvent, err := a.extendEvent(ctx, event)
	if err != nil {
		tilog.Error(ctx, "failed to extend event", zap.String("trace", trace), zap.String("tag", auditlog.TagAuditLogSubmitFailed), zap.Error(err), zap.Any("event", event))
		return err
	}

	if fullEvent.OperatorType == auditlog.OperatorTypeUser {
		authUser, err := a.accountSvc.GetAuthUser(ctx, fullEvent.OperatorEmail)
		if err != nil {
			tilog.Error(ctx, "failed to get auth user", zap.String("trace", trace), zap.String("tag", auditlog.TagAuditLogSubmitFailed), zap.Error(err), zap.Any("event", event))
			return err
		}
		if authUser.IsImpersonated {
			tilog.Info(ctx, "impersonate user create audit log event", zap.String("trace", trace), zap.Any("email", fullEvent.OperatorEmail), zap.String("tag", auditlog.TagAuditLogSubmitRejected), zap.Error(err), zap.Any("event", event))
			return nil
		}
		if fullEvent.OrgID != 0 {
			userInOrg, err := a.checkUserInOrg(ctx, fullEvent.OperatorEmail, fullEvent.OrgID)
			if err != nil {
				tilog.Error(ctx, "failed to check user in org", zap.String("trace", trace), zap.String("tag", auditlog.TagAuditLogSubmitFailed), zap.Error(err), zap.Any("event", event))
				return err
			}
			if !userInOrg {
				tilog.Info(ctx, "user is not in org, ignore to create audit log", zap.String("trace", trace), zap.String("tag", auditlog.TagAuditLogSubmitRejected), zap.Any("event", event))
				return nil
			}
		}

	}

	settings, err := a.auditDao.QueryOrgSettings(ctx, a.db, fullEvent.OrgID)
	if err != nil || settings != nil && settings.Status != consts.SettingsOn && fullEvent.Type != auditlog.EventEnableConsoleAuditLog {
		tilog.Info(ctx, "org disabled audit log", zap.String("trace", trace), zap.String("tag", auditlog.TagAuditLogSubmitRejected), zap.Any("event", event), zap.Uint64("orgID", fullEvent.OrgID))
		return nil
	}

	isInternalEventType := consts.IsInternalEventType(fullEvent.Type)
	if !checkIfEventValid(event, isInternalEventType) {
		tilog.Error(ctx, "event invalid", zap.String("trace", trace), zap.String("tag", auditlog.TagAuditLogSubmitRejected), zap.Any("event", event))
		return nil
	}

	if isInternalEventType {
		dbEvent := dto.ConvertToDBInternalEvent(fullEvent)
		err = a.auditDao.CreateInternalEvent(ctx, a.db, dbEvent)
	} else {
		dbEvent := dto.ConvertToDBEvent(fullEvent)
		err = a.auditDao.CreateEvent(ctx, a.db, dbEvent)
	}
	tilog.Info(ctx, "CreateEvent result", zap.String("trace", trace), zap.Any("event", event), zap.Any("err", err))
	return err
}

func (a *auditSvc) checkUserInOrg(ctx context.Context, email string, orgId uint64) (bool, error) {
	rsp, err := a.accountSvc.ListOrgsByUser(ctx, email)
	if err != nil {
		return false, err
	}
	var userInOrg = false
	if rsp != nil && len(rsp.Orgs) > 0 {
		for _, org := range rsp.Orgs {
			if org.Id == orgId {
				userInOrg = true
				break
			}
		}
	}
	return userInOrg, nil
}

func (a *auditSvc) DeleteEventsOlderThan(ctx context.Context, createdAt time.Time) error {
	return a.auditDao.DeleteEventsOlderThan(ctx, a.db, createdAt)
}

func (a *auditSvc) QueryEvents(ctx context.Context, filter types.QueryAuditLogFilter) ([]entity.Event, int64, error) {
	events, count, err := a.auditDao.QueryEvents(ctx, a.db, filter, false)
	if err != nil {
		tilog.Error(ctx, "failed to query events", zap.Error(err))
		return nil, 0, errs.NewInternalDB("failed to query events", err)
	}

	return events, count, nil
}

func (a *auditSvc) ExportEvents(ctx context.Context, filter types.ExportAuditLogReq) ([]entity.Event, error) {
	events, _, err := a.auditDao.QueryEvents(ctx, a.db, filter.QueryAuditLogFilter, true)
	if err != nil {
		return nil, errs.NewInternalDB("failed to export events", err)
	}

	return events, nil
}
