package application

import (
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"math"
	"mayfly-go/internal/db/domain/entity"
	"mayfly-go/internal/db/domain/repository"
	"mayfly-go/pkg/logx"
	"mayfly-go/pkg/model"
	"mayfly-go/pkg/utils/timex"
	"sync"
	"time"

	"gorm.io/gorm"

	"github.com/google/uuid"
)

const maxBackupHistoryDays = 30

var (
	errRestoringBackupHistory = errors.New("正在从备份历史中恢复数据库")
)

type DbBackupApp struct {
	scheduler         *dbScheduler               `inject:"DbScheduler"`
	backupRepo        repository.DbBackup        `inject:"DbBackupRepo"`
	backupHistoryRepo repository.DbBackupHistory `inject:"DbBackupHistoryRepo"`
	restoreRepo       repository.DbRestore       `inject:"DbRestoreRepo"`
	dbApp             Db                         `inject:"DbApp"`
	mutex             sync.Mutex
	closed            chan struct{}
	wg                sync.WaitGroup
	ctx               context.Context
	cancel            context.CancelFunc
}

func (app *DbBackupApp) Init() error {
	var jobs []*entity.DbBackup
	if err := app.backupRepo.ListToDo(&jobs); err != nil {
		return err
	}
	if err := app.scheduler.AddJob(context.Background(), jobs); err != nil {
		return err
	}
	app.ctx, app.cancel = context.WithCancel(context.Background())
	app.wg.Add(1)
	go func() {
		defer app.wg.Done()
		for app.ctx.Err() == nil {
			if err := app.prune(app.ctx); err != nil {
				logx.Errorf("清理数据库备份历史失败: %s", err.Error())
				timex.SleepWithContext(app.ctx, time.Minute*15)
				continue
			}
			timex.SleepWithContext(app.ctx, time.Hour*24)
		}
	}()
	return nil
}

