package api

import (
	"bytes"
	"encoding/csv"
	"errors"
	"fmt"
	"net/http"
	"strconv"

	"github.com/gin-gonic/gin"
	"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/handler/centralhandler"
	"github.com/pingcap/dbaas/billing2/model/costexplorer"
	"github.com/pingcap/dbaas/billing2/utils/ginutils"
	"github.com/pingcap/dbaas/billing2/utils/pagination"
	"gorm.io/gorm"
)

// @Summary List Recalculate Runs
// @Schemes
// @Accept json
// @Produce json
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int true "billing_config_id"
// @Param page query int false "page,default=1,min=1,max=100"
// @Param page_size query int false "page_size,default=20,min=1,max=100"
// @Success 200 {object} forms.RecalculateRunListResponse
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/recalculate/runs [get]
func (s *API) ListRecalculateRuns(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	uri := &RecalculateBillingConfigURIStruct{}
	if err := c.ShouldBindUri(uri); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if err := ensureCostExplorerBillsVisible(c, uri.TenantID, uri.BillingConfigID); err != nil {
		return nil, err
	}
	p, err := pagination.GetPagination(c)
	if err != nil {
		return nil, err
	}
	return invoiceModel.ListRecalculateRuns(uri.TenantID, uri.BillingConfigID, p)
}

// @Summary Get Recalculate Run Bills Details
// @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 int true "source_run_id"
// @Success 200 {object} forms.CostExplorerDetailsResult
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/recalculate/runs/{source_run_id}/bills_details [get]
func (s *API) RecalculateRunBillsDetails(c *gin.Context) (ginutils.HandlerFuncResult, error) {
	uri := &RecalculateRunURIStruct{}
	if err := c.ShouldBindUri(uri); err != nil {
		return nil, billing_errors.NewInvalidInputError(err)
	}
	if err := ensureCostExplorerBillsVisible(c, uri.TenantID, uri.BillingConfigID); err != nil {
		return nil, err
	}
	return (&costexplorer.CostExplorerModel{}).GetRecalculateRunBillsDetails(uri.TenantID, uri.BillingConfigID, uri.SourceRunID)
}

// @Summary Export Recalculate Run Bills CSV
// @Schemes
// @Accept json
// @Produce octet-stream
// @Param tenant_id path string true "tenant_id"
// @Param billing_config_id path int true "billing_config_id"
// @Param source_run_id path int true "source_run_id"
// @Success 200 {file} file "CSV file"
// @Router /tenants/{tenant_id}/billing_config_id/{billing_config_id}/recalculate/runs/{source_run_id}/bills_details/export [get]
func (s *API) ExportRecalculateRunBillsCSV(c *gin.Context) {
	uri := &RecalculateRunURIStruct{}
	if err := c.ShouldBindUri(uri); err != nil {
		ginutils.ReturnError(c, billing_errors.NewInvalidInputError(err))
		return
	}
	if err := ensureCostExplorerBillsVisible(c, uri.TenantID, uri.BillingConfigID); err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	run, err := loadRerunRunForExport(uri.TenantID, uri.BillingConfigID, uri.SourceRunID)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	details, err := (&costexplorer.CostExplorerModel{}).GetRecalculateRunExportDetails(uri.TenantID, uri.BillingConfigID, uri.SourceRunID)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}
	tenant, err := centralhandler.GetCentralHandler().GetTenant(uri.TenantID)
	if err != nil {
		ginutils.ReturnError(c, err)
		return
	}

	rows := [][]string{
		{"Billing Period", run.PeriodMonth},
		{"Organization Name", tenant.Name},
		{"Organization ID", strconv.FormatUint(uint64(uri.TenantID), 10)},
		{},
		{"Billed Date", "Service", "Cloud Provider", "Region", "Project Name", "Project ID", "Cluster ID", "Cluster Name", "Running Total", "Discounts", "Credits", "Total Cost", "Usage", "Unit"},
	}
	const precision int32 = 6
	for _, detail := range details {
		amount := detail.Amount.Truncate(precision)
		creditUsage := detail.CreditUsage.Truncate(precision)
		billableCost := detail.BillableCost.Truncate(precision)
		rows = append(rows, []string{
			detail.BilledDate,
			detail.Service,
			detail.CloudProvider,
			detail.Region,
			detail.ProjectName,
			detail.ProjectID,
			detail.ClusterID,
			detail.ClusterName,
			amount.String(),
			amount.Sub(creditUsage).Sub(billableCost).String(),
			creditUsage.String(),
			billableCost.String(),
			formatExportUsageValue(detail, precision),
			detail.Unit,
		})
	}

	var output bytes.Buffer
	if err := csv.NewWriter(&output).WriteAll(rows); err != nil {
		ginutils.ReturnError(c, billing_errors.NewSystemError(err))
		return
	}
	c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=recalculate_run_%d_bills.csv", uri.SourceRunID))
	c.Data(http.StatusOK, "application/octet-stream", output.Bytes())
}

func loadRerunRunForExport(tenantID uint, billingConfigID uint, runID uint) (*entity.BillingSnapshotRun, error) {
	var run entity.BillingSnapshotRun
	err := db.GetDB().Model(&entity.BillingSnapshotRun{}).
		Joins("JOIN billing_snapshot_input_bundle ON billing_snapshot_input_bundle.bundle_id = billing_snapshot_run.input_bundle_id").
		Where("billing_snapshot_run.run_id = ?", runID).
		Where("billing_snapshot_run.tenant_id = ?", tenantID).
		Where("billing_snapshot_run.billing_config_id = ?", billingConfigID).
		Where("billing_snapshot_input_bundle.bundle_type IN ?", []string{
			entity.BillingSnapshotInputBundleTypeRerunCandidate,
			entity.BillingSnapshotInputBundleTypeRerunPublished,
		}).
		Select("billing_snapshot_run.*").First(&run).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, billing_errors.ObjectNotExistsError
	}
	if err != nil {
		return nil, err
	}
	if run.Status != entity.BillingSnapshotRunStatusSuccess && run.Status != entity.BillingSnapshotRunStatusFrozen {
		return nil, billing_errors.NewInvalidInputError(fmt.Errorf("snapshot run %d is not successful: %s", runID, run.Status))
	}
	return &run, nil
}
