package costexplorer

import (
	"context"
	"errors"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/dbaas/billing2/config"
	"github.com/pingcap/dbaas/billing2/db"
	"github.com/pingcap/dbaas/billing2/db/entity"
	"github.com/pingcap/dbaas/billing2/forms"
	"github.com/pingcap/dbaas/billing2/handler/cachehandler"
	"github.com/pingcap/dbaas/billing2/utils/log"
	"github.com/pingcap/dbaas/billing2/utils/timeutils"
	"go.uber.org/zap"
)

type UsageExplorerModel struct{}

const (
	MeasureLabelKeyAppId = "appId"
	DataAPIRequestCount  = "request"
	DataAPINetWorkCount  = "network"
	AppCfgSuffix         = "-usage-units-map"
)

func (c *UsageExplorerModel) GetMonthlyAppUsageWithCache(param forms.MonthlyUsageURI) (*forms.AppAPIUsageExplore, error) {
	cache := cachehandler.Get()
	result := &forms.AppAPIUsageExplore{}
	found := cache.Get("GetMonthlyAppUsageWithCache", &result, param.TenantID, param.Year, param.Month, param.AppId, param.AppName)
	if found {
		return result, nil
	}
	query := getUsageQueryParma(param.Year, param.Month)
	result, err := c.GetMonthlyAppUsage(param.TenantID, query, param.AppId, param.AppName)
	if err == nil {
		cache.Set("GetMonthlyAppUsageWithCache", result, param.TenantID, param.Year, param.Month, param.AppId, param.AppName)
	}
	return result, err
}

