package api

import (
	"errors"
	"strconv"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/constant"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/model/payment"
	"github.com/pingcap/dbaas/billing2/model/payment/paymentsub"
	"github.com/pingcap/dbaas/billing2/utils/ginutils"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
)

var invoiceModel = payment.InvoiceModel{}

// @Summary List Invoices
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param brief query bool false "brief"
// @Success 200 {object} []forms.Invoice
// @Router /tenants/{tenant_id}/invoices [get]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/invoices [get]
func (s *API) ListAllInvoices(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	var query forms.BriefForm
	if err := c.ShouldBindQuery(&query); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	isHourlyPayment, err := paymentModel.IsHourlyPayment(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID)
	if err != nil {
		return nil, err
	}
	if isHourlyPayment {
		return invoiceModel.ListAllInvoicesWithType(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID, query.Brief, entity.InvoiceType_COMMITMENT)
	}
	// do not list hourly invoices in ops, after customer change payment method from marketplace to other
	ret, err := invoiceModel.ListAllInvoicesWithoutType(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID, query.Brief, entity.InvoiceType_HOURLY, entity.InvoiceType_DAILY)
	return ret, err
}

type InvoiceURIStruct struct {
	TenantID  uint `uri:"tenant_id" binding:"required"`
	InvoiceID uint `uri:"invoice_id" binding:"required"`
}

type RecalculateBillingConfigURIStruct struct {
	TenantID        uint `uri:"tenant_id" binding:"required"`
	BillingConfigID uint `uri:"billing_config_id" binding:"required"`
}

type RecalculateInvoiceURIStruct struct {
	RecalculateBillingConfigURIStruct
	InvoiceID uint `uri:"invoice_id" binding:"required"`
}

type RecalculateRunURIStruct struct {
	RecalculateBillingConfigURIStruct
	SourceRunID uint `uri:"source_run_id" binding:"required"`
}

func getInvoiceURIParams(c *gin.Context) (*InvoiceURIStruct, error) {
	invoiceURIStruct := &InvoiceURIStruct{}
	err := c.ShouldBindUri(invoiceURIStruct)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	return invoiceURIStruct, nil
}

// @Summary Get Invoice
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param invoice_id path string true "invoice_id"
// @Success 200 {object} forms.Invoice
// @Router /tenants/{tenant_id}/invoices/{invoice_id} [get]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/invoices/{invoice_id} [get]
func (s *API) GetInvoice(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	invoiceURI, err := getInvoiceURIParams(c)
	if err != nil {
		return nil, err
	}
	invoice, err := invoiceModel.GetTenantInvoice(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID, invoiceURI.InvoiceID)
	if err != nil {
		return nil, err
	}
	return invoice, nil
}

// @Summary Confirm Invoice
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param invoice_id path string true "invoice_id"
// @Success 200 {object} forms.Invoice
// @Router /tenants/{tenant_id}/invoices/{invoice_id}/confirm [put]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/invoices/{invoice_id}/confirm [put]
func (s *API) ConfirmInvoice(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	invoiceURI, err := getInvoiceURIParams(c)
	if err != nil {
		return nil, err
	}

	// Direct-pay path: the single-invoice confirm endpoint now drives the
	// push inline (audit -> status-flip -> pay) instead of leaving the row
	// in CONFIRMED for the next SendAllUnsentInvoice worker tick. The batch
	// worker is still the recovery path for any Pay failure that drops
	// the row back to CONFIRMED.
	return invoiceModel.ConfirmAndPayInvoice(invoiceURI.InvoiceID)
}

// @Summary Suspend Invoice
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param invoice_id path string true "invoice_id"
// @Success 200 {object} forms.Invoice
// @Router /tenants/{tenant_id}/invoices/{invoice_id}/suspend [put]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/invoices/{invoice_id}/suspend [put]
func (s *API) SuspendInvoice(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	invoiceURI, err := getInvoiceURIParams(c)
	if err != nil {
		return nil, err
	}

	invoice, err := invoiceModel.SuspendInvoice(invoiceURI.InvoiceID)
	if err != nil {
		return nil, err
	}
	return invoice, nil
}

