package models

import (
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"

	log "github.com/sirupsen/logrus"
)

type BilingTenanter interface {
	GetTenantID() uint64
}

// BillingMetering is the metering data for Azure billing
type BillingMetering struct {
	ID          uint  `gorm:"primaryKey"`
	CreatedAt   int64 `gorm:"autoCreateTime"`
	UpdatedAt   int64 `gorm:"autoUpdateTime"`
	TenantID    string
	ProjectID   string
	ClusterID   string
	UsedBy      string
	BillingDate int64
	ResourceID  string
	Cost        int64
	Unit        string //sku name
}

// BillingReport is the report to billing server
type BillingReport struct {
	TenantID      uint64 `json:"tenant_id"`
	CostID        string `json:"cost_id" gorm:"primaryKey"`
	Cost          int64  `json:"cost"`
	CostTimestamp int64  `json:"cost_time_stamp"`
	Labels        string `json:"labels"`
	Unit          string `json:"unit"`
	CreatedAt     int64  `gorm:"autoCreateTime"`
}

func (b BillingReport) GetTenantID() uint64 {
	return b.TenantID
}

func (b BillingReport) IsNodeSKU() bool {
	return strings.HasPrefix(b.Unit, "node")
}

func (b BillingReport) IsTiproxySKU() bool {
	return strings.HasPrefix(b.Unit, "tiproxy")
}

type BillingReportV2 struct {
	SchemeVersion string `json:"scheme_version"`
	TenantID      uint64 `json:"tenant_id"`
	CostID        string `json:"cost_id" gorm:"primaryKey"`
	CostTimestamp int64  `json:"cost_time_stamp"`
	Unit          string `json:"unit"`
	UnitValue     string `json:"unit_value"`
	Cost          int64  `json:"cost"`
	Labels        string `json:"labels"`
	CreatedAt     int64  `gorm:"autoCreateTime"`
}

func (b BillingReportV2) IsTiproxySKU() bool {
	return strings.HasPrefix(b.Unit, "tiproxy")
}

func (b BillingReportV2) GetTenantID() uint64 {
	return b.TenantID
}

func (b BillingReportV2) Convert2OldReporter() (BillingReport, error) {
	//we must know the unit value is what,then we can convert it to old reporter
	if strings.HasPrefix(b.Unit, "node") {
		old := BillingReport{
			TenantID:      b.TenantID,
			CostID:        b.CostID,
			Cost:          b.Cost,
			CostTimestamp: b.CostTimestamp,
			Labels:        b.Labels,
			Unit:          b.Unit,
		}
		log.Debugf("convert to old reporter,new repoter is %v, old reporter is %v", b, old)
		return old, nil
	} else if strings.HasPrefix(b.Unit, "tiproxy") {
		old := BillingReport{
			TenantID:      b.TenantID,
			CostID:        b.CostID,
			Cost:          b.Cost,
			CostTimestamp: b.CostTimestamp,
			Labels:        b.Labels,
			Unit:          b.Unit,
		}
		log.Debugf("convert to old reporter,new repoter is %v, old reporter is %v", b, old)
		return old, nil
	} else if strings.HasPrefix(b.Unit, "disk-storage") {
		//deal disk-storage
		storage, err := strconv.ParseInt(b.UnitValue, 10, 64)
		if err != nil {
			log.Errorf("convert to old reporter,convert storage to int64 failed, error is %v", err)
			return BillingReport{}, err
		}
		cost := storage * b.Cost //this is the size of disk * number of disk
		old := BillingReport{
			TenantID:      b.TenantID,
			CostID:        b.CostID,
			Cost:          cost,
			CostTimestamp: b.CostTimestamp,
			Labels:        b.Labels,
			Unit:          b.Unit,
		}
		log.Debugf("convert to old reporter,new repoter is %v, old reporter is %v", b, old)
		return old, nil
	} else if strings.HasPrefix(b.Unit, "disk-iops") {
		labels := getjson(b.Labels)
		labels["iops"] = b.UnitValue
		labelsJson := tojson(labels)
		old := BillingReport{
			TenantID:      b.TenantID,
			CostID:        b.CostID,
			Cost:          b.Cost,
			CostTimestamp: b.CostTimestamp,
			Labels:        labelsJson,
			Unit:          b.Unit,
		}
		log.Debugf("convert to old reporter,new repoter is %v, old reporter is %v", b, old)
		return old, nil
	} else if strings.HasPrefix(b.Unit, "disk-throughput") {
		labels := getjson(b.Labels)
		labels["throughput"] = b.UnitValue
		labelsJson := tojson(labels)
		old := BillingReport{
			TenantID:      b.TenantID,
			CostID:        b.CostID,
			Cost:          b.Cost,
			CostTimestamp: b.CostTimestamp,
			Labels:        labelsJson,
			Unit:          b.Unit,
		}
		log.Debugf("convert to old reporter,new repoter is %v, old reporter is %v", b, old)
		return old, nil
	} else if strings.HasPrefix(b.Unit, "disk-additional-iops") {
		addtionaliops, err := strconv.ParseInt(b.UnitValue, 10, 64)
		if err != nil {
			log.Errorf("convert to old reporter,convert storage to int64 failed, error is %v", err)
			return BillingReport{}, err
		}
		cost := addtionaliops * b.Cost //this is the size of disk additional iops * number of disk
		old := BillingReport{
			TenantID:      b.TenantID,
			CostID:        b.CostID,
			Cost:          cost,
			CostTimestamp: b.CostTimestamp,
			Labels:        b.Labels,
			Unit:          b.Unit,
		}
		log.Debugf("convert to old reporter,new repoter is %v, old reporter is %v", b, old)
		return old, nil
	}
	log.Errorf("convert to old reporter,unit is not supported,unit is %s", b.Unit)
	return BillingReport{}, errors.New("unit is not supported")
}

