package nextgenmetering

import (
	"context"
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/pingcap/metering_sdk/common"
	sdkconfig "github.com/pingcap/metering_sdk/config"
	"github.com/pingcap/metering_sdk/reader"
	metareader "github.com/pingcap/metering_sdk/reader/meta"
	meteringreader "github.com/pingcap/metering_sdk/reader/metering"
	"github.com/pingcap/metering_sdk/storage"
	"github.com/tidbcloud/metering/pkg/common/config"
	"github.com/tidbcloud/metering/pkg/common/constants"
	"go.uber.org/zap"
)

// MeteringLoader handles loading metering data using metering_sdk
type MeteringLoader struct {
	conf       *config.ServiceConfig
	storage    storage.ObjectStorageProvider
	reader     *meteringreader.MeteringReader
	metaReader *metareader.MetaReader
	sdkCfg     *sdkconfig.Config
	log        *zap.Logger
}

// NewMeteringLoader creates a new metering loader instance
func NewMeteringLoader(conf *config.ServiceConfig, log *zap.Logger) *MeteringLoader {
	return &MeteringLoader{
		conf: conf,
		log:  log.With(zap.String("module", "meteringloader")),
	}
}

// Init initializes the metering loader with storage provider and SDK components
func (m *MeteringLoader) Init() error {
	// Create storage provider based on vendor configuration
	storageConfig, err := m.createStorageConfig()
	if err != nil {
		return fmt.Errorf("failed to create storage config: %w", err)
	}

	// Create storage provider
	provider, err := storage.NewObjectStorageProvider(storageConfig)
	if err != nil {
		return fmt.Errorf("failed to create storage provider: %w", err)
	}
	m.storage = provider

	// Create SDK configuration
	m.sdkCfg = sdkconfig.DefaultConfig()
	m.sdkCfg = m.sdkCfg.WithLogger(m.log)

	// Create metering reader
	m.reader = meteringreader.NewMeteringReader(provider, m.sdkCfg)

	// Create meta reader with memory cache configuration (10MB)
	cacheConfig := &metareader.CacheConfig{
		Type:    metareader.CacheTypeMemory,
		MaxSize: 10 * 1024 * 1024, // 10MB
	}

	metaReaderConfig := &metareader.Config{
		Cache: cacheConfig,
	}

	metaReader, err := metareader.NewMetaReader(provider, m.sdkCfg, metaReaderConfig)
	if err != nil {
		return fmt.Errorf("failed to create meta reader: %w", err)
	}
	m.metaReader = metaReader

	return nil
}

