// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package models

import (
	"context"
	"regexp"
	"strconv"
	"strings"

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

	api "yunion.io/x/onecloud/pkg/apis/compute"
	"yunion.io/x/onecloud/pkg/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/httperrors"
	"yunion.io/x/onecloud/pkg/mcclient"
)

var IsolatedDeviceModelManager *SIsolatedDeviceModelManager

func init() {
	IsolatedDeviceModelManager = &SIsolatedDeviceModelManager{
		SStandaloneAnonResourceBaseManager: db.NewStandaloneAnonResourceBaseManager(
			SIsolatedDeviceModel{},
			"isolated_device_models_tbl",
			"isolated_device_model",
			"isolated_device_models",
		),
	}
	IsolatedDeviceModelManager.SetVirtualObject(IsolatedDeviceModelManager)
}

type SIsolatedDeviceModelManager struct {
	db.SStandaloneAnonResourceBaseManager
}

type SIsolatedDeviceModel struct {
	db.SStandaloneAnonResourceBase

	Model string `width:"512" charset:"ascii" nullable:"false" list:"domain" create:"domain_required" update:"domain"`

	VendorId string `width:"16" charset:"ascii" nullable:"false" list:"domain" create:"domain_required" update:"domain"`
	DeviceId string `width:"16" charset:"ascii" nullable:"false" list:"domain" create:"domain_required" update:"domain"`

	DevType string `width:"16" charset:"ascii" nullable:"false" list:"domain" create:"domain_required"`

	HotPluggable tristate.TriState `default:"false" list:"domain" create:"domain_optional" update:"domain"`

	// Disable auto detect isolated devices on host
	DisableAutoDetect tristate.TriState `default:"false" list:"domain" create:"domain_optional" update:"domain"`
}

var _ db.IStandaloneModel = new(SIsolatedDeviceModel)

func (self *SIsolatedDeviceModel) SetName(name string) {}

func (manager *SIsolatedDeviceModelManager) ValidateCreateData(ctx context.Context,
	userCred mcclient.TokenCredential,
	ownerId mcclient.IIdentityProvider,
	query jsonutils.JSONObject,
	input api.IsolatedDeviceModelCreateInput,
) (api.IsolatedDeviceModelCreateInput, error) {
	if utils.IsInStringArray(input.DevType, api.VALID_PASSTHROUGH_TYPES) {
		return input, httperrors.NewInputParameterError("device type %q unsupported", input.DevType)
	}

	input.VendorId = strings.ToLower(input.VendorId)
	input.DeviceId = strings.ToLower(input.DeviceId)
	deviceVendorReg := regexp.MustCompile(`^[a-f0-9]{4}$`)

	if !deviceVendorReg.MatchString(input.VendorId) {
		return input, httperrors.NewInputParameterError("bad vendor id %s", input.VendorId)
	}
	if !deviceVendorReg.MatchString(input.DeviceId) {
		return input, httperrors.NewInputParameterError("bad vendor id %s", input.DeviceId)
	}

	if cnt := manager.Query().Equals("vendor_id", input.VendorId).Equals("device_id", input.DeviceId).Count(); cnt > 0 {
		return input, httperrors.NewDuplicateResourceError("vendor %s device %s has been registered", input.VendorId, input.DeviceId)
	}

	if cnt := manager.Query().Equals("model", input.Model).Count(); cnt > 0 {
		return input, httperrors.NewDuplicateResourceError("model %s has been registered", input.Model)
	}

	return input, nil
}

func (self *SIsolatedDeviceModel) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
	input := api.IsolatedDeviceModelCreateInput{}
	err := data.Unmarshal(&input)
	if err != nil {
		log.Errorf("!!!data.Unmarshal api.IsolatedDeviceModelCreateInput fail %s", err)
	}

	go func() {
		for i := range input.Hosts {
			iHost, err := HostManager.FetchByIdOrName(ctx, userCred, input.Hosts[i])
			if err != nil {
				log.Errorf("failed fetch host %s: %s", input.Hosts[i], err)
				continue
			}
			host := iHost.(*SHost)

			if self.DisableAutoDetect.Bool() {
				err = self.Attach2Host(ctx, userCred, host)
				if err != nil {
					log.Errorf("failed attach to host %s: %s", input.Hosts[i], err)
					continue
				}
			}

			log.Infof("start request host %s scan isolated devices", host.GetName())
			if err := host.RequestScanIsolatedDevices(ctx, userCred); err != nil {
				log.Errorf("failed scan isolated device %s", err)
			}
		}
	}()
}

