package payment

import (
	"errors"
	"fmt"
	"time"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/model/billing"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/errorutils"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"github.com/shopspring/decimal"
)

type InvoiceModel struct {
}

func invoiceFromEntity(invoiceEntity *entity.Invoice) *forms.Invoice {
	result := &forms.Invoice{
		Costs: forms.Costs{
			CategoryCosts: forms.CategoryCosts{
				Price: forms.Price{
					CostPrice: invoiceEntity.CostPrice,
					RawPrice:  invoiceEntity.RawPrice,
				},
			},
		},
		InvoiceID: invoiceEntity.ID,
		TenantID:  invoiceEntity.TenantID,
		Status:    invoiceEntity.Status,
		StartDate: invoiceEntity.InvoiceDate,
		Type:      invoiceEntity.Type,
	}
	switch invoiceEntity.Type {
	case entity.InvoiceType_MONTHLY:
		result.EndDate = result.StartDate.AddDate(0, 1, 0)
	case entity.InvoiceType_HOURLY:
		result.EndDate = result.StartDate.Add(time.Hour)
	default:
		result.EndDate = result.StartDate
	}

	return result
}

func invoiceFromEntityWithCosts(invoiceEntity *entity.Invoice) (*forms.Invoice, error) {
	invoice := invoiceFromEntity(invoiceEntity)
	costModel := CostExplorerModel{}
	costs, err := costModel.GetInvoiceCosts(invoiceEntity.TenantID, invoiceEntity.ID)
	if err != nil {
		return nil, err
	}

	if invoiceEntity.Type != entity.InvoiceType_COMMITMENT {
		invoice.Costs = *costs
	}

	return invoice, nil
}

func (*InvoiceModel) ListAllInvoices(tenantID uint) ([]forms.Invoice, error) {
	invoices := []entity.Invoice{}
	err := entity.NewInvoiceQuerySet(db.GetDB()).TenantIDEq(tenantID).OrderDescByInvoiceDate().All(&invoices)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	infos := make([]forms.Invoice, len(invoices))
	for i, invoice := range invoices {
		invoiceInfo, err := invoiceFromEntityWithCosts(&invoice)
		if err != nil {
			return nil, err
		}
		infos[i] = *invoiceInfo
	}
	return infos, nil
}

func (*InvoiceModel) ListAllInvoicesWithType(tenantID uint, invoiceType string) ([]forms.Invoice, error) {
	invoices := []entity.Invoice{}
	err := entity.NewInvoiceQuerySet(db.GetDB()).TenantIDEq(tenantID).TypeEq(invoiceType).OrderDescByInvoiceDate().All(&invoices)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	infos := make([]forms.Invoice, len(invoices))
	for i, invoice := range invoices {
		invoiceInfo, err := invoiceFromEntityWithCosts(&invoice)
		if err != nil {
			return nil, err
		}
		infos[i] = *invoiceInfo
	}
	return infos, nil
}

func (*InvoiceModel) GetInvoice(invoiceID uint) (*forms.Invoice, error) {
	invoice := &entity.Invoice{}
	err := errorutils.WrapSQLError(entity.NewInvoiceQuerySet(db.GetDB()).IDEq(invoiceID).One(invoice), fmt.Sprintf("invoice %d", invoiceID))

	if err != nil {
		return nil, err
	}
	info, err := invoiceFromEntityWithCosts(invoice)
	if err != nil {
		return nil, err
	}
	return info, nil
}

