package knowledge

import (
	"context"
	"fmt"
	"log"

	"chat-switchboard/models"
	"chat-switchboard/roles"
	"chat-switchboard/store"
)

// ── Configuration ────────────────────────────

const (
	// DefaultBatchSize is the max chunks per embedding API call.
	// Most providers handle 100 inputs per request comfortably.
	DefaultBatchSize = 100

	// MaxVectorDim is the column width in pgvector (vector(3072)).
	// Vectors shorter than this are zero-padded on storage.
	MaxVectorDim = 3072
)

// ── Embedder ─────────────────────────────────

// Embedder generates vector embeddings for text chunks using the
// embedding role resolver. It handles batching and dimension
// normalization (zero-padding to MaxVectorDim).
type Embedder struct {
	resolver  *roles.Resolver
	stores    store.Stores // optional — for usage tracking
	batchSize int
}

// NewEmbedder creates an embedder that dispatches through the role resolver.
func NewEmbedder(resolver *roles.Resolver) *Embedder {
	return &Embedder{
		resolver:  resolver,
		batchSize: DefaultBatchSize,
	}
}

// WithStores attaches stores for usage tracking. Returns self for chaining.
func (e *Embedder) WithStores(s store.Stores) *Embedder {
	e.stores = s
	return e
}

// EmbedResult holds the output of a batch embedding operation.
type EmbedResult struct {
	Vectors       [][]float64 // one per input chunk, zero-padded to MaxVectorDim
	Model         string      // model that produced the embeddings
	Dimensions    int         // native dimension before padding
	InputTokens   int         // total tokens consumed across all batches
	ConfigID      string      // provider config that was used
	ProviderScope string      // "personal", "team", or "global"
}

// EmbedChunks generates embeddings for a slice of text chunks.
// Chunks are batched in groups of batchSize to avoid provider limits.
// Uses the embedding role resolution chain: personal → team → global.
//
// Returns vectors in the same order as input chunks.
func (e *Embedder) EmbedChunks(ctx context.Context, userID string, teamID *string, texts []string) (*EmbedResult, error) {
	if len(texts) == 0 {
		return &EmbedResult{Vectors: [][]float64{}}, nil
	}

	// Verify embedding role is configured before starting
	cfg, err := e.resolver.GetConfig(ctx, roles.RoleEmbedding, userID, teamID)
	if err != nil {
		return nil, fmt.Errorf("embedding role not configured: %w", err)
	}
	if cfg.Primary == nil {
		return nil, fmt.Errorf("embedding role has no primary binding")
	}

	allVectors := make([][]float64, 0, len(texts))
	var model string
	var nativeDim int
	var configID, provScope string
	totalTokens := 0

	for i := 0; i < len(texts); i += e.batchSize {
		end := i + e.batchSize
		if end > len(texts) {
			end = len(texts)
		}
		batch := texts[i:end]

		log.Printf("  embed batch %d/%d (%d chunks)",
			(i/e.batchSize)+1, (len(texts)+e.batchSize-1)/e.batchSize, len(batch))

		result, err := e.resolver.Embed(ctx, roles.RoleEmbedding, userID, teamID, batch)
		if err != nil {
			return nil, fmt.Errorf("embed batch starting at chunk %d: %w", i, err)
		}

		if len(result.Embeddings) != len(batch) {
			return nil, fmt.Errorf("embed batch %d: expected %d vectors, got %d",
				i/e.batchSize, len(batch), len(result.Embeddings))
		}

		// Capture model info from first batch
		if model == "" {
			model = result.Model
			configID = result.ConfigID
			provScope = result.ProviderScope
			if len(result.Embeddings) > 0 {
				nativeDim = len(result.Embeddings[0])
			}
		}

		totalTokens += result.InputTokens
		allVectors = append(allVectors, result.Embeddings...)
	}

	// Zero-pad to MaxVectorDim for uniform storage
	for i, vec := range allVectors {
		allVectors[i] = padVector(vec, MaxVectorDim)
	}

	return &EmbedResult{
		Vectors:       allVectors,
		Model:         model,
		Dimensions:    nativeDim,
		InputTokens:   totalTokens,
		ConfigID:      configID,
		ProviderScope: provScope,
	}, nil
}

// LogUsage records an embedding usage entry. Silently no-ops if stores
// were not attached via WithStores or if there are no tokens to log.
func (e *Embedder) LogUsage(ctx context.Context, userID string, channelID *string, result *EmbedResult) {
	if e.stores.Usage == nil || result == nil || result.InputTokens == 0 {
		return
	}

	role := "embedding"
	entry := &models.UsageEntry{
		ChannelID:        channelID,
		UserID:           userID,
		ProviderConfigID: strPtr(result.ConfigID),
		ProviderScope:    result.ProviderScope,
		ModelID:          result.Model,
		Role:             &role,
		PromptTokens:     result.InputTokens,
		CompletionTokens: 0,
	}

	if err := e.stores.Usage.Log(ctx, entry); err != nil {
		log.Printf("⚠ Failed to log embedding usage: %v", err)
	}
}

// IsConfigured returns true if the embedding role has at least a primary binding.
func (e *Embedder) IsConfigured(ctx context.Context) bool {
	return e.resolver.IsConfigured(ctx, roles.RoleEmbedding)
}

// ── Helpers ──────────────────────────────────

// padVector zero-pads a vector to the target dimension.
// If the vector is already the target size or larger, it's truncated.
func padVector(vec []float64, targetDim int) []float64 {
	if len(vec) == targetDim {
		return vec
	}
	if len(vec) > targetDim {
		return vec[:targetDim]
	}
	padded := make([]float64, targetDim)
	copy(padded, vec)
	return padded
}

func strPtr(s string) *string {
	if s == "" {
		return nil
	}
	return &s
}
