package models

import (
	"context"
	"database/sql"
	"net/http"
	"slices"
	"strings"
	"time"

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

	commonapi "yunion.io/x/onecloud/pkg/apis"
	computeapi "yunion.io/x/onecloud/pkg/apis/compute"
	apis "yunion.io/x/onecloud/pkg/apis/llm"
	"yunion.io/x/onecloud/pkg/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
	"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
	"yunion.io/x/onecloud/pkg/httperrors"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/mcclient/modules/compute"
	"yunion.io/x/onecloud/pkg/util/logclient"
)

func (llm *SLLM) getMountedInstantModels(ctx context.Context, probedExt map[string]apis.LLMInternalInstantMdlInfo) (map[string]struct{}, error) {
	container, err := llm.GetLLMSContainer(ctx)
	if err != nil {
		return nil, errors.Wrap(err, "GetSContainer")
	}
	if container.Spec == nil {
		return nil, errors.Wrap(errors.ErrEmpty, "no Spec")
	}
	if len(container.Spec.VolumeMounts) == 0 {
		return nil, errors.Wrap(errors.ErrEmpty, "no VolumeMounts")
	}
	if container.Spec.VolumeMounts[0].Disk == nil {
		return nil, errors.Wrap(errors.ErrEmpty, "no Disk")
	}
	if len(container.Spec.VolumeMounts[0].Disk.PostOverlay) == 0 {
		return nil, nil
	}
	mdlNameToId := make(map[string]string)
	for mdlId, model := range probedExt {
		mdlNameToId[model.Name+":"+model.Tag] = mdlId
	}
	mdlMap := make(map[string]struct{})
	postOverlays := container.Spec.VolumeMounts[0].Disk.PostOverlay
	drv, err := GetLLMContainerInstantModelDriver(llm.GetLLMContainerDriver().GetType())
	if err != nil {
		return nil, nil
	}
	for i := range postOverlays {
		postOverlay := postOverlays[i]
		mdlId := drv.GetInstantModelIdByPostOverlay(postOverlay, mdlNameToId)
		if mdlId != "" {
			mdlMap[mdlId] = struct{}{}
		}
	}
	return mdlMap, nil
}

func (llm *SLLM) getProbedInstantModelsExt(ctx context.Context, userCred mcclient.TokenCredential, instantModelIds ...string) (map[string]apis.LLMInternalInstantMdlInfo, error) {
	drv, err := GetLLMContainerInstantModelDriver(llm.GetLLMContainerDriver().GetType())
	if err != nil {
		return nil, nil
	}
	return drv.GetProbedInstantModelsExt(ctx, userCred, llm, instantModelIds...)
}

type sInstantModelStatus struct {
	apis.LLMInternalInstantMdlInfo

	Probed  bool
	Mounted bool
}

func (llm *SLLM) getProbedMountedInstantModels(ctx context.Context, userCred mcclient.TokenCredential) (map[string]*sInstantModelStatus, error) {
	mdlMap := make(map[string]*sInstantModelStatus)
	probedExt, errExt := llm.getProbedInstantModelsExt(ctx, userCred)
	if errExt != nil {
		return nil, errors.Wrap(errExt, "getProbedInstantModelsExt")
	}

	for modelId := range probedExt {
		mdlMap[modelId] = &sInstantModelStatus{
			LLMInternalInstantMdlInfo: probedExt[modelId],
			Probed:                    true,
		}
	}

	mounted, err := llm.getMountedInstantModels(ctx, probedExt)
	if err != nil {
		return nil, errors.Wrap(err, "llm.getMountedInstantModels")
	}
	for instantModelId := range mounted {
		if _, ok := mdlMap[instantModelId]; ok {
			mdlMap[instantModelId].Mounted = true
		} else {
			// 仅 mounted 未 probed 时无 ollama model id，ModelId 留空
			mdlMap[instantModelId] = &sInstantModelStatus{
				LLMInternalInstantMdlInfo: apis.LLMInternalInstantMdlInfo{},
				Mounted:                   true,
			}
		}
	}
	return mdlMap, nil
}

func (llm *SLLM) uninstallInstantModel(ctx context.Context, userCred mcclient.TokenCredential, instantModelId string) error {
	boolFalse := false
	// uninstalled
	probed := &boolFalse
	mounted := &boolFalse
	_, err := GetLLMInstantModelManager().updateInstantModel(ctx, llm.Id, instantModelId, "", "", probed, mounted)
	if err != nil {
		return errors.Wrap(err, "uninstallPackage")
	}
	return nil
}

