package models

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"time"

	"yunion.io/x/jsonutils"
	"yunion.io/x/log"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/httputils"
	"yunion.io/x/pkg/utils"

	commonapi "yunion.io/x/onecloud/pkg/apis"
	computeapi "yunion.io/x/onecloud/pkg/apis/compute"
	imageapi "yunion.io/x/onecloud/pkg/apis/image"
	api "yunion.io/x/onecloud/pkg/apis/llm"
	"yunion.io/x/onecloud/pkg/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
	"yunion.io/x/onecloud/pkg/httperrors"
	"yunion.io/x/onecloud/pkg/llm/options"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/mcclient/auth"
	"yunion.io/x/onecloud/pkg/mcclient/modules/compute"
	"yunion.io/x/onecloud/pkg/mcclient/modules/image"
	"yunion.io/x/onecloud/pkg/util/logclient"
)

func init() {
	GetVolumeManager()
}

var volumeManager *SVolumeManager

func GetVolumeManager() *SVolumeManager {
	if volumeManager != nil {
		return volumeManager
	}
	volumeManager = &SVolumeManager{
		SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager(
			SVolume{},
			"volumes_tbl",
			"llm_volume",
			"llm_volumes",
		),
	}
	volumeManager.SetVirtualObject(volumeManager)
	return volumeManager
}

type SVolumeManager struct {
	db.SVirtualResourceBaseManager
	SMountedModelsResourceManager
}

type SVolume struct {
	db.SVirtualResourceBase
	SMountedModelsResource

	LLMId string `width:"128" charset:"ascii" nullable:"true" list:"user" create:"admin_optional" update:"user"`
	// 存储类型
	StorageType string `width:"16" charset:"ascii" nullable:"true" list:"user" create:"admin_optional" update:"user"`
	// 模板ID
	TemplateId string `width:"128" charset:"ascii" nullable:"true" list:"user" create:"admin_optional" update:"user"`
	// size in MB
	SizeMB     int                          `nullable:"false" default:"0" create:"optional" list:"user" update:"user"`
	CmpId      string                       `width:"128" charset:"ascii" nullable:"true" list:"user"`
	Containers api.ContainerVolumeRelations `charset:"utf8" nullable:"true" list:"user" create:"optional"`
}

func (volume *SVolume) StartDeleteTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error {
	task, err := taskman.TaskManager.NewTask(ctx, "VolumeDeleteTask", volume, userCred, nil, parentTaskId, "", nil)
	if err != nil {
		return err
	}
	volume.SetStatus(ctx, userCred, commonapi.STATUS_DELETING, "")
	task.ScheduleRun(nil)
	return nil
}

func (volume *SVolume) RealDelete(ctx context.Context, userCred mcclient.TokenCredential) error {
	return volume.SVirtualResourceBase.Delete(ctx, userCred)
}

func (volume *SVolume) StartResizeTask(ctx context.Context, userCred mcclient.TokenCredential, input api.VolumeResizeTaskInput, parentTaskId string) (*taskman.STask, error) {
	volume.SetStatus(ctx, userCred, computeapi.DISK_START_RESIZE, "StartResizeTask")
	params := jsonutils.Marshal(input).(*jsonutils.JSONDict)
	task, err := taskman.TaskManager.NewTask(ctx, "VolumeResizeTask", volume, userCred, params, parentTaskId, "", nil)
	if err != nil {
		return nil, errors.Wrap(err, "NewTask")
	}
	if err := task.ScheduleRun(nil); err != nil {
		return nil, errors.Wrap(err, "ScheduleRun")
	}
	return task, nil
}

func fetchImage(ctx context.Context, userCred mcclient.TokenCredential, imageId string) (*imageapi.ImageDetails, error) {
	s := auth.GetSession(ctx, userCred, options.Options.Region)
	imgObj, err := image.Images.Get(s, imageId, nil)
	if err != nil {
		return nil, errors.Wrapf(err, "Image.Get %s", imageId)
	}
	img := imageapi.ImageDetails{}
	err = imgObj.Unmarshal(&img)
	if err != nil {
		return nil, errors.Wrap(err, "Unmarshal")
	}
	return &img, nil
}

func (volume *SVolume) UpdateMountedModelFullNames(mountModels []string) error {
	_, err := db.Update(volume, func() error {
		volume.MountedModels = mountModels
		return nil
	})
	if err != nil {
		return errors.Wrap(err, "update volume mounted_apps")
	}
	return nil
}

func (volume *SVolume) GetDisk(ctx context.Context) (*computeapi.DiskDetails, error) {
	if len(volume.CmpId) == 0 {
		return nil, errors.ErrInvalidStatus
	}
	s := auth.GetAdminSession(ctx, "")
	disk := computeapi.DiskDetails{}
	resp, err := compute.Disks.GetById(s, volume.CmpId, jsonutils.Marshal(map[string]interface{}{
		"scope": "max",
	}))
	if err != nil {
		if httputils.ErrorCode(err) == 404 {
			return nil, errors.Wrapf(errors.ErrNotFound, "GetById %s", volume.CmpId)
		}
		return nil, errors.Wrap(err, "fetch disk")
	}
	resp.Unmarshal(&disk)
	return &disk, nil
}

