// 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"
	"database/sql"
	"time"

	"yunion.io/x/jsonutils"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/sqlchemy"

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

var (
	migrationAlertMan *SMigrationAlertManager
)

func init() {
	migrationAlertMan = GetMigrationAlertManager()
}

func GetMigrationAlertManager() *SMigrationAlertManager {
	if migrationAlertMan != nil {
		return migrationAlertMan
	}
	migrationAlertMan = &SMigrationAlertManager{
		SAlertManager: *NewAlertManager(SMigrationAlert{}, "migrationalert", "migrationalerts"),
	}
	migrationAlertMan.SetVirtualObject(migrationAlertMan)
	return migrationAlertMan
}

// +onecloud:swagger-gen-model-singular=migrationalert
// +onecloud:swagger-gen-model-plural=migrationalerts
type SMigrationAlertManager struct {
	SAlertManager
}

type SMigrationAlert struct {
	SAlert

	MetricType   string               `create:"admin_required" list:"admin" get:"admin"`
	MigrateNotes jsonutils.JSONObject `nullable:"true" list:"admin" get:"admin" update:"admin" create:"admin_optional"`
}

type MigrateNoteGuest struct {
	Id        string  `json:"id"`
	Name      string  `json:"name"`
	HostId    string  `json:"host_id"`
	Host      string  `json:"host"`
	VCPUCount int     `json:"vcpu_count"`
	VMemSize  int     `json:"vmem_size"`
	Score     float64 `json:"score"`
}

type MigrateNoteTarget struct {
	Id    string  `json:"id"`
	Name  string  `json:"name"`
	Score float64 `json:"score"`
}

type MigrateNote struct {
	Guest  *MigrateNoteGuest  `json:"guest"`
	Target *MigrateNoteTarget `json:"target_host"`
	Error  string             `json:"error"`
}

func (m *SMigrationAlertManager) ListItemFilter(ctx context.Context, q *sqlchemy.SQuery, userCred mcclient.TokenCredential, query monitor.MigrationAlertListInput) (*sqlchemy.SQuery, error) {
	q, err := m.SAlertManager.ListItemFilter(ctx, q, userCred, query.AlertListInput)
	if err != nil {
		return nil, err
	}
	if len(query.MetricType) > 0 {
		q = q.Equals("metric_type", query.MetricType)
	}
	q = q.Equals("used_by", AlertNotificationUsedByMigrationAlert)
	return q, nil
}

func (m *SMigrationAlertManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input *monitor.MigrationAlertCreateInput) (*monitor.MigrationAlertCreateInput, error) {
	if input.Period == "" {
		input.Period = "5m"
	}
	if _, err := time.ParseDuration(input.Period); err != nil {
		return nil, httperrors.NewInputParameterError("Invalid period format: %s", input.Period)
	}

	if err := monitor.IsValidMigrationAlertMetricType(input.MetricType); err != nil {
		return nil, httperrors.NewInputParameterError("Invalid metric_type %v", err)
	}

	if input.MigrationSettings == nil {
		input.MigrationSettings = &monitor.MigrationAlertSettings{
			Source: new(monitor.MigrationAlertSettingsSource),
			Target: new(monitor.MigrationAlertSettingsTarget),
		}
	}
	if err := m.ValidateMigrationSettings(input.MigrationSettings); err != nil {
		return nil, errors.Wrap(err, "validate migration settings")
	}

	aInput := *(input.ToAlertCreateInput())
	aInput, err := AlertManager.ValidateCreateData(ctx, userCred, nil, query, aInput)
	if err != nil {
		return input, errors.Wrap(err, "AlertManager.ValidateCreateData")
	}
	input.AlertCreateInput = aInput

	return input, nil
}

func (m *SMigrationAlertManager) ValidateMigrationSettings(s *monitor.MigrationAlertSettings) error {
	if s.Source != nil {
		if err := m.ValidateMigrationSettingsSource(s.Source); err != nil {
			return errors.Wrap(err, "validate source")
		}
	}
	if s.Target != nil {
		if err := m.ValidateMigrationSettingsTarget(s.Target); err != nil {
			return errors.Wrap(err, "validate target")
		}
	}
	return nil
}

func (m *SMigrationAlertManager) GetResourceByIdOrName(rType string, id string) (jsonutils.JSONObject, error) {
	ok, objs := MonitorResourceManager.GetResourceObjByResType(rType)
	if !ok {
		return nil, errors.Errorf("Get by %q", rType)
	}
	for _, obj := range objs {
		name, _ := obj.GetString("name")
		if name == id {
			return obj, nil
		}
		objId, _ := obj.GetString("id")
		if objId == id {
			return obj, nil
		}
	}
	return nil, errors.Errorf("Not found resource %q by %q", rType, id)
}

func (m *SMigrationAlertManager) GetHostByIdOrName(id string) (jsonutils.JSONObject, error) {
	return m.GetResourceByIdOrName(monitor.METRIC_RES_TYPE_HOST, id)
}

func (m *SMigrationAlertManager) GetGuestByIdOrName(id string) (jsonutils.JSONObject, error) {
	return m.GetResourceByIdOrName(monitor.METRIC_RES_TYPE_GUEST, id)
}

func (m *SMigrationAlertManager) validateResource(vf func(idOrName string) (jsonutils.JSONObject, error), ids []string) ([]string, error) {
	nIds := make([]string, len(ids))
	for idx, idName := range ids {
		obj, err := vf(idName)
		if err != nil {
			return nil, errors.Wrapf(err, "find by %s", idName)
		}
		id, err := obj.GetString("id")
		if err != nil {
			return nil, errors.Wrap(err, "get id")
		}
		nIds[idx] = id
	}
	return nIds, nil
}

