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

	"yunion.io/x/jsonutils"
	"yunion.io/x/log"
	"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"
	"yunion.io/x/onecloud/pkg/mcclient/auth"
	"yunion.io/x/onecloud/pkg/mcclient/modules/compute"
	merrors "yunion.io/x/onecloud/pkg/monitor/errors"
	"yunion.io/x/onecloud/pkg/monitor/options"
	"yunion.io/x/onecloud/pkg/util/stringutils2"
)

const (
	MeterAlertMetadataType      = "type"
	MeterAlertMetadataProjectId = "project_id"
	MeterAlertMetadataAccountId = "account_id"
	MeterAlertMetadataProvider  = "provider"
)

var MeterAlertManager *SMeterAlertManager

func init() {
	MeterAlertManager = NewMeterAlertManager()
}

type IMeterAlertDriver interface {
	GetType() string
	GetName() string
	GetFor() time.Duration
	ToAlertCreateInput(input monitor.MeterAlertCreateInput, allAccountIds []string, level string) monitor.AlertCreateInput
}

// +onecloud:swagger-gen-ignore
type SMeterAlertManager struct {
	SV1AlertManager

	drivers map[string]IMeterAlertDriver
}

func NewMeterAlertManager() *SMeterAlertManager {
	man := &SMeterAlertManager{
		SV1AlertManager: SV1AlertManager{
			SAlertManager: *NewAlertManager(SMeterAlert{}, "meteralert", "meteralerts"),
		},
	}
	man.SetVirtualObject(man)
	man.registerDriver(man.newDailyFeeDriver())
	man.registerDriver(man.newMonthFeeDriver())
	return man
}

// +onecloud:swagger-gen-ignore
type SMeterAlert struct {
	SV1Alert
}

func (man *SMeterAlertManager) newDailyFeeDriver() IMeterAlertDriver {
	return new(sMeterDailyFee)
}

func (man *SMeterAlertManager) newMonthFeeDriver() IMeterAlertDriver {
	return new(sMeterMonthFee)
}

func (man *SMeterAlertManager) registerDriver(drv IMeterAlertDriver) {
	if man.drivers == nil {
		man.drivers = make(map[string]IMeterAlertDriver, 0)
	}
	man.drivers[drv.GetType()] = drv
}

func (man *SMeterAlertManager) GetDriver(typ string) IMeterAlertDriver {
	return man.drivers[typ]
}

func (man *SMeterAlertManager) genName(ctx context.Context, ownerId mcclient.IIdentityProvider, hint string) (string, error) {
	return db.GenerateName(ctx, man, ownerId, hint)
}

func (man *SMeterAlertManager) getAllBillAccounts(ctx context.Context) ([]jsonutils.JSONObject, error) {
	s := auth.GetAdminSession(ctx, options.Options.Region)
	results := make([]jsonutils.JSONObject, 0)
	for {
		q := jsonutils.NewDict()
		q.Add(jsonutils.NewString("system"), "scope")
		q.Add(jsonutils.NewInt(int64(len(results))), "offset")
		q.Add(jsonutils.NewInt(20), "limit")
		ret, err := compute.Cloudaccounts.List(s, q)
		if err != nil {
			return nil, err
		}
		if len(ret.Data) == 0 {
			break
		}
		results = append(results, ret.Data...)
		if ret.Total <= len(results) {
			break
		}
	}
	return results, nil
}

func (man *SMeterAlertManager) getAllBillAccountIds(ctx context.Context) ([]string, error) {
	objs, err := man.getAllBillAccounts(ctx)
	if err != nil {
		return nil, err
	}
	ids := make([]string, len(objs))
	for idx, obj := range objs {
		id, err := obj.GetString("id")
		if err != nil {
			return nil, err
		}
		ids[idx] = id
	}
	return ids, nil
}

