package manifest

import (
	"fmt"
	"sort"
	"strconv"
	"strings"

	"github.com/ollama/ollama/x/imagegen/mlx"
)

// ManifestWeights provides fast weight loading from tensor blobs.
// Uses native mmap loading with synthetic safetensors headers for zero-copy.
type ManifestWeights struct {
	manifest    *ModelManifest
	component   string
	tensors     map[string]ManifestLayer // name -> layer
	cache       map[string]*mlx.Array    // name -> loaded array
	nativeCache []*mlx.SafetensorsFile   // keep native handles alive
	quantType   string                   // quantization type from blob metadata (e.g., "int4", "int8")
	groupSize   int                      // quantization group size from blob metadata
}

// LoadWeightsFromManifest creates a weight loader from manifest storage.
// If component is empty, loads all tensors (for LLM models).
// If component is specified, loads only tensors for that component and strips the prefix.
func LoadWeightsFromManifest(manifest *ModelManifest, component string) (*ManifestWeights, error) {
	layers := manifest.GetTensorLayers(component)
	if len(layers) == 0 {
		if component == "" {
			return nil, fmt.Errorf("no tensor layers found in manifest")
		}
		return nil, fmt.Errorf("no tensor layers found for component %q", component)
	}

	// Strip component prefix from tensor names for model loading
	// e.g., "text_encoder/model.embed_tokens.weight" -> "model.embed_tokens.weight"
	tensors := make(map[string]ManifestLayer, len(layers))
	for _, layer := range layers {
		if component == "" {
			tensors[layer.Name] = layer
		} else {
			tensorName := strings.TrimPrefix(layer.Name, component+"/")
			tensors[tensorName] = layer
		}
	}

	return &ManifestWeights{
		manifest:  manifest,
		component: component,
		tensors:   tensors,
		cache:     make(map[string]*mlx.Array),
	}, nil
}

// Load loads all tensor blobs using native mmap (zero-copy).
// Blobs are stored in safetensors format for native mlx_load_safetensors mmap.
// Combined quantized blobs contain tensors keyed by name, name+".scale", and optional name+".bias"
// with quantization metadata. Scale and bias are stored in cache as name+"_scale"
// and name+"_qbias" for compatibility with downstream loading code.
// Packed blobs (e.g., for expert groups) contain multiple tensors; the manifest name
// is a group prefix and individual tensors are loaded by their actual names from the blob.
// If dtype is non-zero, non-quantized tensors are converted to the specified dtype.
func (mw *ManifestWeights) Load(dtype mlx.Dtype) error {
	// Track native handles to free after batch eval
	nativeHandles := make([]*mlx.SafetensorsFile, 0, len(mw.tensors))
	arrays := make([]*mlx.Array, 0, len(mw.tensors))

	// Group tensors by digest to avoid loading the same blob multiple times
	type blobEntry struct {
		name  string
		layer ManifestLayer
	}
	blobGroups := make(map[string][]blobEntry)
	for name, layer := range mw.tensors {
		blobGroups[layer.Digest] = append(blobGroups[layer.Digest], blobEntry{name, layer})
	}

	for digest, entries := range blobGroups {
		path := mw.manifest.BlobPath(digest)

		// Load blob as safetensors (native mmap, zero-copy)
		sf, err := mlx.LoadSafetensorsNative(path)
		if err != nil {
			for _, h := range nativeHandles {
				h.Free()
			}
			return fmt.Errorf("load %s: %w", entries[0].name, err)
		}
		nativeHandles = append(nativeHandles, sf)

		// Read quantization metadata from blob
		if qt := sf.GetMetadata("quant_type"); qt != "" && mw.quantType == "" {
			mw.quantType = qt
			if gs := sf.GetMetadata("group_size"); gs != "" {
				mw.groupSize, _ = strconv.Atoi(gs)
			}
		}

		for _, entry := range entries {
			name := entry.name

			// Try to get tensor by stripped name first, then with component prefix,
			// then fall back to "data" for legacy blobs created by older versions
			// that stored all tensors with the generic key "data".
			lookupName := name
			arr := sf.Get(lookupName)
			if arr == nil && mw.component != "" {
				lookupName = mw.component + "/" + name
				arr = sf.Get(lookupName)
			}
			if arr == nil {
				// Legacy blob format: tensor stored as "data"
				lookupName = "data"
				arr = sf.Get(lookupName)
			}
			if arr != nil {
				// Single-tensor blob or tensor found by name
				if dtype != 0 && arr.Dtype() != dtype {
					arr = mlx.AsType(arr, dtype)
				}
				arr = mlx.Contiguous(arr)
				mw.cache[name] = arr
				arrays = append(arrays, arr)

				// Check for scale tensor
				if scale := sf.Get(lookupName + ".scale"); scale != nil {
					scale = mlx.Contiguous(scale)
					mw.cache[name+"_scale"] = scale
					arrays = append(arrays, scale)
				}

				// Check for bias tensor
				if bias := sf.Get(lookupName + ".bias"); bias != nil {
					bias = mlx.Contiguous(bias)
					mw.cache[name+"_qbias"] = bias
					arrays = append(arrays, bias)
				}
			} else {
				// Packed blob: manifest name is a group prefix, not a tensor name.
				// Load all individual tensors from the blob.
				tensorNames, err := ParseBlobTensorNames(path)
				if err != nil {
					for _, h := range nativeHandles {
						h.Free()
					}
					return fmt.Errorf("parse packed blob for %s: %w", name, err)
				}

				for _, tensorName := range tensorNames {
					tArr := sf.Get(tensorName)
					if tArr == nil {
						continue
					}

					if dtype != 0 && tArr.Dtype() != dtype {
						tArr = mlx.AsType(tArr, dtype)
					}
					tArr = mlx.Contiguous(tArr)

					// Strip component prefix from blob-internal names so cache keys
					// match the stripped names used by LoadModule.
					cacheName := tensorName
					if mw.component != "" {
						cacheName = strings.TrimPrefix(tensorName, mw.component+"/")
					}
					mw.cache[cacheName] = tArr
					arrays = append(arrays, tArr)

					// Check for scale tensor
					if scale := sf.Get(tensorName + ".scale"); scale != nil {
						scale = mlx.Contiguous(scale)
						mw.cache[cacheName+"_scale"] = scale
						arrays = append(arrays, scale)
					}

					// Check for bias tensor
					if bias := sf.Get(tensorName + ".bias"); bias != nil {
						bias = mlx.Contiguous(bias)
						mw.cache[cacheName+"_qbias"] = bias
						arrays = append(arrays, bias)
					}
				}
			}
		}
	}

	// Batch evaluate all tensors at once (much faster than one at a time)
	mlx.Eval(arrays...)

	// Now safe to free all native handles
	for _, sf := range nativeHandles {
		sf.Free()
	}

	return nil
}

