package repo

import (
	"encoding/json"
	"strings"

	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"google.golang.org/genproto/googleapis/type/date"
	"gorm.io/gorm"
	"strconv"
	"time"
)

type AlertRepo struct{}

type IAlertRepo interface {
	WithByType(alertType string) DBOption
	WithByStatusIn(status []string) DBOption
	WithByProject(project string) DBOption
	WithByCount(count uint) DBOption
	WithByAlertId(alertId uint) DBOption
	WithByCreateAt(date *date.Date) DBOption
	WithByLicenseId(licenseId string) DBOption
	WithByRecordId(recordId uint) DBOption
	WithByAlertMethodContainsConfigID(id uint) DBOption
	WithByMethodConfigIDs(ids []uint) DBOption

	Create(alert *model.Alert) error
	Get(opts ...DBOption) (model.Alert, error)
	Page(page, size int, opts ...DBOption) (int64, []model.Alert, error)
	List(opts ...DBOption) ([]model.Alert, error)
	Delete(opts ...DBOption) error
	Save(alert *model.Alert) error
	Update(maps map[string]interface{}, opts ...DBOption) error

	GetLog(opts ...DBOption) (model.AlertLog, error)
	CreateLog(alertLog *model.AlertLog) error
	PageLog(limit, offset int, opts ...DBOption) (int64, []model.AlertLog, error)
	ListLog(opts ...DBOption) ([]model.AlertLog, error)
	UpdateLog(id uint, maps map[string]interface{}) error
	BatchUpdateLogBy(maps map[string]interface{}, opts ...DBOption) error
	DeleteLog(opts ...DBOption) error
	CleanAlertLogs() error

	CreateAlertTask(alertTaskBase *model.AlertTask) error
	DeleteAlertTask(opts ...DBOption) error
	GetAlertTask(opts ...DBOption) (model.AlertTask, error)
	LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
	GetTaskLog(alertType string, alertId uint) (time.Time, error)
	GetLicensePushCount(method string) (uint, error)

	GetConfig(opts ...DBOption) (model.AlertConfig, error)
	GetConfigById(id uint) (model.AlertConfig, error)
	AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
	UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
	CreateAlertConfig(config *model.AlertConfig) error
	DeleteAlertConfig(opts ...DBOption) error

	WithByTypeNotIn(types []string) DBOption
	PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error)

	SyncAll(data []model.AlertConfig) error
}

func NewIAlertRepo() IAlertRepo {
	return &AlertRepo{}
}

func (a *AlertRepo) WithByType(alertType string) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("`type` = ?", alertType)
	}
}

func (a *AlertRepo) WithByStatusIn(status []string) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("status in (?)", status)
	}
}

func (a *AlertRepo) WithByCount(count uint) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("count = ?", count)
	}
}

func (a *AlertRepo) WithByProject(project string) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("project = ?", project)
	}
}

func (a *AlertRepo) WithByAlertId(alertId uint) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("alert_id = ?", alertId)
	}
}

func (a *AlertRepo) WithByLicenseId(licenseId string) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("license_id = ?", licenseId)
	}
}

func (a *AlertRepo) WithByRecordId(recordId uint) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("record_id = ?", recordId)
	}
}

func (a *AlertRepo) WithByAlertMethodContainsConfigID(id uint) DBOption {
	method := strconv.Itoa(int(id))
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("(method = ? OR method LIKE ? OR method LIKE ? OR method LIKE ?)", method, method+",%", "%,"+method, "%,"+method+",%")
	}
}

func (a *AlertRepo) WithByMethodConfigIDs(ids []uint) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		methods := make([]string, 0, len(ids))
		for _, id := range ids {
			methods = append(methods, strconv.Itoa(int(id)))
		}
		return g.Where("method IN ?", methods)
	}
}

func (a *AlertRepo) WithByCreateAt(createAt *date.Date) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("DATE(created_at) = DATE(?)", createAt)
	}
}

func (a *AlertRepo) Create(alert *model.Alert) error {
	return global.AlertDB.Model(&model.Alert{}).Create(alert).Error
}

func (a *AlertRepo) Save(alert *model.Alert) error {
	return global.AlertDB.Save(alert).Error
}