func (m *SMigrationAlertManager) ValidateMigrationSettingsSource(input *monitor.MigrationAlertSettingsSource) error {
	if len(input.HostIds) != 0 {
		hIds, err := m.validateResource(m.GetHostByIdOrName, input.HostIds)
		if err != nil {
			return errors.Wrap(err, "validate host")
		}
		input.HostIds = hIds
	}
	if len(input.GuestIds) != 0 {
		gIds, err := m.validateResource(m.GetGuestByIdOrName, input.GuestIds)
		if err != nil {
			return errors.Wrap(err, "validate guest")
		}
		input.GuestIds = gIds
	}
	return nil
}

func (m *SMigrationAlertManager) ValidateMigrationSettingsTarget(input *monitor.MigrationAlertSettingsTarget) error {
	if len(input.HostIds) != 0 {
		hIds, err := m.validateResource(m.GetHostByIdOrName, input.HostIds)
		if err != nil {
			return errors.Wrap(err, "validate host")
		}
		input.HostIds = hIds
	}
	return nil
}

func (alert *SMigrationAlert) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
	out := new(monitor.MigrationAlertCreateInput)
	if err := data.Unmarshal(out); err != nil {
		return errors.Wrap(err, "Unmarshal to MigrationAlertCreateInput")
	}
	fs := out.GetMetricDriver().GetQueryFields()
	alert.ResType = string(fs.ResourceType)
	alert.UsedBy = AlertNotificationUsedByMigrationAlert

	if err := alert.SAlert.CustomizeCreate(ctx, userCred, ownerId, query, data); err != nil {
		return errors.Wrap(err, "SAlert.CustomizeCreate")
	}
	return alert.CreateNotification(ctx, userCred)
}

func (m *SMigrationAlertManager) FetchAllMigrationAlerts() ([]SMigrationAlert, error) {
	objs := make([]SMigrationAlert, 0)
	q := m.Query()
	q = q.IsTrue("enabled")
	err := db.FetchModelObjects(m, q, &objs)
	if err != nil && err != sql.ErrNoRows {
		return nil, errors.Wrap(err, "db.FetchModelObjects")
	}
	return objs, nil
}

func (m *SMigrationAlertManager) GetInMigrationAlerts() ([]*SMigrationAlert, error) {
	alerts, err := m.FetchAllMigrationAlerts()
	if err != nil {
		return nil, errors.Wrap(err, "FetchAllMigrationAlerts")
	}
	objs := make([]*SMigrationAlert, 0)
	for _, a := range alerts {
		if ok, _, _ := a.IsInMigrationProcess(); ok {
			tmp := a
			objs = append(objs, &tmp)
		}
	}
	return objs, nil
}

func (alert *SMigrationAlert) GetMigrateNotes() (map[string]MigrateNote, error) {
	if alert.MigrateNotes == nil {
		return make(map[string]MigrateNote, 0), nil
	}
	objs := make(map[string]MigrateNote, 0)
	if err := alert.MigrateNotes.Unmarshal(&objs); err != nil {
		return nil, errors.Wrap(err, "Unmarshal")
	}
	return objs, nil
}

func (alert *SMigrationAlert) SetMigrateNote(ctx context.Context, ns *MigrateNote, isDelete bool) error {
	_, err := db.UpdateWithLock(ctx, alert, func() error {
		curNotes, err := alert.GetMigrateNotes()
		if err != nil {
			return errors.Wrap(err, "GetMigrateNotes")
		}
		if isDelete {
			delete(curNotes, ns.Guest.Id)
		} else {
			curNotes[ns.Guest.Id] = *ns
		}
		alert.MigrateNotes = jsonutils.Marshal(curNotes)
		return nil
	})
	return err
}

func (alert *SMigrationAlert) IsInMigrationProcess() (bool, map[string]MigrateNote, error) {
	notes, err := alert.GetMigrateNotes()
	if err != nil {
		return false, nil, errors.Wrap(err, "GetMigrateNotes")
	}
	if len(notes) == 0 {
		return false, nil, nil
	}
	return true, nil, nil
}

func (alert *SMigrationAlert) GetMigrationSettings() (*monitor.MigrationAlertSettings, error) {
	if alert.CustomizeConfig == nil {
		return nil, errors.Errorf("CustomizeConfig is nil")
	}
	out := new(monitor.MigrationAlertSettings)
	if err := alert.CustomizeConfig.Unmarshal(out); err != nil {
		return nil, err
	}
	return out, nil
}

func (alert *SMigrationAlert) CreateNotification(ctx context.Context, userCred mcclient.TokenCredential) error {
	if alert.Id == "" {
		alert.Id = db.DefaultUUIDGenerator()
	}
	noti, err := NotificationManager.CreateAutoMigrationNotification(ctx, userCred, alert)
	if err != nil {
		return errors.Wrap(err, "CreateAutoMigrationNotification")
	}
	if _, err := alert.AttachNotification(ctx, userCred, noti, monitor.AlertNotificationStateUnknown, ""); err != nil {
		return errors.Wrap(err, "alert.AttachNotification")
	}
	return nil
}

func (alert *SMigrationAlert) GetMetricType() monitor.MigrationAlertMetricType {
	return monitor.MigrationAlertMetricType(alert.MetricType)
}

func (alert *SMigrationAlert) CustomizeDelete(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
	if err := alert.deleteNotifications(ctx, userCred, query, data); err != nil {
		return errors.Wrap(err, "delete related notification")
	}
	return alert.SAlert.CustomizeDelete(ctx, userCred, query, data)
}