func (app *DbBackupApp) prune(ctx context.Context) error {
	jobs, err := app.backupRepo.SelectByCond(map[string]any{})
	if err != nil {
		return err
	}
	for _, job := range jobs {
		if ctx.Err() != nil {
			return nil
		}
		historyCond := map[string]any{
			"db_backup_id": job.Id,
		}
		histories, _ := app.backupHistoryRepo.SelectByCond(historyCond)
		expiringTime := time.Now().Add(-math.MaxInt64)
		if job.MaxSaveDays > 0 {
			expiringTime = time.Now().Add(-time.Hour * 24 * time.Duration(job.MaxSaveDays+1))
		}
		for _, history := range histories {
			if ctx.Err() != nil {
				return nil
			}
			if history.CreateTime.After(expiringTime) {
				break
			}
			err := app.DeleteHistory(ctx, history.Id)
			if errors.Is(err, errRestoringBackupHistory) {
				break
			}
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func (app *DbBackupApp) Close() {
	app.scheduler.Close()
	if app.cancel != nil {
		app.cancel()
		app.cancel = nil
	}
	app.wg.Wait()
}

func (app *DbBackupApp) Create(ctx context.Context, jobs []*entity.DbBackup) error {
	app.mutex.Lock()
	defer app.mutex.Unlock()

	if err := app.backupRepo.AddJob(ctx, jobs); err != nil {
		return err
	}
	return app.scheduler.AddJob(ctx, jobs)
}

func (app *DbBackupApp) Update(ctx context.Context, job *entity.DbBackup) error {
	app.mutex.Lock()
	defer app.mutex.Unlock()

	if err := app.backupRepo.UpdateById(ctx, job); err != nil {
		return err
	}
	_ = app.scheduler.UpdateJob(ctx, job)
	return nil
}

func (app *DbBackupApp) Delete(ctx context.Context, jobId uint64) error {
	app.mutex.Lock()
	defer app.mutex.Unlock()

	if err := app.scheduler.RemoveJob(ctx, entity.DbJobTypeBackup, jobId); err != nil {
		return err
	}
	history := &entity.DbBackupHistory{
		DbBackupId: jobId,
	}
	err := app.backupHistoryRepo.GetByCond(history)
	switch {
	default:
		return err
	case err == nil:
		return fmt.Errorf("请先删除关联的数据库备份历史【%s】", history.Name)
	case errors.Is(err, gorm.ErrRecordNotFound):
	}
	if err := app.backupRepo.DeleteById(ctx, jobId); err != nil {
		return err
	}
	return nil
}

func (app *DbBackupApp) Enable(ctx context.Context, jobId uint64) error {
	app.mutex.Lock()
	defer app.mutex.Unlock()

	repo := app.backupRepo
	job, err := repo.GetById(jobId)
	if err != nil {
		return err
	}
	if job.IsEnabled() {
		return nil
	}
	if job.IsExpired() {
		return errors.New("任务已过期")
	}
	_ = app.scheduler.EnableJob(ctx, job)
	if err := repo.UpdateEnabled(ctx, jobId, true); err != nil {
		logx.Errorf("数据库备份任务已启用( jobId: %d )，任务状态保存失败: %v", jobId, err)
		return err
	}
	return nil
}

func (app *DbBackupApp) Disable(ctx context.Context, jobId uint64) error {
	app.mutex.Lock()
	defer app.mutex.Unlock()

	repo := app.backupRepo
	job, err := repo.GetById(jobId)
	if err != nil {
		return err
	}
	if !job.IsEnabled() {
		return nil
	}
	_ = app.scheduler.DisableJob(ctx, entity.DbJobTypeBackup, jobId)
	if err := repo.UpdateEnabled(ctx, jobId, false); err != nil {
		logx.Errorf("数据库恢复任务已禁用( jobId: %d )，任务状态保存失败: %v", jobId, err)
		return err
	}
	return nil
}

func (app *DbBackupApp) StartNow(ctx context.Context, jobId uint64) error {
	app.mutex.Lock()
	defer app.mutex.Unlock()

	job, err := app.backupRepo.GetById(jobId)
	if err != nil {
		return err
	}
	if !job.IsEnabled() {
		return errors.New("任务未启用")
	}
	_ = app.scheduler.StartJobNow(ctx, job)
	return nil
}

// GetPageList 分页获取数据库备份任务
func (app *DbBackupApp) GetPageList(condition *entity.DbBackupQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
	return app.backupRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}

// GetDbNamesWithoutBackup 获取未配置定时备份的数据库名称
func (app *DbBackupApp) GetDbNamesWithoutBackup(instanceId uint64, dbNames []string) ([]string, error) {
	return app.backupRepo.GetDbNamesWithoutBackup(instanceId, dbNames)
}

// GetHistoryPageList 分页获取数据库备份历史
func (app *DbBackupApp) GetHistoryPageList(condition *entity.DbBackupHistoryQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
	return app.backupHistoryRepo.GetPageList(condition, pageParam, toEntity, orderBy...)
}

func (app *DbBackupApp) GetHistories(backupHistoryIds []uint64, toEntity any) error {
	return app.backupHistoryRepo.GetHistories(backupHistoryIds, toEntity)
}

func NewIncUUID() (uuid.UUID, error) {
	var uid uuid.UUID
	now, seq, err := uuid.GetTime()
	if err != nil {
		return uid, err
	}
	timeHi := uint32((now >> 28) & 0xffffffff)
	timeMid := uint16((now >> 12) & 0xffff)
	timeLow := uint16(now & 0x0fff)
	timeLow |= 0x1000 // Version 1

	binary.BigEndian.PutUint32(uid[0:], timeHi)
	binary.BigEndian.PutUint16(uid[4:], timeMid)
	binary.BigEndian.PutUint16(uid[6:], timeLow)
	binary.BigEndian.PutUint16(uid[8:], seq)

	copy(uid[10:], uuid.NodeID())

	return uid, nil
}

func (app *DbBackupApp) DeleteHistory(ctx context.Context, historyId uint64) (retErr error) {
	app.mutex.Lock()
	defer app.mutex.Unlock()

	if _, err := app.backupHistoryRepo.UpdateDeleting(false, historyId); err != nil {
		return err
	}
	ok, err := app.backupHistoryRepo.UpdateDeleting(true, historyId)
	if err != nil {
		return err
	}
	if !ok {
		return errRestoringBackupHistory
	}
	job, err := app.backupHistoryRepo.GetById(historyId)
	if err != nil {
		return err
	}
	conn, err := app.dbApp.GetDbConnByInstanceId(job.DbInstanceId)
	if err != nil {
		return err
	}
	dbProgram, err := conn.GetDialect().GetDbProgram()
	if err != nil {
		return err
	}
	if err := dbProgram.RemoveBackupHistory(ctx, job.DbBackupId, job.Uuid); err != nil {
		return err
	}
	return app.backupHistoryRepo.DeleteById(ctx, historyId)
}