func findInstantModelWithModelInfo(allModels []SLLMInstantModel, mdl apis.ModelInfo) *SLLMInstantModel {
	for i := range allModels {
		if allModels[i].InstantModelId == mdl.Id {
			return &allModels[i]
		}
	}
	return nil
}

func findModelsToUninstall(allModels []SLLMInstantModel, input apis.LLMSyncModelTaskInput) []SLLMInstantModel {
	ret := make([]SLLMInstantModel, 0)
	for i := range input.Models {
		existingModel := findInstantModelWithModelInfo(allModels, input.Models[i])
		if existingModel != nil && existingModel.IsMounted && (input.Method == apis.QuickModelUninstall || input.Method == apis.QuickModelReinstall || (!existingModel.IsProbed && input.Method == apis.QuickModelInstall)) {
			ret = append(ret, *existingModel)
		}
	}
	return ret
}

func findModelsToUnmount(allModels []SLLMInstantModel, input apis.LLMSyncModelTaskInput) []SLLMInstantModel {
	ret := make([]SLLMInstantModel, 0)
	for i := range input.Models {
		existingModel := findInstantModelWithModelInfo(allModels, input.Models[i])
		if existingModel != nil && existingModel.IsMounted && (input.Method == apis.QuickModelUninstall || input.Method == apis.QuickModelReinstall || (!existingModel.IsProbed && input.Method == apis.QuickModelInstall)) {
			ret = append(ret, *existingModel)
		}
	}
	return ret
}

func isImageInUnmountModels(imageId string, mdls []SLLMInstantModel) (bool, error) {
	instMdl, err := GetInstantModelManager().findInstantModelByImageId(imageId)
	if err != nil {
		return false, errors.Wrap(err, "findInstantAppByImageId")
	}
	if instMdl == nil {
		return false, nil
	}
	for i := range mdls {
		if mdls[i].InstantModelId == instMdl.Id {
			return true, nil
		}
	}
	return false, nil
}

func (llm *SLLM) RefreshInstantModels(ctx context.Context, userCred mcclient.TokenCredential, refresh bool) error {
	lockman.LockObject(ctx, llm)
	defer lockman.ReleaseObject(ctx, llm)

	if !llm.LastInstantModelProbe.IsZero() && time.Since(llm.LastInstantModelProbe) < apis.LLM_PROBE_INSTANT_MODEl_INTERVAL_SECOND*time.Second && (!refresh || time.Since(llm.LastInstantModelProbe) < apis.LLM_PROBE_INSTANT_MODEl_INTERVAL_SECOND*time.Second) {
		// already probed, null operation
		return nil
	}

	mdlMap, err := llm.getProbedMountedInstantModels(ctx, userCred)
	if err != nil {
		return errors.Wrap(err, "getProbedMountedInstantModels")
	}

	models, err := llm.FetchModels(nil, nil, nil)
	if err != nil {
		return errors.Wrap(err, "FetchModels")
	}

	var errs []error
	for i := range models {
		mdl := models[i]
		var probed *bool
		var mounted *bool
		if status, ok := mdlMap[mdl.InstantModelId]; ok {
			// probed
			probed = &status.Probed
			mounted = &status.Mounted
			_, err = GetLLMInstantModelManager().updateInstantModel(ctx, llm.Id, mdl.InstantModelId, status.Name, status.Tag, probed, mounted)
			delete(mdlMap, mdl.InstantModelId)
		} else {
			// uninstalled
			err = llm.uninstallInstantModel(ctx, userCred, mdl.InstantModelId)
		}
		if err != nil {
			errs = append(errs, err)
		}
	}

	if len(mdlMap) > 0 {
		for instantModelId, status := range mdlMap {
			_, err := GetLLMInstantModelManager().updateInstantModel(ctx, llm.Id, instantModelId, status.Name, status.Tag, &status.Probed, &status.Mounted)
			if err != nil {
				errs = append(errs, err)
			}
		}
	}

	if len(errs) > 0 {
		return errors.NewAggregate(errs)
	}
	// update timer
	_, err = db.Update(llm, func() error {
		llm.LastInstantModelProbe = time.Now()
		return nil
	})
	if err != nil {
		return errors.Wrap(err, "update last_instant_model_probe")
	}

	return nil
}

