package models

import (
	"context"
	"fmt"
	"reflect"
	"time"

	"yunion.io/x/jsonutils"
	"yunion.io/x/log"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/billing"
	"yunion.io/x/sqlchemy"

	billing_api "yunion.io/x/onecloud/pkg/apis/billing"
	api "yunion.io/x/onecloud/pkg/apis/compute"
	notifyapi "yunion.io/x/onecloud/pkg/apis/notify"
	"yunion.io/x/onecloud/pkg/cloudcommon/db"
	"yunion.io/x/onecloud/pkg/cloudcommon/notifyclient"
	"yunion.io/x/onecloud/pkg/compute/options"
	"yunion.io/x/onecloud/pkg/mcclient"
	"yunion.io/x/onecloud/pkg/mcclient/auth"
	"yunion.io/x/onecloud/pkg/mcclient/modules/notify"
	"yunion.io/x/onecloud/pkg/util/logclient"
)

type SBillingResourceCheckManager struct {
	db.SVirtualResourceBaseManager
}

var BillingResourceCheckManager *SBillingResourceCheckManager

func init() {
	BillingResourceCheckManager = &SBillingResourceCheckManager{
		SVirtualResourceBaseManager: db.NewVirtualResourceBaseManager(
			SBillingResourceCheck{},
			"billing_resource_checks_tbl",
			"billing_resource_check",
			"billing_resource_checks",
		),
	}
	BillingResourceCheckManager.SetVirtualObject(BillingResourceCheckManager)
}

type SBillingResourceCheck struct {
	db.SVirtualResourceBase
	SBillingResourceBase

	ResourceType string `width:"36" charset:"ascii" list:"user"`
}

type IBillingModelManager interface {
	db.IModelManager
	GetExpiredModels(advanceDay int) ([]IBillingModel, error)
}

type IBillingModel interface {
	db.IModel
	GetOwnerId() mcclient.IIdentityProvider
	GetStatus() string
	GetExpiredAt() time.Time
	GetReleaseAt() time.Time
	GetAutoRenew() bool
	GetCreatedAt() time.Time
	SetReleaseAt(releaseAt time.Time)
	SetExpiredAt(expireAt time.Time)
	SetBillingCycle(billingCycle string)
	SetBillingType(billingType billing_api.TBillingType)
	GetBillingType() billing_api.TBillingType
}

// 即将到期释放资源列表
func (manager *SBillingResourceCheckManager) ListItemFilter(
	ctx context.Context,
	q *sqlchemy.SQuery,
	userCred mcclient.TokenCredential,
	query api.BillingResourceCheckListInput,
) (*sqlchemy.SQuery, error) {
	var err error
	q, err = manager.SVirtualResourceBaseManager.ListItemFilter(ctx, q, userCred, query.VirtualResourceListInput)
	if err != nil {
		return nil, errors.Wrap(err, "SVirtualResourceBaseManager.ListItemFilter")
	}
	if len(query.ResourceType) > 0 {
		q = q.In("resource_type", query.ResourceType)
	}
	return q, nil
}

func SaveReleaseAt(ctx context.Context, model IBillingModel, userCred mcclient.TokenCredential, releaseAt time.Time) error {
	diff, err := db.Update(model, func() error {
		model.SetReleaseAt(releaseAt)
		return nil
	})
	if err != nil {
		return errors.Wrap(err, "Update")
	}
	if len(diff) > 0 {
		db.OpsLog.LogEvent(model, db.ACT_SET_RELEASE_TIME, fmt.Sprintf("release at: %s", releaseAt), userCred)
	}
	if len(diff) > 0 && userCred != nil {
		logclient.AddActionLogWithContext(ctx, model, logclient.ACT_SET_RELEASE_TIME, diff, userCred, true)
	}
	return nil
}