func (self *SIsolatedDeviceModel) Attach2Host(ctx context.Context, userCred mcclient.TokenCredential, host *SHost) error {
	hs := SHostIsolatedDeviceModel{}
	hs.SetModelManager(HostIsolatedDeviceModelManager, &hs)

	hs.IsolatedDeviceModelId = self.Id
	hs.HostId = host.Id
	err := HostIsolatedDeviceModelManager.TableSpec().Insert(ctx, &hs)
	if err != nil {
		return err
	}
	db.OpsLog.LogAttachEvent(ctx, host, self, userCred, nil)
	return nil
}

func (self *SIsolatedDeviceModel) ValidateDeleteCondition(ctx context.Context, info jsonutils.JSONObject) error {
	modelIsEmpty, err := IsolatedDeviceManager.CheckModelIsEmpty(self.Model, self.VendorId, self.DeviceId, self.DevType)
	if err != nil {
		return err
	}
	if !modelIsEmpty {
		return httperrors.NewNotEmptyError("device model has guests")
	}
	return nil
}

func (self *SIsolatedDeviceModel) PostDelete(ctx context.Context, userCred mcclient.TokenCredential) {
	hosts, err := IsolatedDeviceManager.GetHostsByModel(self.Model, self.VendorId, self.DeviceId, self.DevType)
	if err != nil {
		log.Errorf("failed get hosts by isolated device model: %s", err)
		return
	}
	go func() {
		for i := range hosts {
			iHost, err := HostManager.FetchByIdOrName(ctx, userCred, hosts[i])
			if err != nil {
				log.Errorf("failed fetch host %s: %s", hosts[i], err)
				continue
			}
			host := iHost.(*SHost)
			log.Infof("start request host %s scan isolated devices", host.GetName())
			if err := host.RequestScanIsolatedDevices(ctx, userCred); err != nil {
				log.Errorf("failed scan isolated device %s", err)
			}
		}
	}()
}

func (self *SIsolatedDeviceModel) ValidateUpdateData(
	ctx context.Context, userCred mcclient.TokenCredential,
	query jsonutils.JSONObject, input api.IsolatedDeviceModelUpdateInput,
) (api.IsolatedDeviceModelUpdateInput, error) {
	input.VendorId = strings.ToLower(input.VendorId)
	input.DeviceId = strings.ToLower(input.DeviceId)
	deviceVendorReg := regexp.MustCompile(`^[a-f0-9]{4}$`)

	if input.VendorId != "" && !deviceVendorReg.MatchString(input.VendorId) {
		return input, httperrors.NewInputParameterError("bad vendor id %s", input.VendorId)
	}
	if input.DeviceId != "" && !deviceVendorReg.MatchString(input.DeviceId) {
		return input, httperrors.NewInputParameterError("bad vendor id %s", input.DeviceId)
	}

	if input.VendorId != "" || input.DeviceId != "" {
		if input.VendorId == "" {
			input.VendorId = self.VendorId
		}
		if input.DeviceId == "" {
			input.DeviceId = self.DeviceId
		}

		if self.VendorId != input.VendorId || self.DeviceId != input.DeviceId {
			if cnt := IsolatedDeviceModelManager.Query().Equals("vendor_id", input.VendorId).Equals("device_id", input.DeviceId).Count(); cnt > 0 {
				return input, httperrors.NewDuplicateResourceError("vendor %s device %s has been registered", input.VendorId, input.DeviceId)
			}
		}
	}

	if self.Model != input.Model {
		if cnt := IsolatedDeviceModelManager.Query().Equals("model", input.Model).Count(); cnt > 0 {
			return input, httperrors.NewDuplicateResourceError("model %s has been registered", input.Model)
		}
	}

	return input, nil
}