// GetTensor returns a tensor from cache. Call Load() first.
func (mw *ManifestWeights) GetTensor(name string) (*mlx.Array, error) {
	if mw.cache == nil {
		return nil, fmt.Errorf("cache not initialized: call Load() first")
	}
	arr, ok := mw.cache[name]
	if !ok {
		return nil, fmt.Errorf("tensor %q not found", name)
	}
	return arr, nil
}

// ListTensors returns all tensor names in sorted order.
// Includes both manifest tensor names and scale/bias entries from combined blobs.
func (mw *ManifestWeights) ListTensors() []string {
	seen := make(map[string]bool, len(mw.tensors)+len(mw.cache))
	for name := range mw.tensors {
		seen[name] = true
	}
	// Also include cache entries (scale/bias from combined blobs)
	for name := range mw.cache {
		seen[name] = true
	}
	names := make([]string, 0, len(seen))
	for name := range seen {
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

// HasTensor checks if a tensor exists in the manifest or cache.
func (mw *ManifestWeights) HasTensor(name string) bool {
	if _, ok := mw.tensors[name]; ok {
		return true
	}
	// Also check cache for scale/bias entries from combined blobs
	if _, ok := mw.cache[name]; ok {
		return true
	}
	return false
}

// Quantization returns the model's quantization type.
// Returns the quant_type from blob metadata (e.g., "int4", "int8", "nvfp4", "mxfp8").
// Returns empty string if not quantized.
// Falls back to model_index.json for image gen models.
func (mw *ManifestWeights) Quantization() string {
	if mw.quantType != "" {
		return strings.ToUpper(mw.quantType)
	}

	if mw.manifest == nil {
		return ""
	}

	// Fallback: read from model_index.json (for image gen models)
	var index struct {
		Quantization string `json:"quantization"`
	}
	if err := mw.manifest.ReadConfigJSON("model_index.json", &index); err == nil && index.Quantization != "" {
		return index.Quantization
	}

	return ""
}

// GroupSize returns the quantization group size.
// Returns the group_size from blob metadata.
// Returns 0 if not specified (caller should use default based on quantization type).
func (mw *ManifestWeights) GroupSize() int {
	if mw.groupSize > 0 {
		return mw.groupSize
	}

	if mw.manifest == nil {
		return 0
	}

	// Fallback: read from model_index.json (for image gen models)
	var index struct {
		GroupSize int `json:"group_size"`
	}
	if err := mw.manifest.ReadConfigJSON("model_index.json", &index); err == nil && index.GroupSize > 0 {
		return index.GroupSize
	}

	return 0
}

// ReleaseAll frees all native handles and clears the tensor cache.
func (mw *ManifestWeights) ReleaseAll() {
	for _, sf := range mw.nativeCache {
		sf.Free()
	}
	mw.nativeCache = nil
	mw.cache = nil
}