func SaveRenewInfo(
	ctx context.Context, userCred mcclient.TokenCredential,
	model IBillingModel, bc *billing.SBillingCycle, expireAt *time.Time, billingType billing_api.TBillingType,
) error {
	_, err := db.Update(model, func() error {
		if len(billingType) == 0 {
			billingType = billing_api.BILLING_TYPE_PREPAID
		}
		if model.GetBillingType() == "" {
			model.SetBillingType(billingType)
		}
		if expireAt != nil && !expireAt.IsZero() {
			model.SetExpiredAt(*expireAt)
		} else if bc != nil {
			model.SetBillingCycle(bc.String())
			model.SetExpiredAt(bc.EndAt(model.GetExpiredAt()))
		}
		return nil
	})
	if err != nil {
		log.Errorf("UpdateItem error %s", err)
		return err
	}
	db.OpsLog.LogEvent(model, db.ACT_RENEW, model.GetShortDesc(ctx), userCred)
	return nil
}

func (manager *SBillingResourceCheckManager) GetExpiredModels(advanceDay int) ([]SBillingResourceCheck, error) {
	upLimit := time.Now().AddDate(0, 0, advanceDay+1)
	downLimit := time.Now().AddDate(0, 0, advanceDay)
	q := manager.Query()
	q = q.Filter(
		sqlchemy.OR(
			sqlchemy.AND(
				sqlchemy.Equals(q.Field("billing_type"), billing_api.BILLING_TYPE_POSTPAID),
				sqlchemy.LE(q.Field("release_at"), upLimit),
				sqlchemy.GE(q.Field("release_at"), downLimit),
			),
			// 跳过自动续费实例
			sqlchemy.AND(
				sqlchemy.Equals(q.Field("billing_type"), billing_api.BILLING_TYPE_PREPAID),
				sqlchemy.LE(q.Field("expired_at"), upLimit),
				sqlchemy.GE(q.Field("expired_at"), downLimit),
				sqlchemy.IsFalse(q.Field("auto_renew")),
			)))

	ret := []SBillingResourceCheck{}
	err := db.FetchModelObjects(manager, q, &ret)
	if err != nil {
		return nil, errors.Wrap(err, "FetchModelObjects")
	}
	return ret, nil
}

func fetchExpiredModels(manager db.IModelManager, advanceDay int) ([]IBillingModel, error) {
	upLimit := time.Now().AddDate(0, 0, advanceDay+1)
	downLimit := time.Now()
	v := reflect.MakeSlice(reflect.SliceOf(manager.TableSpec().DataType()), 0, 0)
	q := manager.Query()
	q = q.Filter(
		sqlchemy.OR(
			sqlchemy.AND(
				sqlchemy.Equals(q.Field("billing_type"), billing_api.BILLING_TYPE_POSTPAID),
				sqlchemy.LE(q.Field("release_at"), upLimit),
				sqlchemy.GE(q.Field("release_at"), downLimit),
			),
			sqlchemy.AND(
				sqlchemy.Equals(q.Field("billing_type"), billing_api.BILLING_TYPE_PREPAID),
				sqlchemy.LE(q.Field("expired_at"), upLimit),
				sqlchemy.GE(q.Field("expired_at"), downLimit),
			)))

	vp := reflect.New(v.Type())
	vp.Elem().Set(v)
	err := db.FetchModelObjects(manager, q, vp.Interface())
	if err != nil {
		return nil, errors.Wrapf(err, "unable to list %s", manager.KeywordPlural())
	}

	v = vp.Elem()
	if v.Len() > 0 {
		log.Debugf("%s length of v: %d will be notified", manager.Alias(), v.Len())
	}

	ms := make([]IBillingModel, v.Len())
	for i := range ms {
		ms[i] = v.Index(i).Addr().Interface().(IBillingModel)
	}
	return ms, nil
}

