package billing

import (
	"fmt"
	"time"

	"github.com/pingcap/dbaas/billing/api/model"
	"github.com/pingcap/dbaas/billing/util"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/central-sdk/db"
	"github.com/tidbcloud/central-sdk/db/entity"
	"github.com/tidbcloud/central-sdk/db/tenant"
	centralModel "github.com/tidbcloud/central-sdk/model"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type Model struct {
	log               *zap.Logger
	client            Service
	permissionChecker PermissionChecker
}

type Credit struct {
	ID      uint64
	Amount  int64
	Created int64
}

type ProjectUsage struct {
	ID    uint64
	Name  string
	Usage int64
}

type PermissionChecker interface {
	HasPermission(*tenant.User) bool
}

type AllowAllPermissionCheck struct{}

func (a *AllowAllPermissionCheck) HasPermission(*tenant.User) bool {
	return true
}

func NewModel(log *zap.Logger, billingClient Service) Model {
	return Model{log: log, client: billingClient, permissionChecker: &AllowAllPermissionCheck{}}
}

type Context struct {
	Model
	tenantAndUser *tenant.User
	log           *zap.Logger
}

// User returns a UserContext
func (m Model) User(user *tenant.User) Context {
	return Context{m, user, m.log.With(zap.Uint64("tenantID", uint64(user.TenantID)), zap.Uint64("userID", uint64(user.ID)), zap.String("module", "billing"))}
}

func (c Context) Log() *zap.Logger {
	return c.log
}

func (c Context) CheckOrgID(orgID entity.ID) error {
	if c.tenantAndUser.IsImpersonated {
		return nil
	}
	if orgID != c.tenantAndUser.TenantID {
		return errcode.NewForbiddenErr(errors.New("org_id parameter is inconsistent with the user's org ID"))
	}
	return nil
}

func (c Context) CheckIfOwnerOrBillingAdmin() error {
	user := c.tenantAndUser
	if user.Role == entity.UserRoleOwner || user.Role == entity.UserRoleBillingAdmin {
		return nil
	}
	return errcode.NewForbiddenErr(errors.New("only owner or billing admin can perform this operation"))
}

func (c Context) GetCurrentBillingCycle() (*model.CurrentBillingCycle, error) {
	id, err := c.checkAndGetTenantIDStr()
	if err != nil {
		return nil, err
	}
	var t entity.Tenant
	err = db.Transaction(func(tx *gorm.DB) error {
		return entity.NewTenantQuerySet(tx).IDEq(c.tenantAndUser.TenantID).One(&t)
	})
	if err != nil {
		return nil, err
	}
	var startTime int64
	if t.BindCreditCardAt != nil {
		//on on_demand return the node usage fee of this month
		startTime = t.BindCreditCardAt.Unix()
		if startTime < util.BeginningOfMonth(time.UTC).Unix() {
			startTime = util.BeginningOfMonth(time.UTC).Unix()
		}
	}
	ret, err := c.client.GetCurrentBillingCycle(id, startTime, time.Now().Unix())
	if err != nil {
		c.Log().Error("failed to get current billing cycle", zap.Error(err))
		return nil, errcode.NewInternalErr(errors.New("failed to get current billing cycle"))
	}
	if t.Plan != entity.TenantPlanOnDemand {
		// non on_demand tenant return all fee since created
		err = db.Transaction(func(tx *gorm.DB) error {
			ret.CurrentUsage, err = centralModel.GetNodeUsageFromDB(tx, t.ID)
			return err
		})
		if err != nil {
			return nil, errcode.NewInternalErr(errors.Annotate(err, "failed to get node usage from DB"))
		}
	}
	return ret, nil
}

func (c Context) GetCustomerBillingInfo() (*model.Customer, string, error) {
	id, err := c.checkAndGetTenantIDStr()
	if err != nil {
		return nil, "", err
	}
	ret, err := c.client.GetCustomer(id)
	if err != nil {
		c.Log().Error("failed to get customer billing info", zap.Error(err))
		return nil, "", errcode.NewInternalErr(errors.New("failed to get customer billing info"))
	}
	var t entity.Tenant
	err = db.Transaction(func(tx *gorm.DB) error {
		return entity.NewTenantQuerySet(tx).IDEq(c.tenantAndUser.TenantID).One(&t)
	})
	if err != nil {
		c.Log().Error("failed to load tenant from DB", zap.Error(err))
		return nil, "", errcode.NewInternalErr(errors.New("failed to get customer billing info"))
	}
	return ret, t.BillingEmail, nil
}

func (c Context) SetupIntent() (*model.SetupIntent, error) {
	id, err := c.checkAndGetTenantIDStr()
	if err != nil {
		return nil, err
	}
	ret, err := c.client.SetupIntent(id)
	if err != nil {
		c.Log().Error("failed to setup intent", zap.Error(err))
		return nil, errcode.NewInternalErr(errors.New("failed to setup intent"))
	}
	return ret, nil
}

func (c Context) UpdateCustomerBillingInfo(billingInfo *model.Customer, billingEmail string) (*model.Customer, string, error) {
	id, err := c.checkAndGetTenantIDStr()
	if err != nil {
		return nil, "", err
	}
	var t entity.Tenant
	var ret *model.Customer
	err = db.Transaction(func(tx *gorm.DB) error {
		ret, err = c.client.UpdateCustomer(id, billingInfo)
		if err != nil {
			c.Log().Error("failed to update customer billing info", zap.Error(err))
			return err
		}
		err := entity.NewTenantQuerySet(tx).IDEq(c.tenantAndUser.TenantID).One(&t)
		if err != nil {
			return err
		}
		if billingEmail != "" {
			t.BillingEmail = billingEmail
			return t.Update(tx, entity.TenantDBSchema.BillingEmail)
		}
		return nil
	})
	if err != nil {
		c.Log().Error("failed to load tenant from DB", zap.Error(err))
		return nil, "", errcode.NewInternalErr(errors.New("failed to get customer billing info"))
	}
	return ret, t.BillingEmail, nil
}

func (c Context) UpdatePaymentMethods(pmId string, payload *model.PaymentMethod) (*model.PaymentMethod, error) {
	id, err := c.checkAndGetTenantIDStr()
	if err != nil {
		return nil, err
	}
	ret, err := c.client.UpdatePaymentMethod(id, pmId, payload)
	if err != nil {
		c.Log().Error("failed to update payment method", zap.Error(err))
		return nil, errcode.NewInternalErr(errors.New("failed to update payment method"))
	}
	return ret, nil
}

func (c Context) DeletePaymentMethod(pmId string) error {
	id, err := c.checkAndGetTenantIDStr()
	if err != nil {
		return err
	}
	_, err = c.client.DeletePaymentMethod(id, pmId)
	if err != nil {
		c.Log().Error("failed to delete payment method", zap.Error(err))
		return errcode.NewInternalErr(errors.New("failed to delete payment method"))
	}
	return nil
}

func (c Context) ListPaymentMethods(pagination Pagination) (*model.ListPaymentMethodResponse, error) {
	id, err := c.checkAndGetTenantIDStr()
	if err != nil {
		return nil, err
	}
	ret, err := c.client.ListPaymentMethods(id, pagination)
	if err != nil {
		c.Log().Error("failed to list payment methods", zap.Error(err))
		return nil, errcode.NewInternalErr(errors.New("failed to list payment methods"))
	}
	return ret, nil
}

func (c Context) GetMonthUsageGroupByProject(year, month int) ([]ProjectUsage, error) {
	id, err := c.checkAndGetTenantIDStr()
	if err != nil {
		return nil, err
	}
	ret, err := c.client.GetMonthUsageGroupByProject(id, year, month)
	if err != nil {
		c.Log().Error("failed to get month usage group by project", zap.Error(err))
		return nil, errcode.NewInternalErr(errors.New("failed to get month usage group by project"))
	}
	if len(ret.Projects) == 0 {
		return nil, nil
	}
	var projects []entity.Project
	if err := db.Transaction(func(tx *gorm.DB) error {
		return entity.NewProjectQuerySet(tx).TenantIDEq(c.tenantAndUser.TenantID).All(&projects)
	}); err != nil {
		return nil, errcode.NewInternalErr(errors.New("failed to load projects"))
	}
	projectsNameMap := make(map[uint64]string)
	for _, item := range projects {
		projectsNameMap[item.ID.Uint64()] = item.Name
	}
	var projectMonthUsage []ProjectUsage
	for _, item := range ret.Projects {
		name, found := projectsNameMap[item.ProjectID]
		if !found {
			name = fmt.Sprintf("id: %d", item.ProjectID)
		}
		projectMonthUsage = append(projectMonthUsage, ProjectUsage{
			ID:    item.ProjectID,
			Name:  name,
			Usage: int64(item.Fee * 100),
		})
	}
	return projectMonthUsage, nil
}

func (c Context) ListInvoices(pagination Pagination) (*model.ListInvoiceResponse, error) {
	id, err := c.checkAndGetTenantIDStr()
	if err != nil {
		return nil, err
	}
	ret, err := c.client.ListInvoices(id, pagination)
	if err != nil {
		c.Log().Error("failed to list invoices", zap.Error(err))
		return nil, errcode.NewInternalErr(errors.New("failed to list invoices"))
	}
	return ret, nil
}

func (c Context) ListCredits() ([]Credit, error) {
	if !c.checkPermission() {
		return nil, errcode.NewForbiddenErr(fmt.Errorf("permission denined"))
	}

	var fs []entity.FreeTrialPoint
	err := db.Transaction(func(tx *gorm.DB) error {
		return entity.NewFreeTrialPointQuerySet(tx).TenantIDEq(c.tenantAndUser.TenantID).OrderAscByCreatedAt().All(&fs)
	})
	if err != nil {
		return nil, errcode.NewInternalErr(err)
	}
	var credits = make([]Credit, len(fs))
	for idx, item := range fs {
		credits[idx] = Credit{
			ID:      item.ID.Uint64(),
			Amount:  item.Amount,
			Created: item.Base.CreatedAt.Unix(),
		}
	}
	return credits, nil
}

func (c Context) checkAndGetTenantIDStr() (string, error) {
	if !c.checkPermission() {
		return "", errcode.NewForbiddenErr(fmt.Errorf("permission denined"))
	}
	return fmt.Sprintf("%d", c.tenantAndUser.TenantID), nil
}

func (c Context) checkPermission() bool {
	return c.permissionChecker.HasPermission(c.tenantAndUser)
}