// createStorageConfig creates storage configuration based on vendor
func (m *MeteringLoader) createStorageConfig() (*storage.ProviderConfig, error) {
	switch m.conf.Vendor {
	case constants.VendorAWS:
		providerCfg := &storage.ProviderConfig{
			Type:   storage.ProviderTypeS3,
			Bucket: m.conf.Bucket,
			Region: m.conf.Region,
			Prefix: m.conf.NextGenMeteringConfig.MeteringPrefix,
		}

		if m.conf.NextGenMeteringConfig != nil {
			if roleArn := strings.TrimSpace(m.conf.NextGenMeteringConfig.RoleArn); roleArn != "" {
				providerCfg.AWS = &storage.AWSConfig{AssumeRoleARN: roleArn}
			}
		}
		return providerCfg, nil
	case constants.VendorAlicloud:
		accessKey := strings.TrimSpace(os.Getenv(constants.AlicloudAccessKeyIDEnv))
		secretKey := strings.TrimSpace(os.Getenv(constants.AlicloudAccessKeySecretEnv))

		if accessKey == "" || secretKey == "" {
			return nil, fmt.Errorf("%s and %s environment variables are required for Alicloud",
				constants.AlicloudAccessKeyIDEnv, constants.AlicloudAccessKeySecretEnv)
		}

		if m.conf.NextGenMeteringConfig == nil {
			return nil, fmt.Errorf("NextGenMeteringConfig is required for Alicloud")
		}

		endpoint := strings.TrimSpace(m.conf.NextGenMeteringConfig.BucketEndpoint)
		if endpoint == "" {
			return nil, fmt.Errorf("bucketEndpoint is required in NextGenMeteringConfig for Alicloud")
		}

		return &storage.ProviderConfig{
			Type:     storage.ProviderTypeOSS,
			Bucket:   m.conf.Bucket,
			Region:   m.conf.Region,
			Prefix:   m.conf.NextGenMeteringConfig.MeteringPrefix,
			Endpoint: endpoint,
			OSS: &storage.OSSConfig{
				AccessKey:       accessKey,
				SecretAccessKey: secretKey,
			},
		}, nil
	case constants.VendorGCP:
		return &storage.ProviderConfig{
			Type:   storage.ProviderTypeGCS,
			Bucket: m.conf.Bucket,
			Prefix: m.conf.NextGenMeteringConfig.MeteringPrefix,
		}, nil
	case constants.VendorAzure:
		accountName := strings.TrimSpace(os.Getenv(constants.AzureStorageAccountNameEnv))
		if accountName == "" {
			return nil, fmt.Errorf("%s environment variable is required for Azure", constants.AzureStorageAccountNameEnv)
		}

		providerCfg := &storage.ProviderConfig{
			Type:   storage.ProviderTypeAzure,
			Bucket: m.conf.Bucket,
			Prefix: m.conf.NextGenMeteringConfig.MeteringPrefix,
			Azure: &storage.AzureConfig{
				AccountName: accountName,
			},
		}

		if accountKey := strings.TrimSpace(os.Getenv(constants.AzureStorageAccountKeyEnv)); accountKey != "" {
			providerCfg.Azure.AccountKey = accountKey
		}
		if sasToken := strings.TrimSpace(os.Getenv(constants.AzureStorageSASTokenEnv)); sasToken != "" {
			providerCfg.Azure.SASToken = sasToken
		}

		return providerCfg, nil
	default:
		// Default to local filesystem for development
		return &storage.ProviderConfig{
			Type: storage.ProviderTypeLocalFS,
			LocalFS: &storage.LocalFSConfig{
				BasePath:   "/tmp/nextgen-metering-data",
				CreateDirs: true,
			},
		}, nil
	}
}

// LoadMeteringData loads metering data for a specific timestamp
func (m *MeteringLoader) LoadMeteringData(ctx context.Context, timestamp int64) (*meteringreader.TimestampFiles, error) {
	if m.reader == nil {
		return nil, fmt.Errorf("metering loader not initialized")
	}

	m.log.Info("loading metering data", zap.Int64("timestamp", timestamp))

	timestampFiles, err := m.reader.ListFilesByTimestamp(ctx, timestamp)
	if err != nil {
		return nil, fmt.Errorf("failed to list files by timestamp: %w", err)
	}

	m.log.Info("found metering files",
		zap.Int64("timestamp", timestamp),
		zap.Int("file_count", len(timestampFiles.Files)))

	return timestampFiles, nil
}

// LoadMeteringDataByCategory loads metering data for a specific category and timestamp
func (m *MeteringLoader) LoadMeteringDataByCategory(ctx context.Context, timestamp int64, category string) ([]*common.MeteringData, error) {
	if m.reader == nil {
		return nil, fmt.Errorf("metering loader not initialized")
	}

	m.log.Info("loading metering data by category",
		zap.Int64("timestamp", timestamp),
		zap.String("category", category))

	// Get files for the specific category and timestamp
	filePaths, err := m.reader.GetFilesByCategory(ctx, timestamp, category)
	if err != nil {
		return nil, fmt.Errorf("failed to get files by category: %w", err)
	}

	if len(filePaths) == 0 {
		m.log.Info("no files found for category",
			zap.Int64("timestamp", timestamp),
			zap.String("category", category))
		return []*common.MeteringData{}, nil
	}

	// Read multiple files
	meteringData, err := m.reader.ReadMultipleFiles(ctx, filePaths)
	if err != nil {
		return nil, fmt.Errorf("failed to read metering files: %w", err)
	}

	m.log.Info("loaded metering data",
		zap.Int64("timestamp", timestamp),
		zap.String("category", category),
		zap.Int("record_count", len(meteringData)))

	return meteringData, nil
}