func (a *AlertRepo) Get(opts ...DBOption) (model.Alert, error) {
	var alert model.Alert
	db, _ := getAlertDB(opts...)
	err := db.First(&alert).Error
	return alert, err
}

func (a *AlertRepo) Page(page, size int, opts ...DBOption) (int64, []model.Alert, error) {
	var alerts []model.Alert
	alertDb, _ := getAlertDB(opts...)
	db := alertDb.Model(&model.Alert{})
	count := int64(0)
	db = db.Count(&count)
	err := db.Limit(size).Offset(size * (page - 1)).Find(&alerts).Error
	return count, alerts, err
}

func (a *AlertRepo) List(opts ...DBOption) ([]model.Alert, error) {
	var alert []model.Alert
	db, _ := getAlertDB(opts...)
	err := db.Find(&alert).Error
	return alert, err
}

func (a *AlertRepo) Update(maps map[string]interface{}, opts ...DBOption) error {
	db, _ := getAlertDB(opts...)
	return db.Model(&model.Alert{}).Updates(maps).Error
}

func (a *AlertRepo) Delete(opts ...DBOption) error {
	db, _ := getAlertDB(opts...)
	return db.Delete(&model.Alert{}).Error
}

func (a *AlertRepo) GetLog(opts ...DBOption) (model.AlertLog, error) {
	var alertLog model.AlertLog
	db, _ := getAlertDB(opts...)
	err := db.First(&alertLog).Error
	return alertLog, err
}

func (a *AlertRepo) CreateLog(log *model.AlertLog) error {
	return global.AlertDB.Model(&model.AlertLog{}).Create(&log).Error
}

func (a *AlertRepo) UpdateLog(id uint, maps map[string]interface{}) error {
	return global.AlertDB.Model(&model.AlertLog{}).Where("id = ?", id).Updates(maps).Error
}

func (a *AlertRepo) BatchUpdateLogBy(maps map[string]interface{}, opts ...DBOption) error {
	db, _ := getAlertDB(opts...)
	if len(opts) == 0 {
		db = db.Where("1=1")
	}
	return db.Model(&model.AlertLog{}).Updates(&maps).Error
}

func (a *AlertRepo) PageLog(page, size int, opts ...DBOption) (int64, []model.AlertLog, error) {
	var alerts []model.AlertLog
	db := global.AlertDB.Model(&model.AlertLog{})
	for _, opt := range opts {
		db = opt(db)
	}
	count := int64(0)
	db = db.Order("created_at desc").Count(&count)
	err := db.Limit(size).Offset(size * (page - 1)).Find(&alerts).Error
	return count, alerts, err
}

func (a *AlertRepo) ListLog(opts ...DBOption) ([]model.AlertLog, error) {
	var alertLog []model.AlertLog
	db, _ := getAlertDB(opts...)
	err := db.Find(&alertLog).Error
	return alertLog, err
}

func (a *AlertRepo) DeleteLog(opts ...DBOption) error {
	db, _ := getAlertDB(opts...)
	return db.Delete(&model.AlertLog{}).Error
}

func (a *AlertRepo) CleanAlertLogs() error {
	return global.AlertDB.Where("1 = 1").Delete(&model.AlertLog{}).Error
}

func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error {
	return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).Error
}

func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error {
	db, _ := getAlertDB(opts...)
	return db.Delete(&model.AlertTask{}).Error
}

func (a *AlertRepo) GetAlertTask(opts ...DBOption) (model.AlertTask, error) {
	var data model.AlertTask
	db, _ := getAlertDB(opts...)
	err := db.First(&data).Error
	return data, err
}

func (a *AlertRepo) LoadTaskCount(alertType string, project string, method string) (uint, uint, error) {
	var (
		todayCount int64
		totalCount int64
	)
	_ = global.AlertDB.Model(&model.AlertTask{}).Where("type = ? AND quota_type = ? AND method = ?", alertType, project, method).Count(&totalCount).Error

	now := time.Now()
	todayMidnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	tomorrowMidnight := todayMidnight.Add(24 * time.Hour)
	err := global.AlertDB.Model(&model.AlertTask{}).Where("type =  ? AND quota_type = ?  AND method = ? AND created_at > ? AND created_at < ?", alertType, project, method, todayMidnight, tomorrowMidnight).Count(&todayCount).Error
	return uint(todayCount), uint(totalCount), err
}

