package namespace

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/DeBrosOfficial/network/pkg/client"
	"github.com/DeBrosOfficial/network/pkg/rqlite"
	"github.com/google/uuid"
	"go.uber.org/zap"
)

// NamespacePortAllocator manages the reserved port range (10000-10099) for namespace services.
// Each namespace instance on a node gets a block of 5 consecutive ports.
type NamespacePortAllocator struct {
	db     rqlite.Client
	logger *zap.Logger
}

// NewNamespacePortAllocator creates a new port allocator
func NewNamespacePortAllocator(db rqlite.Client, logger *zap.Logger) *NamespacePortAllocator {
	return &NamespacePortAllocator{
		db:     db,
		logger: logger.With(zap.String("component", "namespace-port-allocator")),
	}
}

// AllocatePortBlock finds and allocates the next available 5-port block on a node.
// Returns an error if the node is at capacity (20 namespace instances).
func (npa *NamespacePortAllocator) AllocatePortBlock(ctx context.Context, nodeID, namespaceClusterID string) (*PortBlock, error) {
	internalCtx := client.WithInternalAuth(ctx)

	// Check if allocation already exists for this namespace on this node
	existingBlock, err := npa.GetPortBlock(ctx, namespaceClusterID, nodeID)
	if err == nil && existingBlock != nil {
		npa.logger.Debug("Port block already allocated",
			zap.String("node_id", nodeID),
			zap.String("namespace_cluster_id", namespaceClusterID),
			zap.Int("port_start", existingBlock.PortStart),
		)
		return existingBlock, nil
	}

	// Retry logic for handling concurrent allocation conflicts
	maxRetries := 10
	retryDelay := 100 * time.Millisecond

	for attempt := 0; attempt < maxRetries; attempt++ {
		block, err := npa.tryAllocatePortBlock(internalCtx, nodeID, namespaceClusterID)
		if err == nil {
			npa.logger.Info("Port block allocated successfully",
				zap.String("node_id", nodeID),
				zap.String("namespace_cluster_id", namespaceClusterID),
				zap.Int("port_start", block.PortStart),
				zap.Int("attempt", attempt+1),
			)
			return block, nil
		}

		// If it's a conflict error, retry with exponential backoff
		if isConflictError(err) {
			npa.logger.Debug("Port allocation conflict, retrying",
				zap.String("node_id", nodeID),
				zap.String("namespace_cluster_id", namespaceClusterID),
				zap.Int("attempt", attempt+1),
				zap.Error(err),
			)
			time.Sleep(retryDelay)
			retryDelay *= 2
			continue
		}

		// Other errors are non-retryable
		return nil, err
	}

	return nil, &ClusterError{
		Message: fmt.Sprintf("failed to allocate port block after %d retries", maxRetries),
	}
}