// LoadMeteringDataByTimestamp loads metering data for a specific timestamp
// This method loads all data for the given timestamp, supporting both single category and all categories
func (m *MeteringLoader) LoadMeteringDataByTimestamp(ctx context.Context, timestamp int64, category string) ([]*common.MeteringData, error) {
	if m.reader == nil {
		return nil, fmt.Errorf("metering loader not initialized")
	}

	m.log.Info("loading metering data for timestamp",
		zap.Int64("timestamp", timestamp),
		zap.String("category", category))

	// Use ListFilesByTimestamp to get all files for this specific timestamp
	timestampFiles, err := m.reader.ListFilesByTimestamp(ctx, timestamp)
	if err != nil {
		return nil, fmt.Errorf("failed to list files by timestamp: %w", err)
	}

	if len(timestampFiles.Files) == 0 {
		m.log.Info("no files found for timestamp",
			zap.Int64("timestamp", timestamp))
		return []*common.MeteringData{}, nil
	}

	var allData []*common.MeteringData

	// Process all categories if category is empty, or specific category if provided
	if category == "" {
		// Load data from all categories
		for cat, filePaths := range timestampFiles.Files {
			if len(filePaths) == 0 {
				continue
			}

			m.log.Debug("reading files for category",
				zap.String("category", cat),
				zap.Int("file_count", len(filePaths)))

			// Read multiple files for this category
			meteringData, err := m.reader.ReadMultipleFiles(ctx, filePaths)
			if err != nil {
				m.log.Warn("failed to read files for category",
					zap.String("category", cat),
					zap.Error(err))
				// Continue with other categories instead of failing completely
				continue
			}

			allData = append(allData, meteringData...)
		}
	} else {
		// Load data for specific category only
		filePaths, exists := timestampFiles.Files[category]
		if !exists || len(filePaths) == 0 {
			m.log.Info("no files found for category",
				zap.Int64("timestamp", timestamp),
				zap.String("category", category))
			return []*common.MeteringData{}, nil
		}

		m.log.Debug("reading files for specific category",
			zap.String("category", category),
			zap.Int("file_count", len(filePaths)))

		// Read multiple files for this category
		meteringData, err := m.reader.ReadMultipleFiles(ctx, filePaths)
		if err != nil {
			return nil, fmt.Errorf("failed to read files for category %s: %w", category, err)
		}

		allData = append(allData, meteringData...)
	}

	m.log.Info("completed loading metering data for timestamp",
		zap.Int64("timestamp", timestamp),
		zap.String("category", category),
		zap.Int("total_records", len(allData)))

	return allData, nil
}