func (manager *SIsolatedDeviceModelManager) ListItemFilter(
	ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	query api.IsolatedDeviceModelListInput,
) (*sqlchemy.SQuery, error) {
	q, err := manager.SStandaloneAnonResourceBaseManager.ListItemFilter(ctx, q, userCred, query.StandaloneAnonResourceListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SStandaloneResourceBaseManager.ListItemFilter")
	}

	if len(query.DevType) > 0 {
		q = q.In("dev_type", query.DevType)
	}
	if len(query.Model) > 0 {
		q = q.In("model", query.Model)
	}

	if len(query.VendorId) > 0 {
		q = q.Equals("vendor_id", query.VendorId)
	}
	if len(query.DeviceId) > 0 {
		q = q.Equals("device_id", query.DeviceId)
	}
	if len(query.HostId) > 0 {
		hidmq := HostIsolatedDeviceModelManager.Query("isolated_device_model_id").Equals("host_id", query.HostId).SubQuery()
		q = q.Filter(sqlchemy.OR(
			sqlchemy.IsFalse(q.Field("disable_auto_detect")),
			sqlchemy.IsNull(q.Field("disable_auto_detect")),
			sqlchemy.In(q.Field("id"), hidmq)),
		)
	}

	return q, nil
}

func (manager *SIsolatedDeviceModelManager) GetByVendorDevice(vendorId, deviceId string) (*SIsolatedDeviceModel, error) {
	devModel := new(SIsolatedDeviceModel)
	err := manager.Query().Equals("vendor_id", vendorId).Equals("device_id", deviceId).First(devModel)
	if err != nil {
		return nil, err
	}
	devModel.SetModelManager(manager, devModel)
	return devModel, nil
}

func (manager *SIsolatedDeviceModelManager) GetByDevModel(model string) (*SIsolatedDeviceModel, error) {
	devModel := new(SIsolatedDeviceModel)
	err := manager.Query().Equals("model", model).First(devModel)
	if err != nil {
		return nil, err
	}
	devModel.SetModelManager(manager, devModel)
	return devModel, nil
}

func (manager *SIsolatedDeviceModelManager) GetByDevType(devType string) (*SIsolatedDeviceModel, error) {
	devModel := new(SIsolatedDeviceModel)
	err := manager.Query().Equals("dev_type", devType).First(devModel)
	if err != nil {
		return nil, err
	}
	devModel.SetModelManager(manager, devModel)
	return devModel, nil
}

func (obj *SIsolatedDeviceModel) PerformSetHardwareInfo(ctx context.Context, userCred mcclient.TokenCredential, _ jsonutils.JSONObject, data *api.IsolatedDeviceModelHardwareInfo) (*api.IsolatedDeviceModelHardwareInfo, error) {
	settings := map[string]interface{}{
		api.ISOLATED_DEVICE_MODEL_METADATA_MEMORY_MB: data.MemoryMB,
		api.ISOLATED_DEVICE_MODEL_METADATA_TFLOPS:    data.TFLOPS,
		api.ISOLATED_DEVICE_MODEL_METADATA_BANDWIDTH: data.Bandwidth,
	}

	for k, v := range settings {
		if err := obj.SetMetadata(ctx, k, v, userCred); err != nil {
			return nil, errors.Wrapf(err, "set %s to %v", k, v)
		}
	}
	return data, nil
}

func (obj *SIsolatedDeviceModel) GetDetailsHardwareInfo(ctx context.Context, userCred mcclient.TokenCredential, _ jsonutils.JSONObject) (*api.IsolatedDeviceModelHardwareInfo, error) {
	info := new(api.IsolatedDeviceModelHardwareInfo)
	// parse memory size MB
	if memStr := obj.GetMetadata(ctx, api.ISOLATED_DEVICE_MODEL_METADATA_MEMORY_MB, userCred); memStr != "" {
		memSize, err := strconv.Atoi(memStr)
		if err != nil {
			return nil, errors.Wrapf(err, "convert memory size %d to int", memSize)
		}
		info.MemoryMB = memSize
	}
	// parse bandwidth
	if bwStr := obj.GetMetadata(ctx, api.ISOLATED_DEVICE_MODEL_METADATA_BANDWIDTH, userCred); bwStr != "" {
		bw, err := strconv.ParseFloat(bwStr, 64)
		if err != nil {
			return nil, errors.Wrapf(err, "convert bandwidth %s to float", bwStr)
		}
		info.Bandwidth = bw
	}
	// parse TFLOPS
	if tflopsStr := obj.GetMetadata(ctx, api.ISOLATED_DEVICE_MODEL_METADATA_TFLOPS, userCred); tflopsStr != "" {
		tflops, err := strconv.ParseFloat(tflopsStr, 64)
		if err != nil {
			return nil, errors.Wrapf(err, "convert TFLOPS %s to float", tflopsStr)
		}
		info.TFLOPS = tflops
	}
	return info, nil
}