type BillingReportSender struct {
	TenantID      uint64            `json:"tenant_id"`
	CostID        string            `json:"cost_id"`
	Cost          int64             `json:"cost"`
	CostTimestamp int64             `json:"cost_time_stamp"`
	Labels        map[string]string `json:"labels"`
	Unit          string            `json:"unit"`
}

// ReportCheckPoint is the checkpoint for report
type ReportCheckPoint struct {
	Timestamp int64 `json:"timestamp"`
}

func getjson(s string) map[string]string {
	var result map[string]string
	err := json.Unmarshal([]byte(s), &result)
	if err != nil {
		//in this case we cna't parese error,so if error occurs,we panic
		panic(err)
	}
	return result
}

func tojson(m map[string]string) string {
	var result string
	b, err := json.Marshal(m)
	if err != nil {
		//in this case we cna't parese error,so if error occurs,we panic
		panic(err)
	}
	result = string(b)
	return result
}

// BillingReportV2Deltalake extends BillingReportV2 for Delta Lake storage
// It includes partition fields and other Delta Lake specific requirements
type BillingReportV2Deltalake struct {
	// Inherited fields from BillingReportV2
	SchemeVersion string `json:"scheme_version"`
	TenantID      uint64 `json:"tenant_id"`
	CostID        string `json:"cost_id"`
	CostTimestamp int64  `json:"cost_time_stamp"`
	Unit          string `json:"unit"`
	UnitValue     string `json:"unit_value"`
	Cost          int64  `json:"cost"`
	Labels        string `json:"labels"`
	CreatedAt     int64  `json:"created_at"`

	// Delta Lake specific fields
	// Note: ClusterID is now included in the Labels field as JSON
	SKUType string `json:"sku_type"` // SKU type classification

	// Delta Lake partition fields
	HourDimension string `json:"hour_dimension"` // Partition field: YYYYMMDDHH format
}

// GetTenantID implements BilingTenanter interface
func (b BillingReportV2Deltalake) GetTenantID() uint64 {
	return b.TenantID
}

// Convert2BillingReportV2 converts to original BillingReportV2 format
func (b BillingReportV2Deltalake) Convert2BillingReportV2() BillingReportV2 {
	return BillingReportV2{
		SchemeVersion: b.SchemeVersion,
		TenantID:      b.TenantID,
		CostID:        b.CostID,
		CostTimestamp: b.CostTimestamp,
		Unit:          b.Unit,
		UnitValue:     b.UnitValue,
		Cost:          b.Cost,
		Labels:        b.Labels,
		CreatedAt:     b.CreatedAt,
	}
}