// @Summary Send Invoice
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param invoice_id path string true "invoice_id"
// @Success 200 {object} forms.Invoice
// @Router /tenants/{tenant_id}/invoices/{invoice_id}/send [put]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/invoices/{invoice_id}/send [put]
func (s *API) SendInvoice(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	invoiceURI, err := getInvoiceURIParams(c)
	if err != nil {
		return nil, err
	}
	invoice, err := invoiceModel.SendInvoice(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID, invoiceURI.InvoiceID)
	if err != nil {
		return nil, err
	}
	return invoice, nil
}

// @Summary Create Invoice
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param CreateInvoiceReq body forms.CreateInvoiceReq true "CreateInvoiceReq"
// @Success 200 {object} forms.Invoice
// @Router /tenants/{tenant_id}/invoices [post]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/invoices [post]
func (s *API) CreateInvoice(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	invoice := &forms.Invoice{}

	createInvoiceReq := &forms.CreateInvoiceReq{}
	err = c.ShouldBindJSON(createInvoiceReq)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if createInvoiceReq.Type == entity.InvoiceType_MONTHLY {
		invoice, err = invoiceModel.CreateMonthlyInvoice(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID, createInvoiceReq.StartDate)

	} else if createInvoiceReq.Type == entity.InvoiceType_COSTQUOTA {
		invoice, err = invoiceModel.CreateCostQuotaInvoice(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID, createInvoiceReq.StartDate)
	} else if createInvoiceReq.Type == entity.InvoiceType_Recal {
		invoice, err = invoiceModel.CreateConfirmedRecalInvoice(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID, createInvoiceReq.StartDate)
	} else {
		return nil, billing_errors.NewInvalidInputError(errors.New("unsupported invoice type"))

	}

	if err != nil {
		return nil, err
	}
	return invoice, nil
}

// @Summary Create Commitment Invoice
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param CommitmentInvoiceInput body forms.CommitmentInvoiceInput true "CommitmentInvoiceInput"
// @Success 200 {object} forms.Invoice
// @Router /tenants/{tenant_id}/commitment_invoices [post]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/commitment_invoices [post]
func (s *API) CreateCommitmentInvoice(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	billingCfgStruct, err := getBillingCfgFromURI(c)
	if err != nil {
		return nil, err
	}
	createInvoiceReq := &forms.CommitmentInvoiceInput{}
	err = c.ShouldBindJSON(createInvoiceReq)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if createInvoiceReq.PaymentCode != nil {
		paymentMethod, err := (&paymentsub.PaymentModel{}).GetActivePaymentMethod(billingCfgStruct.TenantID, billingCfgStruct.BillingConfigID)
		if err != nil {
			return nil, err
		}
		if paymentMethod.PaymentCode != *createInvoiceReq.PaymentCode && (paymentMethod.PaymentSubCode != nil && *paymentMethod.PaymentSubCode != *createInvoiceReq.PaymentCode) {
			return nil, billing_errors.NewInvalidInputError(errors.New("the code is not matched the current active payment"))
		}
	}
	createInvoiceReq.BindedBillingConfigID = &billingCfgStruct.BillingConfigID
	invoice, err := invoiceModel.CreateCommitmentInvoice(billingCfgStruct.TenantID, createInvoiceReq)
	if err != nil {
		return nil, err
	}
	return invoice, nil
}

// @Summary Delete Unsent Invoice
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param invoice_id path string true "invoice_id"
// @Success 200 {object} forms.OKType
// @Router /tenants/{tenant_id}/invoices/{invoice_id} [delete]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/invoices/{invoice_id} [delete]
func (s *API) DeleteInvoice(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	invoiceURI, err := getInvoiceURIParams(c)
	if err != nil {
		return nil, err
	}
	if c.GetHeader(constant.TestHeaderKey) == constant.TestHeaderValue && config.Get().GetString(config.ENV) != "prod" {
		err = invoiceModel.DeleteInvoiceForTest(invoiceURI.InvoiceID)
	} else {
		err = invoiceModel.DeleteInvoice(invoiceURI.InvoiceID)
	}
	if err != nil {
		return nil, err
	}
	return forms.OK, nil
}

