package main

import (
	"context"
	"database/sql/driver"
	"encoding/json"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	log "github.com/sirupsen/logrus"
	"github.com/tidbcloud/metering/pkg/common/models"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

// var networksqltmpl = "SELECT * FROM billing_report_records WHERE id > ? AND project_metering_id > 0 AND tenant_id in (?) AND created_at >= ? AND created_at < ? order by id asc limit ? ;"
var sqltmpl = "SELECT * FROM billing_report_records WHERE id > ? "

const (
	datapath   = "data"
	resultpath = "result"
)

type Source struct {
	config *SupplementaryConfig
}

func NewSource(config *SupplementaryConfig) *Source {
	return &Source{config: config}
}

func (s *Source) buildSQL() string {
	var builder strings.Builder
	builder.WriteString(sqltmpl)
	if len(s.config.TenantIDs) > 0 {
		tenantids := strings.Join(s.config.TenantIDs, ",")
		builder.WriteString(fmt.Sprintf("AND tenant_id in (%s) ", tenantids))
	}
	switch s.config.MeteringType {
	case MeteringTypeNetwork:
		{
			builder.WriteString("AND project_metering_id > 0 ")
		}
	case MeteringTypeInstance:
		{
			builder.WriteString("AND cluster_metering_id > 0 ")
		}
	}
	builder.WriteString("AND created_at >= ? AND created_at < ? order by id asc limit ? ;")
	return builder.String()
}

func (s *Source) LoadMeteringFromMysql(ctx context.Context) error {
	dsn := s.config.SourceMysql
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	if err != nil {
		panic(err)
	}
	// start at startTime, end at now time. include more data.
	createTimeEnd := time.Now().Unix()
	createTimeStart := s.config.StartTime
	cursor := uint64(0)
	sql := s.buildSQL()
	if s.config.BatchSize <= 0 {
		s.config.BatchSize = 1000
	}
	var listToReport []*models.BillingReport
	for {
		var records []BillingReportRecord
		db.Raw(sql,
			cursor,
			time.Unix(createTimeStart, 0).UTC().Format(time.DateTime),
			time.Unix(createTimeEnd, 0).UTC().Format(time.DateTime),
			s.config.BatchSize).Scan(&records)

		log.Infof("get %d records, cursor = %d", len(records), cursor)
		for _, record := range records {
			for _, item := range record.Data.Items {
				if item.CostTimeStamp < s.config.StartTime || item.CostTimeStamp > s.config.EndTime {
					log.Infof("skip item, cost_id = %s, cost_timestamp = %d", item.CostID, item.CostTimeStamp)
					continue
				}

				labelsJson, err := json.Marshal(item.Labels)
				if err != nil {
					log.Panicf("marshal item.labels failed. %s", err.Error())
				}

				listToReport = append(listToReport, &models.BillingReport{
					CreatedAt:     record.CreatedAt.Unix(),
					TenantID:      record.TenantID,
					CostID:        item.CostID,
					Cost:          item.Cost,
					CostTimestamp: item.CostTimeStamp,
					Labels:        string(labelsJson),
					Unit:          item.SKU,
				})
			}
		}

		// if mode == DryRunMode {
		// 	jsonData, err := json.Marshal(listToReport)
		// 	if err != nil {
		// 		log.Panicf("marshal reports failed. %s", err.Error())
		// 	}
		// 	log.Infof("dry run %d reports", len(listToReport))
		// 	log.Infof("%s", jsonData)
		// } else if mode == RePushMode {
		// 	for _, report := range listToReport {
		// 		e := facade.GetMeasureSender(ctx).SendMeasure(ctx, *report)
		// 		if e != nil {
		// 			log.Panicf("SendMeasure failed. %s", err.Error())
		// 		}
		// 		log.Infof("repush data succeed, cost_id = %s", report.CostID)
		// 		jsonData, err := json.Marshal(report)
		// 		if err != nil {
		// 			log.Panicf("marshal report failed. %s", err.Error())
		// 		}
		// 		log.Infof("%s", jsonData)
		// 	}
		// } else {
		// 	log.Panicf("unsupported mode %d", mode)
		// }

		if len(records) < int(s.config.BatchSize) {
			break
		}
		cursor = records[len(records)-1].ID
		time.Sleep(time.Second)
	}
	//save to sqlite
	dir := fmt.Sprintf("%s/%s", datapath, resultpath)
	err = os.MkdirAll(dir, 0755)
	if err != nil {
		log.Panicf("mkdir failed. %s", err.Error())
	}
	billingReportFile := fmt.Sprintf("%s/%d-%d.%s", dir, s.config.StartTime, s.config.EndTime, "sqlite")
	// Remove the file if it exists, ignore error if file doesn't exist
	_ = os.Remove(billingReportFile)
	billingReportDB, err := GetBillingReportDB(billingReportFile, OldReporter)
	if err != nil {
		log.Panicf("get billing report db failed. %s", err.Error())
	}

	err = billingReportDB.SaveBillingReports(ctx, listToReport)
	if err != nil {
		return err
	}
	log.Infoln("get metering repoter from mysql succeed")
	return nil
}

func (s *Source) LoadMeteringFromObjectStorage(ctx context.Context) error {
	cli, err := NewBillingRepoterObjectStorage(s.config).Init()
	if err != nil {
		return err
	}
	//save to sqlite
	dir := fmt.Sprintf("%s/%s", datapath, resultpath)
	err = os.MkdirAll(dir, 0755)
	if err != nil {
		log.Panicf("mkdir failed. %s", err.Error())
	}
	billingReportFile := fmt.Sprintf("%s/%d-%d.%s", dir, s.config.StartTime, s.config.EndTime, "sqlite")
	// Remove the file if it exists, ignore error if file doesn't exist
	_ = os.Remove(billingReportFile)
	billingReportDB, err := GetBillingReportDB(billingReportFile, s.config.ReporterType)
	if err != nil {
		log.Panicf("get billing report db failed. %s", err.Error())
	}
	//for network metering, object like 20241027.db,the start and end time like 20241027 00:00:00(UTC) - 20241028 00:00:00(UTC)
	if s.config.MeteringType == MeteringTypeNetwork {
		cursorTime := s.config.StartTime
		for ; cursorTime <= s.config.EndTime; cursorTime += 24 * 3600 {
			res, resv2, err := cli.LoadNetworkRepoterWithTs(ctx, cursorTime)
			if err != nil {
				return err
			}
			switch s.config.ReporterType {
			case V2Reporter:
				filtered := filterReporter(resv2, s.config.TenantIDs)
				err = billingReportDB.SaveBillingReportsV2(ctx, filtered)
				if err != nil {
					return err
				}
			case V1Reporter:
				filtered := filterReporter(res, s.config.TenantIDs)
				err = billingReportDB.SaveBillingReports(ctx, filtered)
				if err != nil {
					return err
				}
			case OldReporter:
				filtered := filterReporter(res, s.config.TenantIDs)
				err = billingReportDB.SaveBillingReports(ctx, filtered)
				if err != nil {
					return err
				}
			}
		}
		log.Infof("get network metering repoter from object storage succeed")
		return nil
	} else if s.config.MeteringType == MeteringTypeInstance {
		cursorTime := s.config.StartTime
		for ; cursorTime <= s.config.EndTime; cursorTime += 60 {
			res, resv2, err := cli.LoadInstanceRepoterWithTs(ctx, cursorTime)
			if err != nil {
				return err
			}
			switch s.config.ReporterType {
			case V2Reporter:
				filtered := filterReporter(resv2, s.config.TenantIDs)
				err = billingReportDB.SaveBillingReportsV2(ctx, filtered)
				if err != nil {
					return err
				}
			case V1Reporter:
				filtered := filterReporter(res, s.config.TenantIDs)
				err = billingReportDB.SaveBillingReports(ctx, filtered)
				if err != nil {
					return err
				}
			case OldReporter:
				filtered := filterReporter(res, s.config.TenantIDs)
				err = billingReportDB.SaveBillingReports(ctx, filtered)
				if err != nil {
					return err
				}
			}
		}
		log.Infof("get instance metering repoter from object storage succeed")
		return nil
	}
	log.Panicf("unsupported metering type %s", s.config.MeteringType)
	return nil
}

func filterReporter[T models.BilingTenanter](reporter []T, tenantIDs []string) []T {
	if len(tenantIDs) == 0 {
		return reporter
	}

	// Convert tenantIDs from string to uint64
	tenantIDMap := make(map[uint64]struct{})
	for _, id := range tenantIDs {
		if idInt, err := strconv.ParseUint(id, 10, 64); err == nil {
			tenantIDMap[idInt] = struct{}{}
		}
	}

	filtered := make([]T, 0, len(reporter))
	for _, r := range reporter {
		if _, exists := tenantIDMap[r.GetTenantID()]; exists {
			filtered = append(filtered, r)
		}
	}
	return filtered
}

type BillingReportRecord struct {
	ID                uint64             `gorm:"primary_key;not null"`
	CreatedAt         time.Time          `gorm:"created_at;type:TIMESTAMP;not null;<-:create"`
	UpdatedAt         time.Time          `gorm:"updated_at;type:TIMESTAMP"`
	Data              MeteringReportData `gorm:"type:JSON"`
	Status            string             `gorm:"not null;type:ENUM('synced', 'unsynced')"`
	ProjectMeteringID uint64             `gorm:"not null;default '0';index"`
	ClusterMeteringID uint64             `gorm:"not null;default '0';index"`
	TenantID          uint64             `gorm:"not null;index"`
}

type MeteringReportData struct {
	Items []*MeteringReportDataItem `json:"items"`
}

func (m MeteringReportData) Value() (driver.Value, error) {
	b, err := json.Marshal(m)
	return string(b), err
}

func (m *MeteringReportData) Scan(input interface{}) error {
	return json.Unmarshal(input.([]byte), m)
}

type MeteringReportDataItem struct {
	CostID        string            `json:"cost_id"`
	SKU           string            `json:"sku"`
	CostTimeStamp int64             `json:"cost_time_stamp"`
	Cost          int64             `json:"cost"`
	Labels        map[string]string `json:"labels"`
	Memo          string            `json:"memo"`
}