func (a *AlertRepo) GetTaskLog(alertType string, alertId uint) (time.Time, error) {
	var newDate time.Time
	status := []string{constant.AlertSuccess, constant.AlertPushSuccess, constant.AlertSyncError, constant.AlertPushing}
	now := time.Now()
	todayMidnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	tomorrowMidnight := todayMidnight.Add(24 * time.Hour)
	err := global.AlertDB.Model(&model.AlertLog{}).
		Where("type = ? AND alert_id = ? AND status in ? AND created_at > ? AND created_at < ?", alertType, alertId, status, todayMidnight, tomorrowMidnight).
		Order("created_at DESC").
		Limit(1).
		Pluck("created_at", &newDate).Error
	if err != nil {
		return time.Time{}, err
	}

	if newDate.IsZero() {
		return time.Time{}, nil
	}

	return newDate, nil
}

func getAlertDB(opts ...DBOption) (*gorm.DB, error) {
	var db *gorm.DB
	db = global.AlertDB
	for _, opt := range opts {
		db = opt(db)
	}
	return db, nil
}

func (a *AlertRepo) GetLicensePushCount(method string) (uint, error) {
	var (
		todayCount int64
	)
	now := time.Now()
	todayMidnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
	tomorrowMidnight := todayMidnight.Add(24 * time.Hour)
	err := global.AlertDB.Model(&model.AlertTask{}).Where("created_at > ? AND created_at < ? AND method = ?", todayMidnight, tomorrowMidnight, method).Count(&todayCount).Error
	return uint(todayCount), err
}

func (a *AlertRepo) AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error) {
	var config []model.AlertConfig
	db, _ := getAlertDB(opts...)
	err := db.Find(&config).Error
	return config, err
}

func (a *AlertRepo) UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error {
	db, _ := getAlertDB(opts...)
	return db.Model(&model.AlertConfig{}).Updates(maps).Error
}

func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error {
	return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
}

func (a *AlertRepo) DeleteAlertConfig(opts ...DBOption) error {
	db, _ := getAlertDB(opts...)
	return db.Delete(&model.AlertConfig{}).Error
}

func (a *AlertRepo) GetConfig(opts ...DBOption) (model.AlertConfig, error) {
	var alertConfig model.AlertConfig
	db, _ := getAlertDB(opts...)
	err := db.First(&alertConfig).Error
	return alertConfig, err
}

func (a *AlertRepo) GetConfigById(id uint) (model.AlertConfig, error) {
	var config model.AlertConfig
	err := global.AlertDB.First(&config, id).Error
	return config, err
}

func (a *AlertRepo) WithByTypeNotIn(types []string) DBOption {
	return func(g *gorm.DB) *gorm.DB {
		return g.Where("`type` NOT IN (?)", types)
	}
}

func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
	var configs []model.AlertConfig
	db := global.AlertDB.Model(&model.AlertConfig{})
	for _, opt := range opts {
		db = opt(db)
	}
	count := int64(0)
	db = db.Count(&count)
	err := db.Limit(size).Offset(size * (page - 1)).Find(&configs).Error
	return count, configs, err
}

var singletonTypes = map[string]bool{
	constant.CommonConfig: true,
}