func (man *SMeterAlertManager) ValidateCreateData(
	ctx context.Context, userCred mcclient.TokenCredential,
	ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject,
	data monitor.MeterAlertCreateInput) (*monitor.MeterAlertCreateInput, error) {
	if data.Period == "" {
		// default 30 minutes
		data.Period = "30m"
	}
	if data.Window == "" {
		// default 5 minutes
		data.Window = "5m"
	}
	if _, err := time.ParseDuration(data.Period); err != nil {
		return nil, httperrors.NewInputParameterError("Invalid period format: %s", data.Period)
	}
	drv := man.GetDriver(data.Type)
	if drv == nil {
		return nil, httperrors.NewInputParameterError("not support type %q", data.Type)
	}
	name, err := man.genName(ctx, ownerId, drv.GetName())
	if err != nil {
		return nil, err
	}
	allAccountIds := []string{}
	if data.AccountId == "" {
		allAccountIds, err = man.getAllBillAccountIds(ctx)
		if err != nil {
			return nil, err
		}
	}
	if data.Recipients == "" {
		return nil, merrors.NewArgIsEmptyErr("recipients")
	}

	alertInput := drv.ToAlertCreateInput(data, allAccountIds, data.Level)
	alertInput, err = AlertManager.ValidateCreateData(ctx, userCred, ownerId, query, alertInput)
	if err != nil {
		return nil, err
	}
	data.AlertCreateInput = alertInput
	data.Name = name
	return &data, nil
}

type sMeterDailyFee struct{}

func (_ *sMeterDailyFee) GetType() string {
	return monitor.MeterAlertTypeDailyResFee
}

func (_ *sMeterDailyFee) GetName() string {
	return "日消费"
}

func (_ *sMeterDailyFee) GetFor() time.Duration {
	return 12 * time.Hour
}

func (f *sMeterDailyFee) ToAlertCreateInput(
	input monitor.MeterAlertCreateInput,
	allAccountIds []string,
	level string,
) monitor.AlertCreateInput {
	freq, _ := time.ParseDuration(input.Window)
	customizeConfig := monitor.MeterCustomizeConfig{
		Name: f.GetName(),
	}
	ret := monitor.AlertCreateInput{
		Name:            f.GetName(),
		Level:           level,
		Frequency:       int64(freq / time.Second),
		CustomizeConfig: jsonutils.Marshal(customizeConfig),
		Settings: GetMeterAlertSetting(input,
			"account_daily_resfee",
			"meter_db", allAccountIds, "sumDate"),
	}
	return ret
}

type sMeterMonthFee struct{}

func (_ *sMeterMonthFee) GetType() string {
	return monitor.MeterAlertTypeMonthResFee
}

func (_ *sMeterMonthFee) GetName() string {
	return "月消费"
}

func (_ *sMeterMonthFee) GetFor() time.Duration {
	return 24 * time.Hour
}

func (f *sMeterMonthFee) ToAlertCreateInput(
	input monitor.MeterAlertCreateInput,
	allAccountIds []string,
	level string,
) monitor.AlertCreateInput {
	freq, _ := time.ParseDuration(input.Window)
	customizeConfig := monitor.MeterCustomizeConfig{
		Name: f.GetName(),
	}
	ret := monitor.AlertCreateInput{
		Name:            f.GetName(),
		Level:           level,
		Frequency:       int64(freq / time.Second),
		CustomizeConfig: jsonutils.Marshal(customizeConfig),
		Settings: GetMeterAlertSetting(input,
			"account_month_resfee",
			"meter_db", allAccountIds, "sumMonth"),
	}
	return ret
}

func GetMeterAlertSetting(
	input monitor.MeterAlertCreateInput,
	measurement string,
	db string,
	accountIds []string,
	groupByStr string,
) monitor.AlertSetting {
	q, reducer, eval := GetMeterAlertQuery(input, measurement, db, accountIds, groupByStr)
	return monitor.AlertSetting{
		Conditions: []monitor.AlertCondition{
			{
				Type:     "query",
				Operator: "and",
				Query: monitor.AlertQuery{
					Model: q,
					From:  input.Period,
					To:    "now",
				},
				Reducer:   reducer,
				Evaluator: eval,
			},
		},
	}
}