// @ModifyInvoiceStatus
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int false "billing_config_id"
// @Param invoice_id path string true "invoice_id"
// @Success 200 {object} forms.OKType
// @Router /tenants/{tenant_id}/invoices/{invoice_id}/modify [POST]
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/invoices/{invoice_id}/modify [POST]
func (s *API) ReSendInvoice(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	invoiceURI, err := getInvoiceURIParams(c)
	if err != nil {
		return nil, err
	}
	err = invoiceModel.ReSendInvoice(invoiceURI.TenantID, invoiceURI.InvoiceID)
	if err != nil {
		return nil, err
	}
	return forms.OK, nil
}

// @ListInvoices
// @Schemes
// @Accept json
// @Produce json
// @Param brief query bool false "brief"
// @Param from query string false "from, example: 2024-01-01T00:00:00Z"
// @Param to query string false "to, example: 2024-01-01T00:00:00Z"
// @Param status query string false "invoice status"
// @Param statuses[] query []string false "invoice statuses"
// @Param active_payment query string false "active payment type"
// @Param tenant_ids query string false "comma-separated tenant IDs"
// @Success 200 {object} []forms.InvoiceForOpsList
// @Router /invoices [GET]
func parseInvoiceTenantIDs(value string) ([]uint, error) {
	if value == "" {
		return nil, nil
	}

	parts := strings.Split(value, ",")
	tenantIDs := make([]uint, 0, len(parts))
	seen := make(map[uint]struct{}, len(parts))
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part == "" {
			return nil, errors.New("tenant_ids contains an empty value")
		}
		id, err := strconv.ParseUint(part, 10, 0)
		if err != nil || id == 0 {
			return nil, errors.New("tenant_ids must contain positive integers")
		}
		tenantID := uint(id)
		if _, ok := seen[tenantID]; ok {
			continue
		}
		seen[tenantID] = struct{}{}
		tenantIDs = append(tenantIDs, tenantID)
	}
	return tenantIDs, nil
}

func (s *API) ListInvoicesForOperation(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	query := forms.InvoicesQuery{}
	if err := c.ShouldBindQuery(&query); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	tenantIDs, err := parseInvoiceTenantIDs(query.TenantIDs)
	if err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if query.Status != "" {
		query.Statuses = append(query.Statuses, query.Status)
	}
	maxSpan := config.Get().GetUint(config.InvoicesQueryMaxSpanDays)
	if query.From != nil && query.To != nil {
		span := uint(query.To.Sub(*query.From) / time.Hour / 24)
		if span < 0 || span > maxSpan {
			return nil, billing_errors.NewInvalidInputError(errors.New("out of time range"))
		}
	} else {
		now := timeutils.GetNowAddr()
		query.From = timeutils.GetTimeAddr(now.Add(-time.Hour * 24 * time.Duration(maxSpan)))
		query.To = now
	}
	return invoiceModel.ListInvoicesListInvoicesForOperation(query.Brief, *query.From, *query.To, query.Statuses, query.ActivePayment, tenantIDs)
}

// @Summary Dry Run Recalculate Bills
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int true "billing_config_id"
// @Param req body forms.RecalculateBillsPreviewReq true "RecalculateBillsPreviewReq"
// @Success 200 {object} forms.RecalculateStartResponse
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/recalculate [post]
func (s *API) RecalculateBills(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	uri := &RecalculateBillingConfigURIStruct{}
	if err := c.ShouldBindUri(uri); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	req := &forms.RecalculateBillsPreviewReq{}
	if err := c.ShouldBindJSON(req); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}

	return invoiceModel.RecalculateBills(uri.TenantID, uri.BillingConfigID, req)
}

// @Summary Get Recalculate Run
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int true "billing_config_id"
// @Param source_run_id path string true "source_run_id"
// @Success 200 {object} forms.RecalculatePreviewResponse
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/recalculate/runs/{source_run_id} [get]
func (s *API) GetRecalculateRun(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	uri := &RecalculateRunURIStruct{}
	if err := c.ShouldBindUri(uri); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	return invoiceModel.GetRecalculateRun(uri.TenantID, uri.BillingConfigID, uri.SourceRunID)
}