// tryAllocatePortBlock attempts to allocate a port block (single attempt)
func (npa *NamespacePortAllocator) tryAllocatePortBlock(ctx context.Context, nodeID, namespaceClusterID string) (*PortBlock, error) {
	// In dev environments where all nodes share the same IP, we need to track
	// allocations by IP address to avoid port conflicts. First get this node's IP.
	var nodeInfos []struct {
		IPAddress string `db:"ip_address"`
	}
	nodeQuery := `SELECT ip_address FROM dns_nodes WHERE id = ? LIMIT 1`
	if err := npa.db.Query(ctx, &nodeInfos, nodeQuery, nodeID); err != nil || len(nodeInfos) == 0 {
		// Fallback: if we can't get the IP, allocate per node_id only
		npa.logger.Debug("Could not get node IP, falling back to node_id-only allocation",
			zap.String("node_id", nodeID),
		)
	}

	// Query all allocated port blocks. If nodes share the same IP, we need to
	// check allocations by IP address to prevent port conflicts.
	type portRow struct {
		PortStart int `db:"port_start"`
	}

	var allocatedBlocks []portRow
	var query string
	var err error

	if len(nodeInfos) > 0 && nodeInfos[0].IPAddress != "" {
		// Check if other nodes share this IP - if so, allocate globally by IP
		var sameIPCount []struct {
			Count int `db:"count"`
		}
		countQuery := `SELECT COUNT(DISTINCT id) as count FROM dns_nodes WHERE ip_address = ?`
		if err := npa.db.Query(ctx, &sameIPCount, countQuery, nodeInfos[0].IPAddress); err == nil && len(sameIPCount) > 0 && sameIPCount[0].Count > 1 {
			// Multiple nodes share this IP (dev environment) - allocate globally
			query = `
				SELECT npa.port_start
				FROM namespace_port_allocations npa
				JOIN dns_nodes dn ON npa.node_id = dn.id
				WHERE dn.ip_address = ?
				ORDER BY npa.port_start ASC
			`
			err = npa.db.Query(ctx, &allocatedBlocks, query, nodeInfos[0].IPAddress)
			npa.logger.Debug("Multiple nodes share IP, allocating globally",
				zap.String("ip_address", nodeInfos[0].IPAddress),
				zap.Int("same_ip_nodes", sameIPCount[0].Count),
			)
		} else {
			// Single node per IP (production) - allocate per node
			query = `SELECT port_start FROM namespace_port_allocations WHERE node_id = ? ORDER BY port_start ASC`
			err = npa.db.Query(ctx, &allocatedBlocks, query, nodeID)
		}
	} else {
		// No IP info - allocate per node_id
		query = `SELECT port_start FROM namespace_port_allocations WHERE node_id = ? ORDER BY port_start ASC`
		err = npa.db.Query(ctx, &allocatedBlocks, query, nodeID)
	}

	if err != nil {
		return nil, &ClusterError{
			Message: "failed to query allocated ports",
			Cause:   err,
		}
	}

	// Build map of allocated block starts
	allocatedStarts := make(map[int]bool)
	for _, row := range allocatedBlocks {
		allocatedStarts[row.PortStart] = true
	}

	// Check node capacity
	if len(allocatedBlocks) >= MaxNamespacesPerNode {
		return nil, ErrNodeAtCapacity
	}

	// Find first available port block
	portStart := -1
	for start := NamespacePortRangeStart; start <= NamespacePortRangeEnd-PortsPerNamespace+1; start += PortsPerNamespace {
		if !allocatedStarts[start] {
			portStart = start
			break
		}
	}

	if portStart < 0 {
		return nil, ErrNoPortsAvailable
	}

	// Create port block
	block := &PortBlock{
		ID:                  uuid.New().String(),
		NodeID:              nodeID,
		NamespaceClusterID:  namespaceClusterID,
		PortStart:           portStart,
		PortEnd:             portStart + PortsPerNamespace - 1,
		RQLiteHTTPPort:      portStart + 0,
		RQLiteRaftPort:      portStart + 1,
		OlricHTTPPort:       portStart + 2,
		OlricMemberlistPort: portStart + 3,
		GatewayHTTPPort:     portStart + 4,
		AllocatedAt:         time.Now(),
	}

	// Attempt to insert allocation record
	insertQuery := `
		INSERT INTO namespace_port_allocations (
			id, node_id, namespace_cluster_id, port_start, port_end,
			rqlite_http_port, rqlite_raft_port, olric_http_port, olric_memberlist_port, gateway_http_port,
			allocated_at
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
	`
	_, err = npa.db.Exec(ctx, insertQuery,
		block.ID,
		block.NodeID,
		block.NamespaceClusterID,
		block.PortStart,
		block.PortEnd,
		block.RQLiteHTTPPort,
		block.RQLiteRaftPort,
		block.OlricHTTPPort,
		block.OlricMemberlistPort,
		block.GatewayHTTPPort,
		block.AllocatedAt,
	)
	if err != nil {
		return nil, &ClusterError{
			Message: "failed to insert port allocation",
			Cause:   err,
		}
	}

	return block, nil
}

// DeallocatePortBlock releases a port block when a namespace is deprovisioned
func (npa *NamespacePortAllocator) DeallocatePortBlock(ctx context.Context, namespaceClusterID, nodeID string) error {
	internalCtx := client.WithInternalAuth(ctx)

	query := `DELETE FROM namespace_port_allocations WHERE namespace_cluster_id = ? AND node_id = ?`
	_, err := npa.db.Exec(internalCtx, query, namespaceClusterID, nodeID)
	if err != nil {
		return &ClusterError{
			Message: "failed to deallocate port block",
			Cause:   err,
		}
	}

	npa.logger.Info("Port block deallocated",
		zap.String("namespace_cluster_id", namespaceClusterID),
		zap.String("node_id", nodeID),
	)

	return nil
}