func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
	tx := global.AlertDB.Begin()
	if tx.Error != nil {
		return tx.Error
	}
	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
			panic(r)
		}
	}()

	var oldConfigs []model.AlertConfig
	if err := tx.Find(&oldConfigs).Error; err != nil {
		tx.Rollback()
		return err
	}

	usedConfigIDs, err := loadUsedAlertConfigIDs(tx)
	if err != nil {
		tx.Rollback()
		return err
	}

	oldConfigMap := make(map[string]uint)
	oldConfigByType := make(map[string][]model.AlertConfig)
	oldConfigByKey := make(map[string][]model.AlertConfig)
	consumedConfigIDs := make(map[uint]struct{})
	for _, item := range oldConfigs {
		if singletonTypes[item.Type] {
			oldConfigMap[item.Type] = item.ID
			continue
		}
		oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
		oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
	}
	for _, item := range data {
		if singletonTypes[item.Type] {
			if val, ok := oldConfigMap[item.Type]; ok {
				item.ID = val
				delete(oldConfigMap, item.Type)
				consumedConfigIDs[item.ID] = struct{}{}
			} else {
				item.ID = 0
			}
			if item.ID == 0 {
				if err := tx.Create(&item).Error; err != nil {
					tx.Rollback()
					return err
				}
			} else if err := tx.Save(&item).Error; err != nil {
				tx.Rollback()
				return err
			}
			continue
		}

		key := alertConfigSyncKey(item)
		if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
			item.ID = matched.ID
			consumedConfigIDs[item.ID] = struct{}{}
			if err := tx.Save(&item).Error; err != nil {
				tx.Rollback()
				return err
			}
			deleteAlertConfigByID(oldConfigByType, matched.ID)
			continue
		}

		if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
			item.ID = matched.ID
			consumedConfigIDs[item.ID] = struct{}{}
			if err := tx.Save(&item).Error; err != nil {
				tx.Rollback()
				return err
			}
			continue
		}

		item.ID = 0
		if err := tx.Create(&item).Error; err != nil {
			tx.Rollback()
			return err
		}
	}
	for _, item := range oldConfigs {
		if _, used := usedConfigIDs[item.ID]; used {
			continue
		}
		if _, kept := consumedConfigIDs[item.ID]; kept {
			continue
		}
		if err := tx.Where("id = ?", item.ID).Delete(&model.AlertConfig{}).Error; err != nil {
			tx.Rollback()
			return err
		}
	}
	if err := tx.Commit().Error; err != nil {
		tx.Rollback()
		return err
	}
	return nil
}

func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
	var alerts []model.Alert
	if err := tx.Select("method").Find(&alerts).Error; err != nil {
		return nil, err
	}

	usedIDs := make(map[uint]struct{})
	for _, alert := range alerts {
		for _, item := range strings.Split(alert.Method, ",") {
			item = strings.TrimSpace(item)
			if item == "" {
				continue
			}
			id, err := strconv.ParseUint(item, 10, 64)
			if err != nil {
				continue
			}
			usedIDs[uint(id)] = struct{}{}
		}
	}
	return usedIDs, nil
}

func alertConfigSyncKey(item model.AlertConfig) string {
	return item.Type + "::" + normalizeAlertConfigJSON(item.Config)
}

func normalizeAlertConfigJSON(config string) string {
	trimmed := strings.TrimSpace(config)
	if trimmed == "" {
		return ""
	}

	var data any
	if err := json.Unmarshal([]byte(trimmed), &data); err != nil {
		return trimmed
	}
	buf, err := json.Marshal(data)
	if err != nil {
		return trimmed
	}
	return string(buf)
}

func popAlertConfigByKey(configMap map[string][]model.AlertConfig, key string) (model.AlertConfig, bool) {
	items := configMap[key]
	if len(items) == 0 {
		return model.AlertConfig{}, false
	}

	item := items[0]
	if len(items) == 1 {
		delete(configMap, key)
	} else {
		configMap[key] = items[1:]
	}
	return item, true
}

func popUnusedAlertConfigByType(configMap map[string][]model.AlertConfig, usedConfigIDs map[uint]struct{}, configType string) (model.AlertConfig, bool) {
	items := configMap[configType]
	if len(items) == 0 {
		return model.AlertConfig{}, false
	}

	for idx, item := range items {
		if _, used := usedConfigIDs[item.ID]; used {
			continue
		}
		if idx == 0 {
			if len(items) == 1 {
				delete(configMap, configType)
			} else {
				configMap[configType] = items[1:]
			}
		} else {
			configMap[configType] = append(items[:idx], items[idx+1:]...)
		}
		return item, true
	}
	return model.AlertConfig{}, false
}

func deleteAlertConfigByID(configMap map[string][]model.AlertConfig, id uint) {
	for key, items := range configMap {
		for idx, item := range items {
			if item.ID != id {
				continue
			}
			if len(items) == 1 {
				delete(configMap, key)
			} else {
				configMap[key] = append(items[:idx], items[idx+1:]...)
			}
			return
		}
	}
}