// LoadMetaDataByTypeWithCategory loads metadata by specifying the MetaType and Category
func (m *MeteringLoader) LoadMetaDataByTypeWithCategory(ctx context.Context, clusterID string, metaType common.MetaType, category string, timestamp int64) (*common.MetaData, error) {
	if m.metaReader == nil {
		return nil, fmt.Errorf("metering loader not initialized")
	}

	m.log.Info("loading metadata by type with category",
		zap.String("cluster_id", clusterID),
		zap.String("meta_type", string(metaType)),
		zap.String("category", category),
		zap.Int64("timestamp", timestamp))

	metaData, err := m.metaReader.ReadByTypeWithCategory(ctx, clusterID, metaType, category, timestamp)
	if err != nil {
		// Import the reader package to access ErrFileNotFound
		if errors.Is(err, reader.ErrFileNotFound) {
			// Only return nil for file not found errors
			m.log.Debug("metadata file not found with category, returning nil",
				zap.String("cluster_id", clusterID),
				zap.String("meta_type", string(metaType)),
				zap.String("category", category),
				zap.Int64("timestamp", timestamp),
				zap.Error(err))
			return nil, nil
		}
		// Return other errors as-is
		m.log.Error("failed to load metadata by type with category",
			zap.String("cluster_id", clusterID),
			zap.String("meta_type", string(metaType)),
			zap.String("category", category),
			zap.Int64("timestamp", timestamp),
			zap.Error(err))
		return nil, fmt.Errorf("failed to load metadata by type with category: %w", err)
	}

	m.log.Info("loaded metadata by type with category",
		zap.String("cluster_id", clusterID),
		zap.String("meta_type", string(metaType)),
		zap.String("category", category),
		zap.Int64("timestamp", timestamp),
		zap.Int64("modify_ts", metaData.ModifyTS))

	return metaData, nil
}

// LoadMetaDataFile loads metadata from a specific file path
func (m *MeteringLoader) LoadMetaDataFile(ctx context.Context, path string) (interface{}, error) {
	if m.metaReader == nil {
		return nil, fmt.Errorf("metering loader not initialized")
	}

	m.log.Info("loading metadata file", zap.String("path", path))

	data, err := m.metaReader.ReadFile(ctx, path)
	if err != nil {
		return nil, fmt.Errorf("failed to read metadata file: %w", err)
	}

	m.log.Info("loaded metadata file", zap.String("path", path))

	return data, nil
}

// ListMetaDataFiles lists all metadata files under a specific prefix
func (m *MeteringLoader) ListMetaDataFiles(ctx context.Context, prefix string) ([]string, error) {
	if m.metaReader == nil {
		return nil, fmt.Errorf("metering loader not initialized")
	}

	m.log.Info("listing metadata files", zap.String("prefix", prefix))

	files, err := m.metaReader.List(ctx, prefix)
	if err != nil {
		return nil, fmt.Errorf("failed to list metadata files: %w", err)
	}

	m.log.Info("listed metadata files",
		zap.String("prefix", prefix),
		zap.Int("file_count", len(files)))

	return files, nil
}

// CreateMetaData creates a metadata structure
func (m *MeteringLoader) CreateMetaData(clusterID string, modifyTS int64, metadata map[string]interface{}) *common.MetaData {
	return &common.MetaData{
		ClusterID: clusterID,
		ModifyTS:  modifyTS,
		Metadata:  metadata,
	}
}

// CreateMeteringData creates a metering data structure from raw data
func (m *MeteringLoader) CreateMeteringData(selfID, category string, timestamp int64, data []map[string]interface{}) *common.MeteringData {
	// Ensure timestamp is minute-level aligned
	alignedTimestamp := timestamp / 60 * 60

	return &common.MeteringData{
		SelfID:    selfID,
		Timestamp: alignedTimestamp,
		Category:  category,
		Data:      data,
	}
}

// Close closes the loader and releases resources
func (m *MeteringLoader) Close() error {
	var errors []error

	if m.reader != nil {
		if err := m.reader.Close(); err != nil {
			errors = append(errors, fmt.Errorf("failed to close reader: %w", err))
		}
	}

	if m.metaReader != nil {
		if err := m.metaReader.Close(); err != nil {
			errors = append(errors, fmt.Errorf("failed to close meta reader: %w", err))
		}
	}

	if len(errors) > 0 {
		return fmt.Errorf("errors during close: %v", errors)
	}

	return nil
}

// GetCurrentTimestamp returns current timestamp aligned to minute level
func (m *MeteringLoader) GetCurrentTimestamp() int64 {
	return time.Now().Unix() / 60 * 60
}

// GetStorageProvider returns the underlying storage provider
// This is primarily used for creating writers in tests
func (m *MeteringLoader) GetStorageProvider() storage.ObjectStorageProvider {
	return m.storage
}