func (s *InvoiceModel) ConfirmInvoice(invoiceID uint) (*forms.Invoice, error) {
	info, err := s.GetInvoice(invoiceID)
	if err != nil {
		return nil, err
	}
	if info.Status == entity.InvoiceStatus_SENT || info.Status == entity.InvoiceStatus_PAID || info.Status == entity.InvoiceStatus_CONFIRMED {
		// already confirmed, do nothing
		return info, nil
	}
	if info.Status != entity.InvoiceStatus_GENERATED || info.Type != entity.InvoiceType_MONTHLY {
		return nil, billing_errors.NewInvalidInputError(errors.New("only monthly invoice need confirm"))
	}
	err = entity.NewInvoiceQuerySet(db.GetDB()).IDEq(invoiceID).GetUpdater().SetStatus(entity.InvoiceStatus_CONFIRMED).Update()
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	info.Status = entity.InvoiceStatus_CONFIRMED
	return info, nil
}

func (s *InvoiceModel) SendInvoice(invoiceID uint) (*forms.Invoice, error) {
	info, err := s.GetInvoice(invoiceID)
	if err != nil {
		return nil, err
	}
	if info.Status == entity.InvoiceStatus_SENT || info.Status == entity.InvoiceStatus_PAID {
		// already sent, do nothing
		return info, nil
	}

	if !(info.Status == entity.InvoiceStatus_GENERATED || info.Status == entity.InvoiceStatus_CONFIRMED) {
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("invoice status error:%s", info.Status))
	}

	if info.Status == entity.InvoiceStatus_GENERATED && info.Type == entity.InvoiceType_MONTHLY {
		return nil, billing_errors.NewInvoiceNotConfirmedError(nil)
	}

	paymentModel := &paymentsub.PaymentModel{}
	err = paymentModel.Pay(info.TenantID, info)
	if err != nil {
		return nil, err
	}

	err = entity.NewInvoiceQuerySet(db.GetDB()).IDEq(invoiceID).GetUpdater().SetStatus(entity.InvoiceStatus_SENT).Update()
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	info.Status = entity.InvoiceStatus_SENT
	return info, nil
}

func (s *InvoiceModel) CreateMonthlyInvoice(tenantID uint, date time.Time) (*forms.Invoice, error) {
	start := timeutils.GetMonthDate(date)
	end := start.AddDate(0, 1, 0)

	invoice, err := s.GetMonthlyInvoice(tenantID, date)
	if err == nil {
		return invoice, nil
	}

	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			return nil, err
		}
	}

	return s.createInvoice(tenantID, start, end, entity.InvoiceType_MONTHLY, entity.InvoiceStatus_GENERATED)
}

// only for marketplace
func (s *InvoiceModel) CreateAutoConfirmedMontlyInvoice(tenantID uint, date time.Time) (*forms.Invoice, error) {
	start := timeutils.GetMonthDate(date)
	end := start.AddDate(0, 1, 0)

	invoice, err := s.GetMonthlyInvoice(tenantID, date)
	if err == nil {
		return invoice, nil
	}

	if err != nil {
		if !errors.Is(err, billing_errors.ObjectNotExistsError) {
			return nil, err
		}
	}

	return s.createInvoice(tenantID, start, end, entity.InvoiceType_MONTHLY, entity.InvoiceStatus_CONFIRMED)
}

// only for marketplace
func (s *InvoiceModel) CreateAutoConfirmedHourlyInvoice(tenantID uint, date time.Time) (*forms.Invoice, error) {
	start := timeutils.GetHourDate(date)
	end := start.Add(time.Hour)

	return s.createInvoice(tenantID, start, end, entity.InvoiceType_HOURLY, entity.InvoiceStatus_CONFIRMED)
}