// DeallocateAllPortBlocks releases all port blocks for a namespace cluster
func (npa *NamespacePortAllocator) DeallocateAllPortBlocks(ctx context.Context, namespaceClusterID string) error {
	internalCtx := client.WithInternalAuth(ctx)

	query := `DELETE FROM namespace_port_allocations WHERE namespace_cluster_id = ?`
	_, err := npa.db.Exec(internalCtx, query, namespaceClusterID)
	if err != nil {
		return &ClusterError{
			Message: "failed to deallocate all port blocks",
			Cause:   err,
		}
	}

	npa.logger.Info("All port blocks deallocated",
		zap.String("namespace_cluster_id", namespaceClusterID),
	)

	return nil
}

// GetPortBlock retrieves the port block for a namespace on a specific node
func (npa *NamespacePortAllocator) GetPortBlock(ctx context.Context, namespaceClusterID, nodeID string) (*PortBlock, error) {
	internalCtx := client.WithInternalAuth(ctx)

	var blocks []PortBlock
	query := `
		SELECT id, node_id, namespace_cluster_id, port_start, port_end,
			   rqlite_http_port, rqlite_raft_port, olric_http_port, olric_memberlist_port, gateway_http_port,
			   allocated_at
		FROM namespace_port_allocations
		WHERE namespace_cluster_id = ? AND node_id = ?
		LIMIT 1
	`
	err := npa.db.Query(internalCtx, &blocks, query, namespaceClusterID, nodeID)
	if err != nil {
		return nil, &ClusterError{
			Message: "failed to query port block",
			Cause:   err,
		}
	}

	if len(blocks) == 0 {
		return nil, nil
	}

	return &blocks[0], nil
}

// GetAllPortBlocks retrieves all port blocks for a namespace cluster
func (npa *NamespacePortAllocator) GetAllPortBlocks(ctx context.Context, namespaceClusterID string) ([]PortBlock, error) {
	internalCtx := client.WithInternalAuth(ctx)

	var blocks []PortBlock
	query := `
		SELECT id, node_id, namespace_cluster_id, port_start, port_end,
			   rqlite_http_port, rqlite_raft_port, olric_http_port, olric_memberlist_port, gateway_http_port,
			   allocated_at
		FROM namespace_port_allocations
		WHERE namespace_cluster_id = ?
		ORDER BY port_start ASC
	`
	err := npa.db.Query(internalCtx, &blocks, query, namespaceClusterID)
	if err != nil {
		return nil, &ClusterError{
			Message: "failed to query port blocks",
			Cause:   err,
		}
	}

	return blocks, nil
}

// GetNodeCapacity returns how many more namespace instances a node can host
func (npa *NamespacePortAllocator) GetNodeCapacity(ctx context.Context, nodeID string) (int, error) {
	internalCtx := client.WithInternalAuth(ctx)

	type countResult struct {
		Count int `db:"count"`
	}

	var results []countResult
	query := `SELECT COUNT(*) as count FROM namespace_port_allocations WHERE node_id = ?`
	err := npa.db.Query(internalCtx, &results, query, nodeID)
	if err != nil {
		return 0, &ClusterError{
			Message: "failed to count allocated port blocks",
			Cause:   err,
		}
	}

	if len(results) == 0 {
		return MaxNamespacesPerNode, nil
	}

	allocated := results[0].Count
	available := MaxNamespacesPerNode - allocated

	if available < 0 {
		available = 0
	}

	return available, nil
}

// GetNodeAllocationCount returns the number of namespace instances on a node
func (npa *NamespacePortAllocator) GetNodeAllocationCount(ctx context.Context, nodeID string) (int, error) {
	internalCtx := client.WithInternalAuth(ctx)

	type countResult struct {
		Count int `db:"count"`
	}

	var results []countResult
	query := `SELECT COUNT(*) as count FROM namespace_port_allocations WHERE node_id = ?`
	err := npa.db.Query(internalCtx, &results, query, nodeID)
	if err != nil {
		return 0, &ClusterError{
			Message: "failed to count allocated port blocks",
			Cause:   err,
		}
	}

	if len(results) == 0 {
		return 0, nil
	}

	return results[0].Count, nil
}

// isConflictError checks if an error is due to a constraint violation
func isConflictError(err error) bool {
	if err == nil {
		return false
	}
	errStr := err.Error()
	return strings.Contains(errStr, "UNIQUE") || strings.Contains(errStr, "constraint") || strings.Contains(errStr, "conflict")
}