func GetMeterAlertQuery(
	input monitor.MeterAlertCreateInput,
	measurement string,
	db string,
	allAccountIds []string,
	groupByStr string,
) (
	monitor.MetricQuery,
	monitor.Condition,
	monitor.Condition) {
	var (
		evaluator, reducer monitor.Condition
		alertType, field   string
		filters            []monitor.MetricQueryTag
	)
	groupBy := []monitor.MetricQueryPart{}
	evaluator = monitor.GetNodeAlertEvaluator(input.Comparator, input.Threshold)

	if input.AccountId == "" {
		reducer = monitor.Condition{Type: "sum"}
		alertType = "overview"
		field = "sum"
		for _, aId := range allAccountIds {
			filters = append(filters, monitor.MetricQueryTag{
				Key:       "accountId",
				Value:     aId,
				Condition: "or",
			})
		}
	} else {
		reducer = monitor.Condition{Type: "avg"}
		alertType = "account"
		field = input.Type
		groupBy = append(groupBy, monitor.MetricQueryPart{
			Type:   "field",
			Params: []string{field},
		})
		filters = append(filters, monitor.MetricQueryTag{
			Key:       "accountId",
			Value:     input.AccountId,
			Condition: "and",
		})
		filters = append(filters, monitor.MetricQueryTag{
			Key:   "provider",
			Value: input.Provider,
		})
	}

	log.Debugf("meteralert alertType: %s", alertType)

	if input.ProjectId != "" {
		filters = append(filters, monitor.MetricQueryTag{
			Key:   "projectId",
			Value: input.ProjectId,
		})
	}

	groupBy = append(groupBy, monitor.MetricQueryPart{
		Type:   "field",
		Params: []string{groupByStr},
	})

	sels := make([]monitor.MetricQuerySelect, 0)
	sels = append(sels, monitor.NewMetricQuerySelect(
		monitor.MetricQueryPart{
			Type:   "field",
			Params: []string{input.Type},
		}))
	q := monitor.MetricQuery{
		Selects:     sels,
		Tags:        filters,
		GroupBy:     groupBy,
		Measurement: measurement,
		Database:    db,
	}
	return q, reducer, evaluator
}

func (man *SMeterAlertManager) GetAlert(id string) (*SMeterAlert, error) {
	obj, err := man.FetchById(id)
	if err != nil {
		return nil, err
	}
	return obj.(*SMeterAlert), nil
}

func (man *SMeterAlertManager) ListItemFilter(
	ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	query monitor.MeterAlertListInput,
) (*sqlchemy.SQuery, error) {
	q, err := man.SV1AlertManager.ListItemFilter(ctx, q, userCred, query.V1AlertListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SV1AlertManager.ListItemFilter")
	}
	q.Equals("used_by", AlertNotificationUsedByMeterAlert)
	return q, nil
}

func (man *SMeterAlertManager) OrderByExtraFields(
	ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	query monitor.MeterAlertListInput,
) (*sqlchemy.SQuery, error) {
	var err error

	q, err = man.SV1AlertManager.OrderByExtraFields(ctx, q, userCred, query.V1AlertListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SV1AlertManager.OrderByExtraFields")
	}

	return q, nil
}

func (man *SMeterAlertManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field string) (*sqlchemy.SQuery, error) {
	var err error
	q, err = man.SV1AlertManager.QueryDistinctExtraField(q, field)
	if err == nil {
		return q, nil
	}
	return q, httperrors.ErrNotFound
}

func (man *SMeterAlertManager) CustomizeFilterList(
	ctx context.Context, q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential, query jsonutils.JSONObject) (
	*db.CustomizeListFilters, error) {
	filters, err := man.SV1AlertManager.CustomizeFilterList(ctx, q, userCred, query)
	if err != nil {
		return nil, err
	}
	input := new(monitor.MeterAlertListInput)
	if err := query.Unmarshal(input); err != nil {
		return nil, err
	}
	wrapF := func(f func(obj *SMeterAlert) (bool, error)) func(object jsonutils.JSONObject) (bool, error) {
		return func(data jsonutils.JSONObject) (bool, error) {
			id, err := data.GetString("id")
			if err != nil {
				return false, err
			}
			obj, err := man.GetAlert(id)
			if err != nil {
				return false, err
			}
			return f(obj)
		}
	}

	if input.Type != "" {
		filters.Append(wrapF(func(obj *SMeterAlert) (bool, error) {
			return obj.getType() == input.Type, nil
		}))
	}

	if input.AccountId != "" {
		filters.Append(wrapF(func(obj *SMeterAlert) (bool, error) {
			return obj.getAccountId() == input.AccountId, nil
		}))
	}

	if input.Provider != "" {
		filters.Append(wrapF(func(obj *SMeterAlert) (bool, error) {
			return obj.getProvider() == input.Provider, nil
		}))
	}

	if input.ProjectId != "" {
		filters.Append(wrapF(func(obj *SMeterAlert) (bool, error) {
			return obj.getProjectId() == input.ProjectId, nil
		}))
	}

	return filters, nil
}