func (s *InvoiceModel) createInvoice(tenantID uint, start, end time.Time, invoice_type string, invoice_status string) (*forms.Invoice, error) {
	measureModel := &billing.MeasureModel{}
	costExplorerModel := &CostExplorerModel{}

	res, err := costExplorerModel.CalculateCost(tenantID, &forms.CostExplorerQuery{
		StartDate: start,
		EndDate:   end,
	})

	if err != nil {
		return nil, err
	}

	if res.UnbindedInvoiceCosts.CostPrice.Cmp(decimal.Zero) == 0 {
		return nil, billing_errors.NewInvoiceNoCosts(nil)
	}

	invoiceEntity := &entity.Invoice{
		TenantID:    tenantID,
		Status:      entity.InvoiceStatus_INITED,
		Type:        invoice_type,
		InvoiceDate: start,
	}
	err = invoiceEntity.Create(db.GetDB())
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	err = measureModel.UnpaidCostsBindInvoice(tenantID, start, end, invoiceEntity.ID)
	if err != nil {
		return nil, err
	}

	costs, err := costExplorerModel.GetInvoiceCosts(tenantID, invoiceEntity.ID)
	if err != nil {
		return nil, err
	}
	if costs.CostPrice.Cmp(decimal.Zero) == 0 {
		return nil, billing_errors.NewInvoiceNoCosts(nil)
	}

	invoiceEntity.CostPrice = costs.CostPrice
	invoiceEntity.RawPrice = costs.RawPrice

	invoiceEntity.Status = invoice_status
	err = entity.NewInvoiceQuerySet(db.GetDB()).IDEq(invoiceEntity.ID).GetUpdater().SetCostPrice(invoiceEntity.CostPrice).SetRawPrice(invoiceEntity.RawPrice).SetStatus(invoiceEntity.Status).Update()
	if err != nil {
		return nil, err
	}
	info, err := s.GetInvoice(invoiceEntity.ID)
	if err != nil {
		return nil, err
	}
	return info, nil
}

func (s *InvoiceModel) GetMonthlyInvoice(tenantID uint, date time.Time) (*forms.Invoice, error) {
	invoice_date := timeutils.GetMonthDate(date)
	invoice := &entity.Invoice{}
	err := errorutils.WrapSQLError(entity.NewInvoiceQuerySet(db.GetDB()).TenantIDEq(tenantID).InvoiceDateEq(invoice_date).One(invoice), fmt.Sprintf("tenant %d invoice date %s", tenantID, invoice_date.Format("2006-01-02")))
	if err != nil {
		return nil, err
	}
	return invoiceFromEntity(invoice), nil
}

func (s *InvoiceModel) DeleteInvoice(invoiceID uint) error {
	invoice, err := s.GetInvoice(invoiceID)
	if err != nil {
		return err
	}
	if (invoice.Status == entity.InvoiceStatus_SENT) || (invoice.Status == entity.InvoiceStatus_PAID) {
		return billing_errors.NewInvalidInputError(errors.New("could not delete invoice, invoice already be sent"))
	}
	if invoice.Type != entity.InvoiceType_MONTHLY {
		return billing_errors.NewNotImplementedError(errors.New("delete invoice with type not monthly"))
	}

	measureModel := &billing.MeasureModel{}

	err = measureModel.CostsUnBindInvoice(invoice.TenantID, invoiceID)

	if err != nil {
		return err
	}

	err = errorutils.WrapSQLError(entity.NewInvoiceQuerySet(db.GetDB()).IDEq(invoiceID).Delete(), fmt.Sprintf("invoice %d", invoiceID))
	if err != nil {
		return err
	}

	return nil

}

func (s *InvoiceModel) CreateCommitmentInvoice(tenantID uint, input *forms.CommitmentInvoiceInput) (*forms.Invoice, error) {
	recordEntity := &entity.CommitmentRecords{
		Amount: input.Amount,
		Date:   input.Date,
		Owner:  input.Owner,
	}
	err := recordEntity.Create(db.GetDB())
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	invoiceEntity := &entity.Invoice{
		TenantID:    tenantID,
		Status:      entity.InvoiceStatus_GENERATED,
		Type:        entity.InvoiceType_COMMITMENT,
		InvoiceDate: input.Date,
		CostPrice:   input.Amount,
		RawPrice:    input.Amount,
	}
	err = invoiceEntity.Create(db.GetDB())
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}
	return s.GetInvoice(invoiceEntity.ID)
}