func (bm *SBillingResourceCheckManager) Create(ctx context.Context, res IBillingModel, resourceType string) error {
	bc := &SBillingResourceCheck{
		ResourceType: resourceType,
	}
	bc.Id = res.GetId()
	bc.Name = res.GetName()
	bc.ExpiredAt = res.GetExpiredAt()
	bc.ReleaseAt = res.GetReleaseAt()
	bc.AutoRenew = res.GetAutoRenew()
	bc.BillingType = res.GetBillingType()
	if owner := res.GetOwnerId(); owner != nil {
		bc.ProjectId = owner.GetProjectId()
		bc.DomainId = owner.GetDomainId()
	}
	bc.CreatedAt = res.GetCreatedAt()
	bc.Status = res.GetStatus()
	bc.SetModelManager(bm, bc)
	return bm.TableSpec().InsertOrUpdate(ctx, bc)
}

func (man *SBillingResourceCheckManager) PerformCheck(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, input jsonutils.JSONObject) (jsonutils.JSONObject, error) {
	scanExpiredBillingResources(ctx)
	return jsonutils.NewDict(), nil
}

func (man *SBillingResourceCheckManager) clean(resType string, resourceIds []string) error {
	ids, err := db.FetchField(man, "id", func(q *sqlchemy.SQuery) *sqlchemy.SQuery {
		return q.Equals("resource_type", resType).NotIn("id", resourceIds)
	})
	if err != nil {
		return errors.Wrap(err, "fetchField")
	}
	if len(ids) == 0 {
		return nil
	}
	return db.Purge(man, "id", ids, true)
}

func scanExpiredBillingResources(ctx context.Context) {
	billingResourceManagers := []IBillingModelManager{
		GuestManager,
		DBInstanceManager,
		ElasticcacheManager,
	}

	for _, manager := range billingResourceManagers {
		expiredModels, err := manager.GetExpiredModels(30)
		if err != nil {
			log.Errorf("unable to fetchExpiredModels: %v", err)
			continue
		}

		resourceIds := []string{}
		for _, model := range expiredModels {
			err = BillingResourceCheckManager.Create(ctx, model, manager.Keyword())
			if err != nil {
				log.Errorf("unable to create billing_resource_check for resource %s %s", manager.Keyword(), model.GetId())
				continue
			}
			resourceIds = append(resourceIds, model.GetId())
		}

		err = BillingResourceCheckManager.clean(manager.Keyword(), resourceIds)
		if err != nil {
			log.Errorf("unable to clean billing_resource_check for resource %s", manager.Keyword())
			continue
		}
	}
}

func CheckBillingResourceExpireAt(ctx context.Context, userCred mcclient.TokenCredential, isStart bool) {
	scanExpiredBillingResources(ctx)

	s := auth.GetAdminSession(ctx, options.Options.Region)
	resp, err := notify.NotifyTopic.List(s, jsonutils.Marshal(map[string]interface{}{
		"filter": fmt.Sprintf("name.equals('%s')", notifyapi.DefaultResourceRelease),
		"scope":  "system",
	}))
	if err != nil {
		log.Errorln(errors.Wrap(err, "list topics"))
		return
	}
	topics := []notifyapi.TopicDetails{}
	err = jsonutils.Update(&topics, resp.Data)
	if err != nil {
		log.Errorln(errors.Wrap(err, "update topic"))
		return
	}
	if len(topics) != 1 {
		log.Errorln(errors.Wrapf(errors.ErrNotSupported, "len topics :%d", len(topics)))
		return
	}

	for _, advanceDay := range topics[0].AdvanceDays {
		resources, err := BillingResourceCheckManager.GetExpiredModels(advanceDay)
		if err != nil {
			log.Errorf("unable to fetchExpiredModels: %s", err.Error())
			continue
		}

		for i := range resources {
			detailsDecro := func(ctx context.Context, details *jsonutils.JSONDict) {
				details.Set("advance_days", jsonutils.NewInt(int64(advanceDay)))
			}
			notifyclient.EventNotify(ctx, userCred, notifyclient.SEventNotifyParam{
				Obj:                 &resources[i],
				ObjDetailsDecorator: detailsDecro,
				Action:              notifyclient.ActionExpiredRelease,
				AdvanceDays:         advanceDay,
			})
		}
	}
}