func (alert *SMeterAlert) CustomizeCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) error {
	if err := alert.SAlert.CustomizeCreate(ctx, userCred, ownerId, query, data); err != nil {
		return err
	}
	input := new(monitor.MeterAlertCreateInput)
	if err := data.Unmarshal(input); err != nil {
		return err
	}
	return alert.SV1Alert.CustomizeCreate(ctx, userCred, input.Type, input.Channel, input.Recipients, AlertNotificationUsedByMeterAlert)
}

func (alert *SMeterAlert) setType(ctx context.Context, userCred mcclient.TokenCredential, t string) error {
	return alert.SetMetadata(ctx, MeterAlertMetadataType, t, userCred)
}

func (alert *SMeterAlert) getType() string {
	return alert.GetMetadata(context.Background(), MeterAlertMetadataType, nil)
}

func (alert *SMeterAlert) setProjectId(ctx context.Context, userCred mcclient.TokenCredential, id string) error {
	return alert.SetMetadata(ctx, MeterAlertMetadataProjectId, id, userCred)
}

func (alert *SMeterAlert) getProjectId() string {
	return alert.GetMetadata(context.Background(), MeterAlertMetadataProjectId, nil)
}

func (alert *SMeterAlert) setAccountId(ctx context.Context, userCred mcclient.TokenCredential, id string) error {
	return alert.SetMetadata(ctx, MeterAlertMetadataAccountId, id, userCred)
}

func (alert *SMeterAlert) getAccountId() string {
	return alert.GetMetadata(context.Background(), MeterAlertMetadataAccountId, nil)
}

func (alert *SMeterAlert) setProvider(ctx context.Context, userCred mcclient.TokenCredential, p string) error {
	return alert.SetMetadata(ctx, MeterAlertMetadataProvider, p, userCred)
}

func (alert *SMeterAlert) getProvider() string {
	return alert.GetMetadata(context.Background(), MeterAlertMetadataProvider, nil)
}

func (alert *SMeterAlert) PostCreate(ctx context.Context,
	userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider,
	query jsonutils.JSONObject, data jsonutils.JSONObject) {
	alert.SStatusStandaloneResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
	input := new(monitor.MeterAlertCreateInput)
	if err := data.Unmarshal(input); err != nil {
		log.Errorf("post create unmarshal input: %v", err)
		return
	}
	if input.Type != "" {
		if err := alert.setType(ctx, userCred, input.Type); err != nil {
			log.Errorf("set type: %v", err)
		}
	}
	if input.Provider != "" {
		if err := alert.setProvider(ctx, userCred, input.Provider); err != nil {
			log.Errorf("set proider: %v", err)
		}
	}
	if input.AccountId != "" {
		if err := alert.setAccountId(ctx, userCred, input.AccountId); err != nil {
			log.Errorf("set account_id: %v", err)
		}
	}
	if input.ProjectId != "" {
		if err := alert.setProjectId(ctx, userCred, input.ProjectId); err != nil {
			log.Errorf("set project_id: %v", err)
		}
	}
	forTime := MeterAlertManager.GetDriver(alert.getType()).GetFor()
	if err := alert.SetFor(forTime); err != nil {
		log.Errorf("set for error: %v", err)
	}
}

func (man *SMeterAlertManager) FetchCustomizeColumns(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	query jsonutils.JSONObject,
	objs []interface{},
	fields stringutils2.SSortedStrings,
	isList bool,
) []monitor.MeterAlertDetails {
	rows := make([]monitor.MeterAlertDetails, len(objs))

	v1Rows := man.SV1AlertManager.FetchCustomizeColumns(ctx, userCred, query, objs, fields, isList)

	for i := range rows {
		rows[i] = monitor.MeterAlertDetails{
			AlertV1Details: v1Rows[i],
		}
		rows[i], _ = objs[i].(*SMeterAlert).getMoreDetails(rows[i])
	}

	return rows
}

