package mongo

import (
	"context"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/mongo/readpref"
)

// ConnectionConfig holds MongoDB connection configuration
type ConnectionConfig struct {
	URI                    string        `json:"uri"`
	Database               string        `json:"database"`
	MaxPoolSize            uint64        `json:"maxPoolSize"`
	MinPoolSize            uint64        `json:"minPoolSize"`
	MaxConnIdleTime        time.Duration `json:"maxConnIdleTime"`
	ConnectTimeout         time.Duration `json:"connectTimeout"`
	ServerSelectionTimeout time.Duration `json:"serverSelectionTimeout"`
}

// ConnectionManager manages MongoDB connections and collections
type ConnectionManager struct {
	client   *mongo.Client
	database *mongo.Database
	logger   Logger
	config   ConnectionConfig
}

// NewConnectionManager creates a new MongoDB connection manager
func NewConnectionManager(config ConnectionConfig, logger Logger) (*ConnectionManager, error) {
	// Set default values
	if config.MaxPoolSize == 0 {
		config.MaxPoolSize = 100
	}
	if config.MinPoolSize == 0 {
		config.MinPoolSize = 5
	}
	if config.MaxConnIdleTime == 0 {
		config.MaxConnIdleTime = 30 * time.Minute
	}
	if config.ConnectTimeout == 0 {
		config.ConnectTimeout = 10 * time.Second
	}
	if config.ServerSelectionTimeout == 0 {
		config.ServerSelectionTimeout = 5 * time.Second
	}

	// Create client options
	clientOptions := options.Client().
		ApplyURI(config.URI).
		SetMaxPoolSize(config.MaxPoolSize).
		SetMinPoolSize(config.MinPoolSize).
		SetMaxConnIdleTime(config.MaxConnIdleTime).
		SetConnectTimeout(config.ConnectTimeout).
		SetServerSelectionTimeout(config.ServerSelectionTimeout)

	// Connect to MongoDB
	ctx, cancel := context.WithTimeout(context.Background(), config.ConnectTimeout)
	defer cancel()

	client, err := mongo.Connect(ctx, clientOptions)
	if err != nil {
		logger.Error("Failed to connect to MongoDB", map[string]interface{}{
			"uri":   config.URI,
			"error": err.Error(),
		})
		return nil, fmt.Errorf("failed to connect to MongoDB: %w", err)
	}

	// Ping the database
	if err = client.Ping(ctx, readpref.Primary()); err != nil {
		logger.Error("Failed to ping MongoDB", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("failed to ping MongoDB: %w", err)
	}

	database := client.Database(config.Database)

	logger.Info("Successfully connected to MongoDB", map[string]interface{}{
		"database": config.Database,
		"uri":      config.URI,
	})

	return &ConnectionManager{
		client:   client,
		database: database,
		logger:   logger,
		config:   config,
	}, nil
}

// GetCollection returns a MongoDB collection
func (cm *ConnectionManager) GetCollection(name string) *mongo.Collection {
	return cm.database.Collection(name)
}

// CreateRepository creates a new repository for a given collection
func (cm *ConnectionManager) CreateRepository(collectionName string) *mongo.Collection {
	return cm.GetCollection(collectionName)
}

// CreateIndexes creates indexes for a collection
func (cm *ConnectionManager) CreateIndexes(collectionName string, indexes []Index) error {
	collection := cm.GetCollection(collectionName)

	for _, index := range indexes {
		indexModel := mongo.IndexModel{
			Keys:    index.Keys,
			Options: index.Options,
		}

		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		_, err := collection.Indexes().CreateOne(ctx, indexModel)
		if err != nil {
			cm.logger.Error("Failed to create index", map[string]interface{}{
				"collection": collectionName,
				"index":      index.Name,
				"error":      err.Error(),
			})
			return fmt.Errorf("failed to create index %s: %w", index.Name, err)
		}

		cm.logger.Info("Index created successfully", map[string]interface{}{
			"collection": collectionName,
			"index":      index.Name,
		})
	}

	return nil
}

// DropIndexes drops indexes from a collection
func (cm *ConnectionManager) DropIndexes(collectionName string, indexNames []string) error {
	collection := cm.GetCollection(collectionName)

	for _, indexName := range indexNames {
		ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
		defer cancel()

		_, err := collection.Indexes().DropOne(ctx, indexName)
		if err != nil {
			cm.logger.Error("Failed to drop index", map[string]interface{}{
				"collection": collectionName,
				"index":      indexName,
				"error":      err.Error(),
			})
			return fmt.Errorf("failed to drop index %s: %w", indexName, err)
		}

		cm.logger.Info("Index dropped successfully", map[string]interface{}{
			"collection": collectionName,
			"index":      indexName,
		})
	}

	return nil
}

// ListIndexes lists all indexes for a collection
func (cm *ConnectionManager) ListIndexes(collectionName string) ([]bson.M, error) {
	collection := cm.GetCollection(collectionName)

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	cursor, err := collection.Indexes().List(ctx)
	if err != nil {
		cm.logger.Error("Failed to list indexes", map[string]interface{}{
			"collection": collectionName,
			"error":      err.Error(),
		})
		return nil, fmt.Errorf("failed to list indexes: %w", err)
	}
	defer cursor.Close(ctx)

	var indexes []bson.M
	if err = cursor.All(ctx, &indexes); err != nil {
		cm.logger.Error("Failed to decode indexes", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("failed to decode indexes: %w", err)
	}

	return indexes, nil
}

// Close closes the MongoDB connection
func (cm *ConnectionManager) Close(ctx context.Context) error {
	if err := cm.client.Disconnect(ctx); err != nil {
		cm.logger.Error("Failed to disconnect from MongoDB", map[string]interface{}{
			"error": err.Error(),
		})
		return fmt.Errorf("failed to disconnect from MongoDB: %w", err)
	}

	cm.logger.Info("Successfully disconnected from MongoDB", nil)
	return nil
}

// Ping pings the MongoDB server
func (cm *ConnectionManager) Ping(ctx context.Context) error {
	if err := cm.client.Ping(ctx, readpref.Primary()); err != nil {
		cm.logger.Error("Failed to ping MongoDB", map[string]interface{}{
			"error": err.Error(),
		})
		return fmt.Errorf("failed to ping MongoDB: %w", err)
	}

	return nil
}

// GetStats returns database statistics
func (cm *ConnectionManager) GetStats(ctx context.Context) (bson.M, error) {
	stats := cm.database.RunCommand(ctx, bson.M{"dbStats": 1})
	if stats.Err() != nil {
		cm.logger.Error("Failed to get database stats", map[string]interface{}{
			"error": stats.Err().Error(),
		})
		return nil, fmt.Errorf("failed to get database stats: %w", stats.Err())
	}

	var result bson.M
	if err := stats.Decode(&result); err != nil {
		cm.logger.Error("Failed to decode database stats", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("failed to decode database stats: %w", err)
	}

	return result, nil
}

// GetCollectionStats returns collection statistics
func (cm *ConnectionManager) GetCollectionStats(ctx context.Context, collectionName string) (bson.M, error) {
	collection := cm.GetCollection(collectionName)

	stats := collection.Database().RunCommand(ctx, bson.M{
		"collStats": collectionName,
	})
	if stats.Err() != nil {
		cm.logger.Error("Failed to get collection stats", map[string]interface{}{
			"collection": collectionName,
			"error":      stats.Err().Error(),
		})
		return nil, fmt.Errorf("failed to get collection stats: %w", stats.Err())
	}

	var result bson.M
	if err := stats.Decode(&result); err != nil {
		cm.logger.Error("Failed to decode collection stats", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, fmt.Errorf("failed to decode collection stats: %w", err)
	}

	return result, nil
}