func (llm *SLLM) PerformQuickModels(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input apis.LLMPerformQuickModelsInput) (*apis.LLMBatchPerformOutput, error) {
	if !utils.IsInArray(llm.Status, []string{apis.LLM_STATUS_RUNNING, apis.LLM_STATUS_READY}) {
		return nil, errors.Wrapf(errors.ErrInvalidStatus, "llm:%s(%s) status:%s", llm.Name, llm.Id, llm.Status)
	}

	llmStatus := llm.Status
	if len(input.Method) == 0 {
		input.Method = apis.QuickModelInstall
	}

	var toInstallSizeGb float64
	var errs []error
	for i := range input.Models {
		// specified by ID
		if len(input.Models[i].Id) > 0 {
			instModelObj, err := GetInstantModelManager().FetchByIdOrName(ctx, userCred, input.Models[i].Id)
			if err != nil {
				if errors.Cause(err) == sql.ErrNoRows {
					errs = append(errs, httperrors.NewResourceNotFoundError2(GetInstantModelManager().Keyword(), input.Models[i].Id))
				} else {
					errs = append(errs, errors.Wrap(err, "FetchByIdOrName"))
				}
			} else {
				instMdl := instModelObj.(*SInstantModel)
				input.Models[i].Id = instMdl.Id
				input.Models[i].ModelId = instMdl.ModelId
				input.Models[i].Tag = instMdl.ModelTag
				input.Models[i].LlmType = instMdl.LlmType
				if input.Method == apis.QuickModelInstall {
					toInstallSizeGb += float64(instMdl.GetActualSizeMb()) * 1024 * 1024 / 1000 / 1000 / 1000
				}
			}
		} else {
			mdl, err := GetInstantModelManager().FindInstantModel(input.Models[i].ModelId, input.Models[i].Tag, true)
			if err != nil {
				return nil, errors.Wrapf(err, "findInstantModel %s %s", input.Models[i].ModelId, input.Models[i].Tag)
			}
			if mdl == nil {
				errs = append(errs, httperrors.NewResourceNotFoundError2(GetInstantModelManager().Keyword(), input.Models[i].ModelId))
			} else {
				input.Models[i].Id = mdl.Id
				input.Models[i].Tag = mdl.ModelTag
				input.Models[i].ModelId = mdl.ModelId
				input.Models[i].LlmType = mdl.LlmType
			}
		}
		if !apis.IsLLMContainerType(input.Models[i].LlmType) || apis.LLMContainerType(input.Models[i].LlmType) != llm.GetLLMContainerDriver().GetType() {
			errs = append(errs, errors.Wrapf(httperrors.ErrInvalidStatus, "model %s is not of type %s", input.Models[i].Id, llm.GetLLMContainerDriver().GetType()))
		}
	}
	if len(errs) > 0 {
		return nil, errors.NewAggregate(errs)
	}
	if input.Method == apis.QuickModelInstall {
		if llm.InstantModelQuotaGb > 0 && toInstallSizeGb > float64(llm.InstantModelQuotaGb)-llm.GetTotalInstantModelSizeGb() {
			return nil, errors.Wrapf(httperrors.ErrOutOfQuota, "toInstallSizeGb %f > InstantAppQuotaGb %d - total %f Gb", toInstallSizeGb, llm.InstantModelQuotaGb, llm.GetTotalInstantModelSizeGb())
		}
	}

	task, err := llm.StartLLMInstantModelsSyncTask(ctx, userCred, llmStatus, input, "")
	if err != nil {
		return nil, errors.Wrap(err, "StartLLMInstantModelsSyncTask")
	}

	if input.Method == apis.QuickModelInstall {
		// save pending quota
		llm.insertPendingInstantModelQuota(task.Id, toInstallSizeGb)
	}

	output := apis.LLMBatchPerformOutput{
		Data: []apis.LLMPerformOutput{
			{
				Id:            llm.Id,
				Name:          llm.Name,
				RequestStatus: http.StatusOK,
				TaskId:        task.Id,
			},
		},
		Task: task,
	}
	return &output, nil
}