func (volume *SVolume) WaitDiskStatus(ctx context.Context, userCred mcclient.TokenCredential, targetStatus []string, timeoutSecs int) (*computeapi.DiskDetails, error) {
	expire := time.Now().Add(time.Second * time.Duration(timeoutSecs))
	for time.Now().Before(expire) {
		disk, err := volume.GetDisk(ctx)
		if err != nil {
			return nil, errors.Wrap(err, "GetDisk")
		}
		if utils.IsInArray(disk.Status, targetStatus) {
			return disk, nil
		}
		if strings.Contains(disk.Status, "fail") {
			return nil, errors.Wrap(errors.ErrInvalidStatus, disk.Status)
		}
		time.Sleep(2 * time.Second)
	}
	return nil, errors.Wrapf(httperrors.ErrTimeout, "wait disk status %s timeout", targetStatus)
}

func (volume *SVolume) GetLLM() *SLLM {
	if len(volume.LLMId) == 0 {
		return nil
	}
	obj, err := GetLLMManager().FetchById(volume.LLMId)
	if err != nil {
		log.Errorf("Volume %s fetch llm %s error %s", volume.Id, volume.LLMId, err)
		return nil
	}
	return obj.(*SLLM)
}

func (volume *SVolume) PerformReset(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input api.VolumePerformResetInput) (*api.LLMBatchPerformOutput, error) {
	if volume.Status != computeapi.DISK_READY {
		return nil, errors.Wrapf(httperrors.ErrInvalidStatus, "invalid status %s", volume.Status)
	}
	task, err := volume.StartResetTask(ctx, userCred, input, "")
	if err != nil {
		return nil, errors.Wrap(err, "StartResetTask")
	}
	return &api.LLMBatchPerformOutput{
		Data: []api.LLMPerformOutput{
			{
				Id:            volume.Id,
				Name:          volume.Name,
				RequestStatus: http.StatusOK,
				TaskId:        task.Id,
			},
		},
		Task: task,
	}, nil
}

func (volume *SVolume) StartResetTask(ctx context.Context, userCred mcclient.TokenCredential, input api.VolumePerformResetInput, parentTaskId string) (*taskman.STask, error) {
	volume.SetStatus(ctx, userCred, api.VOLUME_STATUS_START_RESET, "StartResetTask")
	params := jsonutils.Marshal(input).(*jsonutils.JSONDict)
	task, err := taskman.TaskManager.NewTask(ctx, "VolumeResetTask", volume, userCred, params, parentTaskId, "")
	if err != nil {
		return nil, errors.Wrap(err, "NewTask")
	}
	err = task.ScheduleRun(nil)
	if err != nil {
		return nil, errors.Wrap(err, "ScheduleRun")
	}
	return task, nil
}

func (volume *SVolume) DoReset(ctx context.Context, userCred mcclient.TokenCredential, templateId, backupId *string, sizeGb int) error {
	// clear desktop app list
	{
		llm := volume.GetLLM()
		if llm != nil {
			llm.purgeModelList()
		}
	}
	// clear mounts
	{
		_, err := db.Update(volume, func() error {
			volume.MountedModels = nil
			return nil
		})
		if err != nil {
			return errors.Wrap(err, "update volume mounted_apps")
		}
	}

	// reset disk
	s := auth.GetSession(ctx, userCred, options.Options.Region)
	params := computeapi.DiskRebuildInput{}
	params.TemplateId = templateId
	params.BackupId = backupId
	emptyStr := ""
	if params.BackupId == nil {
		params.BackupId = &emptyStr
	}
	if sizeGb > 0 {
		size := fmt.Sprintf("%dG", sizeGb)
		params.Size = &size
	}

	log.Debugf("need to reset phone data disk ... %s %dG", jsonutils.Marshal(params), sizeGb)

	_, err := compute.Disks.PerformAction(s, volume.CmpId, "rebuild", jsonutils.Marshal(params))
	if err != nil {
		return errors.Wrap(err, "rebuild disk")
	}

	return nil
}

func (volume *SVolume) UpdateSize(ctx context.Context, userCred mcclient.TokenCredential, sizeMb int) error {
	if volume.SizeMB == sizeMb {
		return nil
	}
	oldSize := volume.SizeMB
	_, err := db.Update(volume, func() error {
		volume.SizeMB = sizeMb
		return nil
	})
	if err != nil {
		logclient.AddSimpleActionLog(volume, logclient.ACT_RESIZE, err, userCred, false)
		return errors.Wrap(err, "update volume size")
	}
	notes := struct {
		OldSizeMb int `json:"old_size_mb"`
		NewSizeMb int `json:"new_size_mb"`
	}{
		OldSizeMb: oldSize,
		NewSizeMb: sizeMb,
	}
	logclient.AddSimpleActionLog(volume, logclient.ACT_RESIZE, jsonutils.Marshal(notes), userCred, true)
	return nil
}
