package dao

import (
	"context"
	"strconv"
	"strings"
	"time"

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

	"github.com/tidbcloud/auditlog/internal/entity"
	"github.com/tidbcloud/auditlog/types"
)

type AuditDaoIface interface {
	CreateEvent(ctx context.Context, tx *gorm.DB, event *entity.Event) error
	CreateInternalEvent(ctx context.Context, tx *gorm.DB, event *entity.InternalEvent) error
	DeleteEventsOlderThan(ctx context.Context, tx *gorm.DB, createdAt time.Time) error
	DeleteEventsOlderThanAndNotInOrgIds(ctx context.Context, tx *gorm.DB, orgIds []uint64, createdAt time.Time) error
	QueryEvents(ctx context.Context, tx *gorm.DB, filter types.QueryAuditLogFilter, ignorePagination bool) ([]entity.Event, int64, error)
	QueryOrgSettings(ctx context.Context, tx *gorm.DB, orgID uint64) (*entity.OrgAuditLogSettings, error)
	UpdateOrgSettings(ctx context.Context, tx *gorm.DB, orgID uint64, status int) error
}

func NewAuditDao() AuditDaoIface {
	return &auditDao{}
}

type auditDao struct {
}

func (a auditDao) QueryOrgSettings(ctx context.Context, tx *gorm.DB, orgID uint64) (*entity.OrgAuditLogSettings, error) {
	var settings entity.OrgAuditLogSettings

	err := tx.Model(&entity.OrgAuditLogSettings{}).Where("org_id = ?", orgID).First(&settings).Error

	return &settings, err
}

func (a auditDao) UpdateOrgSettings(ctx context.Context, tx *gorm.DB, orgID uint64, status int) error {
	settings, err := a.QueryOrgSettings(ctx, tx, orgID)
	if err == gorm.ErrRecordNotFound {
		settings := entity.OrgAuditLogSettings{
			OrgID:  orgID,
			Status: status,
		}
		return tx.Save(&settings).Error
	}

	settings.Status = status
	return tx.Save(&settings).Error
}

func (a auditDao) CreateEvent(ctx context.Context, tx *gorm.DB, event *entity.Event) error {
	return tx.Create(&event).Error
}

func (a auditDao) CreateInternalEvent(ctx context.Context, tx *gorm.DB, event *entity.InternalEvent) error {
	return tx.Create(&event).Error
}

func (a auditDao) DeleteEventsOlderThan(ctx context.Context, tx *gorm.DB, createdAt time.Time) error {
	return tx.Unscoped().Delete(&entity.Event{}, "created_at < ?", createdAt).Error
}

func (a auditDao) DeleteEventsOlderThanAndNotInOrgIds(ctx context.Context, tx *gorm.DB, orgIds []uint64, createdAt time.Time) error {
	return tx.Unscoped().Delete(&entity.Event{}, "org_id not in (?) and created_at < ?", orgIds, createdAt).Error
}

func parseEventTypes(t string) []auditlog.EventType {
	var eventTypes []auditlog.EventType

	for _, i := range strings.Split(t, ",") {
		tt, err := strconv.Atoi(i)
		if err == nil {
			eventTypes = append(eventTypes, auditlog.EventType(tt))
		}
	}

	return eventTypes
}

func parseClusterType(t string) []auditlog.ClusterType {
	var clusterTypes []auditlog.ClusterType

	for _, i := range strings.Split(t, ",") {
		tt, err := strconv.Atoi(i)
		if err == nil {
			clusterTypes = append(clusterTypes, auditlog.ClusterType(tt))
		}
	}

	return clusterTypes
}

func getOffsetLimit(page, perPage int) (int, int) {
	if perPage < 1 {
		perPage = 1
	}
	if perPage > 200 {
		perPage = 200
	}

	if page < 1 {
		page = 1
	}

	offset := (page - 1) * perPage
	limit := perPage

	return offset, limit
}

func (a auditDao) QueryEvents(ctx context.Context, tx *gorm.DB, filter types.QueryAuditLogFilter, ignorePagination bool) ([]entity.Event, int64, error) {
	q := tx.Debug().Model(&entity.Event{}).Where("org_id = ?", filter.OrgID)

	if len(filter.EventTypes) > 0 {
		q = q.Where("`type` in ?", parseEventTypes(filter.EventTypes))
	}
	if filter.Result != auditlog.ResultUnknown {
		q = q.Where("result = ?", filter.Result)
	}
	if filter.StartedAt != 0 {
		startedAt := time.Unix(filter.StartedAt, 0)
		q = q.Where("ends_at >= ?", startedAt)
	}
	if filter.EndedAt != 0 {
		endedAt := time.Unix(filter.EndedAt, 0)
		q = q.Where("ends_at <= ?", endedAt)
	}
	if filter.OperatorType != auditlog.OperatorTypeUnknown {
		q = q.Where("operator_type = ?", filter.OperatorType)
	}
	if filter.OperatorID != 0 {
		q = q.Where("operator_id = ?", filter.OperatorID)
	}
	if filter.OperatorEmail != "" {
		q = q.Where("operator_email = ?", filter.OperatorEmail)
	}
	if filter.OperatorIP != "" {
		q = q.Where("operator_ip = ?", filter.OperatorIP)
	}
	if filter.OperatorLoginMethod != "" {
		q = q.Where("operator_login_method = ?", filter.OperatorLoginMethod)
	}
	if filter.TraceID != "" {
		q = q.Where("trace_id = ?", filter.TraceID)
	}
	if filter.OrgName != "" {
		q = q.Where("org_name = ?", filter.OrgName)
	}
	if filter.ProjectID != 0 {
		q = q.Where("project_id = ?", filter.ProjectID)
	}
	if filter.ProjectName != "" {
		q = q.Where("project_name = ?", filter.ProjectName)
	}
	if filter.ClusterID != 0 {
		q = q.Where("cluster_id = ?", filter.ClusterID)
	}
	if filter.ClusterName != "" {
		q = q.Where("cluster_name = ?", filter.ClusterName)
	}
	if filter.ClusterType != "" {
		q = q.Where("`cluster_type` in ?", parseClusterType(filter.ClusterType))
	}
	if filter.Search != "" {
		val := "%" + strings.ToLower(filter.Search) + "%"
		q = q.Where("( "+
			"lower(operator_id) like ? or "+
			"lower(operator_email) like ? or "+
			"lower(operator_ip) like ? or "+
			"lower(operator_login_method) like ? or "+
			"lower(org_name) like ? or "+
			"lower(project_id) like ? or "+
			"lower(project_name) like ? or "+
			"lower(cluster_name) like ? "+
			")", val, val, val, val, val, val, val, val)
	}

	var count int64
	err := q.Count(&count).Error
	if err != nil {
		tilog.Error(ctx, "failed to count", zap.Error(err))
		return nil, count, err
	}

	if filter.AscOrder {
		q = q.Order("id asc")
	} else {
		q = q.Order("id desc")
	}

	if !ignorePagination {
		// open api use prev_id
		if filter.OpenApiQuery {
			if filter.PrevID >= 0 {
				if filter.AscOrder {
					q = q.Where("id > ?", filter.PrevID)
				} else {
					q = q.Where("id < ?", filter.PrevID)
				}
			}
			q = q.Limit(filter.PerPage)

		} else {
			offset, limit := getOffsetLimit(filter.Page, filter.PerPage)
			q = q.Offset(offset).Limit(limit)
		}
	}

	var events []entity.Event
	err = q.Find(&events).Error

	return events, count, err
}