func (llm *SLLM) StartLLMInstantModelsSyncTask(ctx context.Context, userCred mcclient.TokenCredential, llmStatus string, input apis.LLMPerformQuickModelsInput, parentTaskid string) (*taskman.STask, error) {
	if !utils.IsInArray(llmStatus, []string{apis.LLM_STATUS_RUNNING, apis.LLM_STATUS_READY}) {
		return nil, errors.Wrapf(errors.ErrInvalidStatus, "cannot sync models in status %s", llmStatus)
	}
	taskInput := apis.LLMSyncModelTaskInput{
		LLMPerformQuickModelsInput: input,
		LLMStatus:                  llmStatus,
	}
	task, err := taskman.TaskManager.NewTask(ctx, "LLMInstantModelsSyncTask", llm, userCred, jsonutils.Marshal(taskInput).(*jsonutils.JSONDict), 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 (llm *SLLM) FetchModels(isProbed, isMounted, isSystem *bool) ([]SLLMInstantModel, error) {
	q := GetLLMInstantModelManager().Query().Equals("llm_id", llm.Id)
	q = GetLLMInstantModelManager().filterModels(q, isProbed, isMounted, isSystem)

	models := make([]SLLMInstantModel, 0)
	err := db.FetchModelObjects(GetLLMInstantModelManager(), q, &models)
	if err != nil {
		return nil, errors.Wrap(err, "FetchModelObjects")
	}
	return models, nil
}

func (llm *SLLM) FetchModelsFullName(isProbed, isMounted *bool) ([]string, error) {
	models, err := llm.FetchModels(isProbed, isMounted, nil)
	if err != nil {
		return nil, errors.Wrap(err, "FetchModels")
	}
	mdlFullNames := make([]string, len(models))
	for idx, mdl := range models {
		mdlFullNames[idx] = mdl.InstantModelId
	}
	return mdlFullNames, nil
}

func (llm *SLLM) FetchMountedModelFullName() ([]string, error) {
	boolTrue := true
	return llm.FetchModelsFullName(nil, &boolTrue)
}

func (llm *SLLM) FetchMountedModelInfo() ([]apis.MountedModelInfo, error) {
	boolTrue := true
	models, err := llm.FetchModels(nil, &boolTrue, nil)
	if err != nil {
		return nil, errors.Wrap(err, "FetchModels")
	}
	result := make([]apis.MountedModelInfo, len(models))
	for idx, mdl := range models {
		instMdl, _ := GetInstantModelManager().GetInstantModelById(mdl.InstantModelId)
		modelIdForDisplay := ""
		if instMdl != nil {
			modelIdForDisplay = instMdl.ModelId
		}
		result[idx] = apis.MountedModelInfo{
			FullName: mdl.ModelName + ":" + mdl.Tag,
			ModelId:  modelIdForDisplay,
			Id:       mdl.InstantModelId,
		}
	}
	return result, nil
}

func (llm *SLLM) RequestUnmountModel(ctx context.Context, userCred mcclient.TokenCredential, input apis.LLMSyncModelTaskInput) ([]string, []*commonapi.ContainerVolumeMountDiskPostOverlay, error) {
	if input.LLMStatus == apis.LLM_STATUS_RUNNING {
		err := llm.RefreshInstantModels(ctx, userCred, true)
		if err != nil {
			return nil, nil, errors.Wrap(err, "RefreshInstantModels")
		}
	}
	allModels, err := llm.FetchModels(nil, nil, nil)
	if err != nil {
		return nil, nil, errors.Wrap(err, "FetchModels")
	}
	drv, err := GetLLMContainerInstantModelDriver(llm.GetLLMContainerDriver().GetType())
	if err != nil {
		return nil, nil, nil
	}

	if input.LLMStatus == apis.LLM_STATUS_RUNNING {
		uninstallModels := findModelsToUninstall(allModels, input)
		for i := range uninstallModels {
			err := drv.UninstallModel(ctx, userCred, llm, &uninstallModels[i])
			if err != nil {
				log.Errorf("fail to uninstall %s", err)
				continue
			}
		}
	}

	// next found out models that need to unmount
	unmountModels := findModelsToUnmount(allModels, input)
	if len(unmountModels) == 0 {
		return nil, nil, nil
	}

	container, err := llm.GetLLMSContainer(ctx)
	if err != nil {
		return nil, nil, errors.Wrap(err, "GetContainer")
	}

	var unmountOverlays []*commonapi.ContainerVolumeMountDiskPostOverlay
	existingOverlays := container.Spec.VolumeMounts[0].Disk.PostOverlay

	for i := range existingOverlays {
		eOverlay := existingOverlays[i]
		if eOverlay.Image != nil && len(eOverlay.Image.Id) > 0 {
			find, err := isImageInUnmountModels(eOverlay.Image.Id, unmountModels)
			if err != nil {
				return nil, nil, errors.Wrap(err, "isImageInUnmountModels")
			}
			if find {
				unmountOverlays = append(unmountOverlays, eOverlay)
			}

		}
	}

	var modelIds []string
	for i := range unmountModels {
		modelIds = append(modelIds, unmountModels[i].InstantModelId)
	}
	return modelIds, unmountOverlays, nil
}

func (llm *SLLM) RequestMountModels(ctx context.Context, userCred mcclient.TokenCredential, input apis.LLMSyncModelTaskInput) ([]string, []string, []*commonapi.ContainerVolumeMountDiskPostOverlay, error) {
	if input.LLMStatus == apis.LLM_STATUS_RUNNING {
		err := llm.RefreshInstantModels(ctx, userCred, true)
		if err != nil {
			return nil, nil, nil, errors.Wrap(err, "RefreshApps")
		}
	}
	existingMdls, err := llm.FetchModels(nil, nil, nil)
	if err != nil {
		return nil, nil, nil, errors.Wrap(err, "FetchApps")
	}
	log.Debugf("=======RequestMountModels input: %s", jsonutils.Marshal(input).PrettyString())
	models, overlays, err := llm.getMountingModelsPostOverlay(ctx, input, existingMdls)
	if err != nil {
		return nil, nil, nil, errors.Wrap(err, "getMountingModelsPostOverlay")
	}
	drv, err := GetLLMContainerInstantModelDriver(llm.GetLLMContainerDriver().GetType())
	if err != nil {
		// return nil, nil, nil, nil
		log.Warningf("driver %s does not support instant model operations", llm.GetLLMContainerDriver().GetType())
	}
	var mdlIds []string
	for i := range models {
		model := models[i]
		if input.LLMStatus == apis.LLM_STATUS_RUNNING {
			if drv != nil {
				err := drv.PreInstallModel(ctx, userCred, llm, &model)
				if err != nil {
					log.Errorf("preinstallPackage fail %s", err)
				}
			}
		}
		mdlIds = append(mdlIds, model.InstantModelId)
	}
	targetDirs := make([]string, 0)
	for i := range overlays {
		if len(overlays[i].ContainerTargetDir) > 0 {
			targetDirs = append(targetDirs, overlays[i].ContainerTargetDir)
		}
	}
	return mdlIds, targetDirs, overlays, nil
}

func (llm *SLLM) TryContainerUnmountPaths(ctx context.Context, userCred mcclient.TokenCredential, s *mcclient.ClientSession, overlays []*commonapi.ContainerVolumeMountDiskPostOverlay, waitSecs int) error {
	start := time.Now()
	for time.Since(start) < time.Second*time.Duration(waitSecs) {
		err := llm.containerUnmountPaths(ctx, userCred, s, overlays)
		if err != nil {
			if strings.Contains(err.Error(), string(errors.ErrInvalidStatus)) {
				// wait
				time.Sleep(5 * time.Second)
			} else {
				return errors.Wrap(err, "containerMountPaths")
			}
		} else {
			// success
			return nil
		}
	}
	return errors.ErrTimeout
}

func (llm *SLLM) containerUnmountPaths(ctx context.Context, userCred mcclient.TokenCredential, s *mcclient.ClientSession, overlays []*commonapi.ContainerVolumeMountDiskPostOverlay) error {
	ctr, err := llm.GetLLMSContainer(ctx)
	if err != nil {
		return errors.Wrap(err, "GetSContainer")
	}

	if !computeapi.ContainerFinalStatus.Has(ctr.Status) {
		return errors.Wrapf(errors.ErrInvalidStatus, "cannot unmount post path in status %s", ctr.Status)
	}

	params := computeapi.ContainerVolumeMountRemovePostOverlayInput{
		Index:       0,
		PostOverlay: overlays,
		UseLazy:     true,
		ClearLayers: true,
	}
	_, err = compute.Containers.PerformAction(s, ctr.Id, "remove-volume-mount-post-overlay", jsonutils.Marshal(params))
	if err != nil {
		return errors.Wrap(err, "PerformAction remove-volume-mount-post-overlay")
	}
	return nil
}

func (llm *SLLM) TryContainerMountPaths(ctx context.Context, userCred mcclient.TokenCredential, s *mcclient.ClientSession, overlays []*commonapi.ContainerVolumeMountDiskPostOverlay, waitSecs int) error {
	start := time.Now()
	for time.Since(start) < time.Second*time.Duration(waitSecs) {
		err := llm.containerMountPaths(ctx, userCred, s, overlays)
		if err != nil {
			if strings.Contains(err.Error(), string(errors.ErrInvalidStatus)) {
				log.Errorf("containerMountPaths error %s, retry", err)
				// retry
				time.Sleep(5 * time.Second)
			} else {
				return errors.Wrap(err, "containerMountPaths")
			}
		} else {
			// success
			return nil
		}
	}
	return errors.ErrTimeout
}

func (llm *SLLM) containerMountPaths(ctx context.Context, userCred mcclient.TokenCredential, s *mcclient.ClientSession, overlays []*commonapi.ContainerVolumeMountDiskPostOverlay) error {
	ctr, err := llm.GetLLMSContainer(ctx)
	if err != nil {
		return errors.Wrap(err, "GetLLMSContainer")
	}

	if !computeapi.ContainerFinalStatus.Has(ctr.Status) {
		return errors.Wrapf(errors.ErrInvalidStatus, "cannot mount post path in status %s", ctr.Status)
	}
	params := computeapi.ContainerVolumeMountAddPostOverlayInput{
		Index:       0,
		PostOverlay: overlays,
	}
	_, err = compute.Containers.PerformAction(s, ctr.Id, "add-volume-mount-post-overlay", jsonutils.Marshal(params))
	if err != nil {
		return errors.Wrap(err, "PerformAction add-volume-mount-post-overlay")
	}
	return nil
}

func (llm *SLLM) MarkInstantModelsUnmounted(ctx context.Context, userCred mcclient.TokenCredential, llmStatus string, mdlIds []string) error {
	return llm.markInstantModelsMounted(ctx, userCred, llmStatus, mdlIds, false)
}

func (llm *SLLM) MarkInstantModelsMounted(ctx context.Context, userCred mcclient.TokenCredential, llmStatus string, mdlIds []string) error {
	return llm.markInstantModelsMounted(ctx, userCred, llmStatus, mdlIds, true)
}

func (llm *SLLM) markInstantModelsMounted(ctx context.Context, userCred mcclient.TokenCredential, llmStatus string, mdlIds []string, mounted bool) error {
	boolFalse := false
	boolTrue := true
	var isProbed *bool
	if !mounted {
		isProbed = &boolFalse
	} else {
		isProbed = &boolTrue
	}

	var errs []error
	for i := range mdlIds {
		_, err := GetLLMInstantModelManager().updateInstantModel(ctx, llm.Id, mdlIds[i], "", "", isProbed, &mounted)
		if err != nil {
			errs = append(errs, err)
		}
	}
	if len(errs) > 0 {
		return errors.NewAggregate(errs)
	}
	if llmStatus == apis.LLM_STATUS_RUNNING {
		err := llm.RefreshInstantModels(ctx, userCred, true)
		if err != nil {
			return errors.Wrap(err, "RefreshApps")
		}
	}
	mountedModelsFullName, err := llm.FetchMountedModelFullName()
	if err != nil {
		return errors.Wrap(err, "FetchMountedModelFullName")
	}
	{
		// save mounted apps to volume
		err := llm.UpdateVolumeMountedModelFullNames(mountedModelsFullName)
		if err != nil {
			return errors.Wrap(err, "UpdateVolumeMountedModelFullNames")
		}
	}
	logclient.AddActionLogWithContext(ctx, llm, logclient.ACT_UPDATE, mountedModelsFullName, userCred, true)
	return nil
}

func (llm *SLLM) UpdateVolumeMountedModelFullNames(mdlFullNames []string) error {
	volume, err := llm.GetVolume()
	if err != nil {
		return errors.Wrap(err, "GetVolume")
	}
	return volume.UpdateMountedModelFullNames(mdlFullNames)
}

type mdlFullNameInfo struct {
	InstantModelId string // instant model 主键 id
	ModelFullName  string
	IsMounted      bool
}

func (llm *SLLM) UpdateMountedModelFullNames(ctx context.Context, userCred mcclient.TokenCredential, mdlinfos []string, isReset bool, imageId string, skuId string) error {
	mdlFullNameInfos := make(map[string]*mdlFullNameInfo)
	for i := range mdlinfos {
		parts := strings.Split(mdlinfos[i], "@")
		mdlFullNameInfos[parts[0]] = &mdlFullNameInfo{
			InstantModelId: parts[0],
			ModelFullName:  parts[1],
			IsMounted:      false,
		}
	}

	preinstallModel := true

	if preinstallModel {
		sku, err := llm.GetLLMSku(skuId)
		if err != nil {
			return errors.Wrap(err, "GetLLMSku")
		}
		var deletedModelIds []string
		if !isReset {
			deletedModelIds, err = GetLLMInstantModelManager().getDeletedModelIds(llm.Id)
			if err != nil {
				return errors.Wrap(err, "getDeletedModelIds")
			}
		}
		mountedModels := sku.GetMountedModels()
		for i := range mountedModels {
			instMdl, err := GetInstantModelManager().FetchByIdOrName(ctx, userCred, mountedModels[i])
			if err != nil {
				return errors.Wrap(err, "FetchByIdOrName")
			}
			instantModle := instMdl.(*SInstantModel)
			if !isReset && slices.Contains(deletedModelIds, instantModle.Id) {
				// if not reset, and the model is deleted, skip it
				continue
			}
			if _, ok := mdlFullNameInfos[instantModle.Id]; !ok {
				mdlFullNameInfos[instantModle.Id] = &mdlFullNameInfo{
					InstantModelId: instantModle.Id,
					ModelFullName:  instantModle.ModelName + ":" + instantModle.ModelTag,
					IsMounted:      false,
				}
			}
		}
	}

	boolTrue := true
	boolFalse := false
	mountedModels, err := llm.FetchModels(nil, &boolTrue, nil)
	if err != nil {
		return errors.Wrap(err, "FetchApps")
	}
	for i := range mountedModels {
		find := false
		if _, ok := mdlFullNameInfos[mountedModels[i].InstantModelId]; ok {
			find = true
			mdlFullNameInfos[mountedModels[i].InstantModelId].IsMounted = true
		}
		if isReset && !find {
			// remove instant model not in mdlInfos
			mountedModel := mountedModels[i]
			log.Debugf("UpdateMountedModelFullNames remove model %s", mountedModel.InstantModelId)
			_, err := GetLLMInstantModelManager().updateInstantModel(ctx, llm.Id, mountedModel.InstantModelId, "", "", &boolFalse, &boolFalse)
			if err != nil {
				return errors.Wrap(err, "remove instant model")
			}
		}
	}

	installModelFullNames := make([]string, 0)
	for _, mdlFullNameInfo := range mdlFullNameInfos {
		installModelFullNames = append(installModelFullNames, mdlFullNameInfo.InstantModelId)
		if !mdlFullNameInfo.IsMounted {
			modelName, modelTag, _ := llm.GetLargeLanguageModelName(mdlFullNameInfo.ModelFullName)
			_, err := GetLLMInstantModelManager().updateInstantModel(ctx, llm.Id, mdlFullNameInfo.InstantModelId, modelName, modelTag, &boolFalse, &boolTrue)
			if err != nil {
				return errors.Wrap(err, "install model")
			}
		}
	}

	volume, _ := llm.GetVolume()
	if volume != nil {
		err := llm.UpdateVolumeMountedModelFullNames(installModelFullNames)
		if err != nil {
			return errors.Wrap(err, "UpdateVolumeMountedModelFullNames")
		}
	}

	return nil
}

func (llm *SLLM) GetMountedModelsPostOverlay() ([]*commonapi.ContainerVolumeMountDiskPostOverlay, error) {
	boolTrue := true
	mdls, err := llm.FetchModels(nil, &boolTrue, nil)
	if err != nil {
		return nil, errors.Wrap(err, "fetchApps")
	}
	if len(mdls) == 0 {
		return nil, nil
	}
	drv, err := GetLLMContainerInstantModelDriver(llm.GetLLMContainerDriver().GetType())
	if err != nil {
		return nil, nil
	}
	overlays, err := models2overlays(drv, mdls, false)
	if err != nil {
		return nil, errors.Wrap(err, "models2overlays")
	}
	return overlays, nil
}

func (llm *SLLM) getMountingModelsPostOverlay(ctx context.Context, input apis.LLMSyncModelTaskInput, existingMdls []SLLMInstantModel) ([]SLLMInstantModel, []*commonapi.ContainerVolumeMountDiskPostOverlay, error) {
	var models []SLLMInstantModel
	for i := range input.Models {
		if input.Method == apis.QuickModelInstall || input.Method == apis.QuickModelReinstall {
			mdl := input.Models[i]
			if input.Method == apis.QuickModelInstall {
				existingModel := findInstantModelWithModelInfo(existingMdls, mdl)
				if existingModel != nil && (existingModel.IsProbed || existingModel.IsMounted) {
					// if the model is already probed or mounted, skip mount
					continue
				}
			}
			model, err := GetLLMInstantModelManager().updateInstantModel(ctx, llm.Id, mdl.Id, mdl.DisplayName, mdl.Tag, nil, nil)
			if err != nil {
				return nil, nil, errors.Wrapf(err, "updateInstantModel %s", mdl.Id)
			}
			models = append(models, *model)
		}
	}
	if len(models) == 0 {
		return nil, nil, nil
	}
	drv, err := GetLLMContainerInstantModelDriver(llm.GetLLMContainerDriver().GetType())
	if err != nil {
		return nil, nil, nil
	}
	overlays, err := models2overlays(drv, models, true)
	if err != nil {
		return nil, nil, errors.Wrap(err, "models2overlays")
	}
	return models, overlays, nil
}

func models2overlays(drv ILLMContainerInstantModelDriver, models []SLLMInstantModel, isInstall bool) ([]*commonapi.ContainerVolumeMountDiskPostOverlay, error) {
	var errs []error
	var allDirs []apis.LLMMountDirInfo
	for i := range models {
		mdlDirs, err := models[i].getMountPaths(isInstall)
		if err != nil {
			errs = append(errs, err)
			continue
		}
		allDirs = append(allDirs, mdlDirs...)
	}
	if len(allDirs) == 0 {
		if len(errs) > 0 {
			return nil, errors.Wrap(errors.NewAggregate(errs), "getMountPaths")
		}
		return nil, nil
	}
	if len(errs) > 0 {
		log.Errorf("models2overlays getMountPaths error %s", errors.NewAggregate(errs))
	}
	var overlays []*commonapi.ContainerVolumeMountDiskPostOverlay
	for i := range allDirs {
		overlay := drv.GetDirPostOverlay(allDirs[i])
		overlays = append(overlays, overlay)
	}
	return overlays, nil
}

func (llm *SLLM) InstallInstantModels(ctx context.Context, userCred mcclient.TokenCredential, dirs []string, mdlIds []string) error {
	drv, err := GetLLMContainerInstantModelDriver(llm.GetLLMContainerDriver().GetType())
	if err != nil {
		return nil
	}
	return drv.InstallModel(ctx, userCred, llm, dirs, mdlIds)
}

func (llm *SLLM) EnsureInstantModelsInstalled(ctx context.Context, userCred mcclient.TokenCredential, mdlIds []string) error {
	mdlMap, err := llm.getProbedInstantModelsExt(ctx, userCred, mdlIds...)
	if err != nil {
		return errors.Wrap(err, "FetchApps")
	}
	if len(mdlIds) == 0 {
		return nil
	}
	instModels := make(map[string]SInstantModel)
	if err := db.FetchModelObjectsByIds(GetInstantModelManager(), "id", mdlIds, &instModels); err != nil {
		return errors.Wrap(err, "FetchModelObjectsByIds")
	}
	var errs []error
	for _, mdlId := range mdlIds {
		if _, ok := mdlMap[mdlId]; ok {
			continue
		}
		instModel, ok := instModels[mdlId]
		if !ok {
			errs = append(errs, errors.Wrapf(errors.ErrNotFound, "mdlId %s", mdlId))
			continue
		}
		found := false
		for _, info := range mdlMap {
			if info.ModelId == instModel.ModelId {
				found = true
				break
			}
		}
		if !found {
			errs = append(errs, errors.Wrapf(errors.ErrNotFound, "mdlId %s", mdlId))
		}
	}
	if len(errs) > 0 {
		return errors.NewAggregate(errs)
	}
	return nil
}