func (c *UsageExplorerModel) GetMonthlyAppUsage(tenantID uint, query *forms.CostExplorerQuery, appID uint64, appName string) (*forms.AppAPIUsageExplore, error) {
	appUsageUnitsMap := getUnitParams(appName, query)

	filter := []string{}
	params := map[string]interface{}{}

	{
		filter = append(filter, "tenant_id = @tenant_id")
		params["tenant_id"] = tenantID

		filter = append(filter, "status = @status")
		params["status"] = entity.UserUnitCostStatus_DONE

		filter = append(filter, "internal = @internal")
		params["internal"] = true
	}
	{

		filter = append(filter, fmt.Sprintf("JSON_UNQUOTE(JSON_EXTRACT(labels, @%s)) = @%s", "label_val_path_0", "label_val_0"))
		params["label_val_path_0"] = fmt.Sprintf("$.%s", MeasureLabelKeyAppId)
		params["label_val_0"] = strconv.FormatUint(appID, 10)

	}

	if !query.StartDate.IsZero() {
		filter = append(filter, "cost_time_stamp > @start_date")
		params["start_date"] = query.StartDate.Unix() - 1
	}

	if !query.EndDate.IsZero() {
		filter = append(filter, "cost_time_stamp < @end_date")
		params["end_date"] = query.EndDate.Unix()
	}

	if len(query.Units) != 0 {
		filter = append(filter, "unit in @unit")
		params["unit"] = query.Units
	}

	disabledTenants := config.Get().GetStringSlice(config.DisableClusterDisplayTenants)
	shouldRemoveLabelsFromGroupBy := false

	if slices.Contains(disabledTenants, fmt.Sprint(tenantID)) {
		shouldRemoveLabelsFromGroupBy = true
		if !query.StartDate.IsZero() {
			cutoffDate := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC)
			if query.StartDate.Before(cutoffDate) {
				shouldRemoveLabelsFromGroupBy = false
			}
		}
	}

	var sqlQuery string
	if shouldRemoveLabelsFromGroupBy {
		sqlQuery = fmt.Sprintf(`select unit, sum(cost) as cost from user_unit_costs where %s group by unit;`, strings.Join(filter, " and "))
	} else {
		sqlQuery = fmt.Sprintf(`select unit, labels, sum(cost) as cost from user_unit_costs where %s group by unit, labels;`, strings.Join(filter, " and "))
	}

	res := &forms.AppAPIUsageExplore{
		DataAPIUsageExplore: forms.DataAPIUsageExplore{
			TenantID:     tenantID,
			RequestCount: 0,
			NetworkUsage: 0,
		},
	}

	usageMap := make(map[string]int64)
	for _, v := range appUsageUnitsMap {
		usageMap[v] = 0
	}

	queryTimeout := config.Get().GetDuration(config.PerDBQueryTimeoutSeconds) * time.Second
	ctx, cancel := context.WithTimeout(context.Background(), queryTimeout)
	defer cancel()

	queryDB := db.GetDB()
	queryStartTime := time.Now()
	rows, err := queryDB.WithContext(ctx).Raw(sqlQuery, params).Rows()
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			log.Error("GetMonthlyAppUsage query timeout",
				zap.Uint("tenantID", tenantID),
				zap.Uint64("appID", appID),
				zap.Duration("timeout", queryTimeout),
				zap.Error(err))
			return nil, billing_errors.NewSystemError(fmt.Errorf("query timeout after %v: %w", queryTimeout, err))
		}
		return nil, billing_errors.NewSystemError(err)
	}
	defer rows.Close()

	costsCount := 0
	for rows.Next() {
		var cost entity.UnitCostSummary
		if err = queryDB.ScanRows(rows, &cost); err != nil {
			if errors.Is(err, context.DeadlineExceeded) {
				log.Error("GetMonthlyAppUsage scan timeout",
					zap.Uint("tenantID", tenantID),
					zap.Uint64("appID", appID),
					zap.Duration("timeout", queryTimeout),
					zap.Error(err))
				return nil, billing_errors.NewSystemError(fmt.Errorf("query timeout after %v: %w", queryTimeout, err))
			}
			return nil, billing_errors.NewSystemError(err)
		}
		costsCount++
		if len(appName) == 0 {
			if appUsageUnitsMap[cost.Unit] == DataAPIRequestCount {
				res.RequestCount = res.RequestCount + int64(cost.Cost)
			} else if appUsageUnitsMap[cost.Unit] == DataAPINetWorkCount {
				res.NetworkUsage = res.NetworkUsage + int64(cost.Cost)
			} else {
				log.Info("dataapp split usage error ", zap.Uint("tenantId", tenantID), zap.Any("cost", cost), zap.Uint64("appID", appID))
			}
		}

		usageMapKey, ok := appUsageUnitsMap[cost.Unit]
		v, ok := usageMap[usageMapKey]
		if ok {
			usageMap[usageMapKey] = v + int64(cost.Cost)
		} else {
			log.Info("app split  usage error ", zap.Uint("tenantId", tenantID), zap.Any("cost", cost), zap.Uint64("appID", appID))
		}
	}
	if err = rows.Err(); err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			log.Error("GetMonthlyAppUsage rows timeout",
				zap.Uint("tenantID", tenantID),
				zap.Uint64("appID", appID),
				zap.Duration("timeout", queryTimeout),
				zap.Error(err))
			return nil, billing_errors.NewSystemError(fmt.Errorf("query timeout after %v: %w", queryTimeout, err))
		}
		return nil, billing_errors.NewSystemError(err)
	}

	log.Info("GetMonthlyAppUsage cost explore query size",
		zap.Int("costsCount", costsCount),
		zap.Uint("tenantID", tenantID),
		zap.Uint64("appID", appID),
		zap.Duration("queryDuration", time.Since(queryStartTime)))

	res.UsageMap = usageMap
	return res, nil
}

func getUnitParams(appName string, query *forms.CostExplorerQuery) map[string]string {
	appUsageUnitsMap := map[string]string{}
	if len(appName) == 0 {
		appUsageUnitsMap = config.Get().GetStringMapString(config.DataapiUsageUnitsMap)
	} else {
		appUsageUnitsMap = config.Get().GetStringMapString(appName + AppCfgSuffix)
	}
	log.Info("getUnitParams", zap.Any("appUsageUnitsMap", appUsageUnitsMap), zap.Any("query", query))

	units := make([]string, 0, len(appUsageUnitsMap))
	for key := range appUsageUnitsMap {
		units = append(units, key)
	}
	query.Units = units
	return appUsageUnitsMap
}

func getUsageQueryParma(year int, month int) *forms.CostExplorerQuery {
	start := timeutils.GetMonthDateByYearMonth(year, month)
	query := &forms.CostExplorerQuery{
		StartDate: start,
		EndDate:   start.AddDate(0, 1, 0),
	}
	return query
}