func (alert *SMeterAlert) getExtraDetails(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	query jsonutils.JSONObject,
	isList bool,
) (monitor.MeterAlertDetails, error) {
	ret := MeterAlertManager.FetchCustomizeColumns(ctx, userCred, query, []interface{}{alert}, nil, isList)
	if len(ret) == 0 {
		return monitor.MeterAlertDetails{}, errors.Error("empty meter alert details")
	}
	return ret[0], nil
}

func (alert *SMeterAlert) getMoreDetails(out monitor.MeterAlertDetails) (monitor.MeterAlertDetails, error) {
	var err error
	out.AlertV1Details, err = alert.SV1Alert.getMoreDetails(out.AlertV1Details, AlertNotificationUsedByMeterAlert)
	if err != nil {
		return out, errors.Wrap(err, "SV1Alert.getMoreDetails")
	}

	out.Type = alert.getType()
	out.ProjectId = alert.getProjectId()
	out.Provider = alert.getProvider()
	out.AccountId = alert.getAccountId()

	return out, nil
}

func (alert *SMeterAlert) ValidateUpdateData(
	ctx context.Context,
	userCred mcclient.TokenCredential,
	query jsonutils.JSONObject,
	input monitor.MeterAlertUpdateInput,
) (monitor.MeterAlertUpdateInput, error) {
	// ret := new(monitor.AlertUpdateInput)
	details, err := alert.getExtraDetails(ctx, userCred, query, false)
	if err != nil {
		return input, err
	}
	if input.Threshold != nil && *input.Threshold != details.Threshold {
		details.Threshold = *input.Threshold
	}

	if input.Comparator != nil && *input.Comparator != details.Comparator {
		details.Comparator = *input.Comparator
	}

	// hack: update notification here
	if err := alert.UpdateNotification(AlertNotificationUsedByMeterAlert, input.Channel, input.Recipients); err != nil {
		return input, errors.Wrap(err, "update notification")
	}
	allAccountIds := []string{}
	if details.AccountId == "" {
		allAccountIds, err = MeterAlertManager.getAllBillAccountIds(ctx)
		if err != nil {
			return input, err
		}
	}
	tmpS := alert.getUpdateSetting(details, allAccountIds)
	input.Settings = &tmpS

	input.V1AlertUpdateInput, err = alert.SV1Alert.ValidateUpdateData(ctx, userCred, query, input.V1AlertUpdateInput)
	if err != nil {
		return input, errors.Wrap(err, "SV1Alert.ValidateUpdateData")
	}

	return input, nil
}

func (alert *SMeterAlert) getUpdateSetting(details monitor.MeterAlertDetails, accountIds []string) monitor.AlertSetting {
	drv := MeterAlertManager.GetDriver(alert.getType())
	input := monitor.MeterAlertCreateInput{
		ResourceAlertV1CreateInput: monitor.ResourceAlertV1CreateInput{
			Period:     details.Period,
			Window:     details.Window,
			Comparator: details.Comparator,
			Threshold:  details.Threshold,
			Channel:    details.Channel,
			Recipients: details.Recipients,
		},
		Type:      details.Type,
		Provider:  details.Provider,
		ProjectId: details.ProjectId,
		AccountId: details.AccountId,
	}
	input.Level = details.Level
	out := drv.ToAlertCreateInput(input, accountIds, details.Level)
	return out.Settings
}

func (alert *SMeterAlert) PostUpdate(
	ctx context.Context, userCred mcclient.TokenCredential,
	query jsonutils.JSONObject, data jsonutils.JSONObject) {
	alert.SV1Alert.PostUpdate(ctx, userCred, query, data)
}

func (alert *SMeterAlert) CustomizeDelete(
	ctx context.Context, userCred mcclient.TokenCredential,
	query jsonutils.JSONObject, data jsonutils.JSONObject) error {
	return alert.SV1Alert.CustomizeDelete(ctx, userCred, query, data)
}