func (s *API) InternalRecalculateBills(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	uri := &RecalculateBillingConfigURIStruct{}
	if err := c.ShouldBindUri(uri); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	req := &forms.RecalculateBillsApplyReq{}
	if err := c.ShouldBindJSON(req); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}

	return invoiceModel.InternalRecalculateBills(uri.TenantID, uri.BillingConfigID, req)
}

// @Summary Dry Run Recalculate Invoice
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int true "billing_config_id"
// @Param invoice_id path string true "invoice_id"
// @Success 200 {object} forms.RecalculateStartResponse
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/invoices/{invoice_id}/recalculate [post]
func (s *API) RecalculateInvoice(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	uri := &RecalculateInvoiceURIStruct{}
	if err := c.ShouldBindUri(uri); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}

	return invoiceModel.RecalculateInvoice(uri.TenantID, uri.BillingConfigID, uri.InvoiceID)
}

func InitializeInvoiceRouter(router ginutils.MyRoute) {
	s := &API{}

	router.HandleWithError("GET", "/invoices", s.ListInvoicesForOperation)

	router.HandleWithError("GET", "/tenants/:tenant_id/invoices", s.ListAllInvoices)
	router.HandleWithError("POST", "/tenants/:tenant_id/invoices", s.CreateInvoice)
	router.HandleWithError("POST", "/tenants/:tenant_id/commitment_invoices", s.CreateCommitmentInvoice)
	router.HandleWithError("GET", "/tenants/:tenant_id/invoices/:invoice_id", s.GetInvoice)
	router.HandleWithError("PUT", "/tenants/:tenant_id/invoices/:invoice_id/confirm", s.ConfirmInvoice)
	router.HandleWithError("PUT", "/tenants/:tenant_id/invoices/:invoice_id/suspend", s.SuspendInvoice)
	router.HandleWithError("PUT", "/tenants/:tenant_id/invoices/:invoice_id/send", s.SendInvoice)
	router.HandleWithError("DELETE", "/tenants/:tenant_id/invoices/:invoice_id", s.DeleteInvoice)
	router.HandleWithError("POST", "/tenants/:tenant_id/invoices/:invoice_id/resend", s.ReSendInvoice)

	// for billing config add api
	router.HandleWithError("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/invoices", s.CreateInvoice)
	router.HandleWithError("PUT", "/tenants/:tenant_id/billing_config_id/:billing_config_id/invoices/:invoice_id/send", s.SendInvoice)
	router.HandleWithError("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/commitment_invoices", s.CreateCommitmentInvoice)
	router.HandleWithError("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/invoices", s.ListAllInvoices)
	router.HandleWithError("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/recalculate", s.RecalculateBills)
	router.HandleWithError("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/recalculate/runs", s.ListRecalculateRuns)
	router.HandleWithError("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/recalculate/runs/:source_run_id", s.GetRecalculateRun)
	router.HandleWithError("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/recalculate/runs/:source_run_id/bills_details", s.RecalculateRunBillsDetails)
	router.Handle("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/recalculate/runs/:source_run_id/bills_details/export", s.ExportRecalculateRunBillsCSV)
	router.HandleWithError("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/internal/recalculate", s.InternalRecalculateBills)
	router.HandleWithError("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/invoices/:invoice_id/recalculate", s.RecalculateInvoice)

	//no modify
	router.HandleWithError("GET", "/tenants/:tenant_id/billing_config_id/:billing_config_id/invoices/:invoice_id", s.GetInvoice)
	router.HandleWithError("PUT", "/tenants/:tenant_id/billing_config_id/:billing_config_id/invoices/:invoice_id/confirm", s.ConfirmInvoice)
	router.HandleWithError("PUT", "/tenants/:tenant_id/billing_config_id/:billing_config_id/invoices/:invoice_id/suspend", s.SuspendInvoice)
	router.HandleWithError("DELETE", "/tenants/:tenant_id/billing_config_id/:billing_config_id/invoices/:invoice_id", s.DeleteInvoice)
	router.HandleWithError("POST", "/tenants/:tenant_id/billing_config_id/:billing_config_id/invoices/:invoice_id/resend", s.ReSendInvoice)

}