// ToBillingReport converts BillingReportV2Deltalake to BillingReport
func (b BillingReportV2Deltalake) Convert2BillingReport() (BillingReport, error) {
	repoter := BillingReport{
		TenantID:      b.TenantID,
		CostID:        b.CostID,
		CostTimestamp: b.CostTimestamp,
		Unit:          b.Unit,
	}
	// Convert unitvalue to cost
	unitvlue, err := strconv.ParseInt(b.UnitValue, 10, 64)
	if err != nil {
		err = fmt.Errorf("convert to billing report,convert unitvalue to int64 failed, error is %v", err)
		return BillingReport{}, err
	}
	quantity := unitvlue
	switch {
	case requiresMiBConversion(b.Unit):
		if unitvlue < 0 {
			return BillingReport{}, fmt.Errorf("convert to billing report, unitvalue should be non-negative, got %d", unitvlue)
		}
		labelsMap := map[string]string{}
		if strings.TrimSpace(b.Labels) != "" {
			if err := json.Unmarshal([]byte(b.Labels), &labelsMap); err != nil {
				return BillingReport{}, fmt.Errorf("convert to billing report, parse labels failed: %w", err)
			}
		}
		const bytesPerMiB int64 = 1024 * 1024
		//support round up
		mibQuantity := (unitvlue + bytesPerMiB - 1) / bytesPerMiB
		//ensure at least 1 MiB is billed
		if mibQuantity < 1 {
			mibQuantity = 1
		}
		labelsMap[NextGenSKULabelKeyOriginalBytes] = strconv.FormatInt(unitvlue, 10)
		encodedLabels, err := json.Marshal(labelsMap)
		if err != nil {
			return BillingReport{}, fmt.Errorf("convert to billing report, marshal labels failed: %w", err)
		}
		repoter.Labels = string(encodedLabels)
		quantity = mibQuantity
	case strings.HasPrefix(b.Unit, NextGenSKURCU):
		if strings.TrimSpace(b.Labels) == "" {
			return BillingReport{}, fmt.Errorf("convert to billing report, labels required for RCU sku %s", b.Unit)
		}
		labelsMap := map[string]string{}
		if err := json.Unmarshal([]byte(b.Labels), &labelsMap); err != nil {
			return BillingReport{}, fmt.Errorf("convert to billing report, parse labels failed: %w", err)
		}
		intervalValueStr, ok := labelsMap[NextGenSKULabelKeyIntervalValue]
		if !ok || strings.TrimSpace(intervalValueStr) == "" {
			return BillingReport{}, fmt.Errorf("convert to billing report, interval value is required for RCU sku %s", b.Unit)
		}
		intervalValue, err := strconv.ParseInt(intervalValueStr, 10, 64)
		if err != nil {
			return BillingReport{}, fmt.Errorf("convert to billing report, invalid interval value %q: %w", intervalValueStr, err)
		}
		if intervalValue < 0 {
			return BillingReport{}, fmt.Errorf("convert to billing report, interval value should be non-negative, got %d", intervalValue)
		}
		var converted int64
		if intervalValue == 0 {
			converted = 0
		} else {
			converted = (intervalValue + 59) / 60
		}
		labelsMap[NextGenSKULabelKeyIntervalValue] = strconv.FormatInt(converted, 10)
		encodedLabels, err := json.Marshal(labelsMap)
		if err != nil {
			return BillingReport{}, fmt.Errorf("convert to billing report, marshal labels failed: %w", err)
		}
		repoter.Labels = string(encodedLabels)
	case strings.HasPrefix(b.Unit, NextGenSKUServerlessRequestCapacityUnit):
		if strings.TrimSpace(b.Labels) == "" {
			return BillingReport{}, fmt.Errorf("convert to billing report, labels required for serverless RCU sku %s", b.Unit)
		}
		labelsMap := map[string]string{}
		if err := json.Unmarshal([]byte(b.Labels), &labelsMap); err != nil {
			return BillingReport{}, fmt.Errorf("convert to billing report, parse labels failed for serverless RCU: %w", err)
		}
		intervalValueStr, ok := labelsMap[NextGenSKULabelKeyIntervalValue]
		if !ok || strings.TrimSpace(intervalValueStr) == "" {
			return BillingReport{}, fmt.Errorf("convert to billing report, interval value is required for serverless RCU sku %s", b.Unit)
		}
		intervalValue, err := strconv.ParseUint(intervalValueStr, 10, 64)
		if err != nil {
			return BillingReport{}, fmt.Errorf("convert to billing report, invalid interval value %q: %w", intervalValueStr, err)
		}
		cost := (intervalValue + 59) / 60
		intervalMinStr, hasMin := labelsMap[NextGenSKULabelKeyIntervalMin]
		if hasMin && strings.TrimSpace(intervalMinStr) != "" {
			intervalMin, err := strconv.ParseUint(intervalMinStr, 10, 64)
			if err != nil {
				return BillingReport{}, fmt.Errorf("convert to billing report, invalid intervalMin %q: %w", intervalMinStr, err)
			}
			if intervalMin > cost {
				cost = intervalMin
			}
		}
		if maxStr, hasMax := labelsMap[NextGenSKULabelKeyIntervalMax]; hasMax && strings.TrimSpace(maxStr) != "" {
			intervalMax, err := strconv.ParseUint(maxStr, 10, 64)
			if err != nil {
				return BillingReport{}, fmt.Errorf("convert to billing report, invalid intervalMax %q: %w", maxStr, err)
			}
			if cost > intervalMax {
				cost = intervalMax
			}
		}
		labelsMap[NextGenSKULabelKeyIntervalValue] = strconv.FormatUint(cost, 10)
		encodedLabels, err := json.Marshal(labelsMap)
		if err != nil {
			return BillingReport{}, fmt.Errorf("convert to billing report, marshal labels failed: %w", err)
		}
		repoter.Labels = string(encodedLabels)
		repoter.Cost = int64(cost)
		return repoter, nil
	case strings.HasPrefix(b.Unit, NextGenSKUDiskIOPS):
		labelsMap := map[string]string{}
		if strings.TrimSpace(b.Labels) != "" {
			if err := json.Unmarshal([]byte(b.Labels), &labelsMap); err != nil {
				return BillingReport{}, fmt.Errorf("convert to billing report, parse labels failed: %w", err)
			}
		}
		labelsMap["iops"] = b.UnitValue
		encodedLabels, err := json.Marshal(labelsMap)
		if err != nil {
			return BillingReport{}, fmt.Errorf("convert to billing report, marshal labels failed: %w", err)
		}
		repoter.Labels = string(encodedLabels)
		repoter.Cost = b.Cost
		return repoter, nil
	case strings.HasPrefix(b.Unit, NextGenSKUDiskThroughput):
		labelsMap := map[string]string{}
		if strings.TrimSpace(b.Labels) != "" {
			if err := json.Unmarshal([]byte(b.Labels), &labelsMap); err != nil {
				return BillingReport{}, fmt.Errorf("convert to billing report, parse labels failed: %w", err)
			}
		}
		labelsMap["throughput"] = b.UnitValue
		encodedLabels, err := json.Marshal(labelsMap)
		if err != nil {
			return BillingReport{}, fmt.Errorf("convert to billing report, marshal labels failed: %w", err)
		}
		repoter.Labels = string(encodedLabels)
		repoter.Cost = b.Cost
		return repoter, nil
	default:
		repoter.Labels = b.Labels
	}
	repoter.Cost = b.Cost * quantity
	return repoter, nil

}

func requiresMiBConversion(unit string) bool {
	return strings.HasPrefix(unit, NextGenSKUDataTransferOut) ||
		strings.HasPrefix(unit, NextGenSKUDataTransferCrossAZ) ||
		(strings.HasPrefix(unit, NextGenSKUBackupStorage) && !isManualBackupStorageSKU(unit)) ||
		strings.HasPrefix(unit, NextGenSKUDiskStorageSize) ||
		strings.HasPrefix(unit, NextGenSKUServerlessRowStorage) ||
		strings.HasPrefix(unit, NextGenSKUServerlessColumnStorage) ||
		strings.HasPrefix(unit, NextGenSKUServerlessRowStorageDualEncryption) ||
		strings.HasPrefix(unit, NextGenSKUServerlessColumnStorageDualEncryption)
}

func isManualBackupStorageSKU(unit string) bool {
	parts := strings.Split(unit, ":")
	return len(parts) == 5 &&
		parts[0] == NextGenSKUBackupStorage &&
		parts[4] == NextGenBackupTypeManual
}
