package convert

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"io/fs"
	"maps"
	"math"
	"os"
	"slices"
	"strconv"
	"strings"

	"github.com/d4l3k/go-bfloat16"
	"github.com/pdevine/tensor"
	"github.com/pdevine/tensor/native"
	"github.com/x448/float16"

	"github.com/ollama/ollama/fs/ggml"
)

type qwen3NextRopeScaling struct {
	Type         string     `json:"type"`
	Factor       ropeFactor `json:"factor"`
	MropeSection []int32    `json:"mrope_section"`
}

type qwen3NextRopeParams struct {
	MRopeInterleaved    bool    `json:"mrope_interleaved"`
	MropeSection        []int32 `json:"mrope_section"`
	RopeType            string  `json:"rope_type"`
	RopeTheta           float32 `json:"rope_theta"`
	PartialRotaryFactor float32 `json:"partial_rotary_factor"`
}

type qwen3NextTextConfig struct {
	MaxPositionEmbeddings uint32  `json:"max_position_embeddings"`
	HiddenSize            uint32  `json:"hidden_size"`
	NumHiddenLayers       uint32  `json:"num_hidden_layers"`
	NumNextNPredictLayers uint32  `json:"num_nextn_predict_layers"`
	MTPNumHiddenLayers    uint32  `json:"mtp_num_hidden_layers"`
	IntermediateSize      uint32  `json:"intermediate_size"`
	NumAttentionHeads     uint32  `json:"num_attention_heads"`
	NumKeyValueHeads      uint32  `json:"num_key_value_heads"`
	HeadDim               uint32  `json:"head_dim"`
	RopeTheta             float32 `json:"rope_theta"`
	RMSNormEPS            float32 `json:"rms_norm_eps"`

	// MoE config
	NumExperts             uint32 `json:"num_experts"`
	NumExpertsPerToken     uint32 `json:"num_experts_per_tok"`
	NormTopkProb           *bool  `json:"norm_topk_prob"`
	MoEIntermediateSize    uint32 `json:"moe_intermediate_size"`
	SharedExpertIntermSize uint32 `json:"shared_expert_intermediate_size"`

	// Hybrid attention config
	FullAttentionInterval uint32   `json:"full_attention_interval"`
	LayerTypes            []string `json:"layer_types"`

	// Linear attention (Gated Delta Net) config
	LinearConvKernelDim uint32 `json:"linear_conv_kernel_dim"`
	LinearKeyHeadDim    uint32 `json:"linear_key_head_dim"`
	LinearNumKeyHeads   uint32 `json:"linear_num_key_heads"`
	LinearNumValueHeads uint32 `json:"linear_num_value_heads"`
	LinearValueHeadDim  uint32 `json:"linear_value_head_dim"`

	// RoPE config
	PartialRotaryFactor float32              `json:"partial_rotary_factor"`
	RopeScaling         qwen3NextRopeScaling `json:"rope_scaling"`
	RopeParameters      qwen3NextRopeParams  `json:"rope_parameters"`
}

type qwen3NextVisionConfig struct {
	Depth                  uint32  `json:"depth"`
	HiddenSize             uint32  `json:"hidden_size"`
	IntermediateSize       uint32  `json:"intermediate_size"`
	NumHeads               uint32  `json:"num_heads"`
	NumPositionEmbeddings  uint32  `json:"num_position_embeddings"`
	InChannels             uint32  `json:"in_channels"`
	OutHiddenSize          uint32  `json:"out_hidden_size"`
	PatchSize              uint32  `json:"patch_size"`
	SpatialMergeSize       uint32  `json:"spatial_merge_size"`
	RMSNormEps             float32 `json:"layer_norm_epsilon"`
	RopeTheta              float32 `json:"rope_theta"`
	TemporalPatchSize      uint32  `json:"temporal_patch_size"`
	DeepstackVisualIndexes []int32 `json:"deepstack_visual_indexes"`

	Size struct {
		ShortestEdge uint32 `json:"shortest_edge"`
		LongestEdge  uint32 `json:"longest_edge"`
	} `json:"size"`

	ImageMean []float32 `json:"image_mean"`
	ImageStd  []float32 `json:"image_std"`
}

type qwen3NextModel struct {
	ModelParameters
	qwen3NextTextConfig

	TextConfig  *qwen3NextTextConfig  `json:"text_config"`
	VisionModel qwen3NextVisionConfig `json:"vision_config"`

	ImageTokenID       uint32 `json:"image_token_id"`
	VisionStartTokenID uint32 `json:"vision_start_token_id"`
	VisionEndTokenID   uint32 `json:"vision_end_token_id"`
}

var (
	_ ModelConverter      = (*qwen3NextModel)(nil)
	_ MultimodalConverter = (*qwen3NextModel)(nil)
)

func (q *qwen3NextModel) parseMore(fsys fs.FS) error {
	if q.TextConfig != nil {
		q.qwen3NextTextConfig = *q.TextConfig
	}
	if q.NumNextNPredictLayers == 0 {
		q.NumNextNPredictLayers = q.MTPNumHiddenLayers
	}
	if q.NumNextNPredictLayers == 0 {
		nextn, err := qwen3NextInferNextNPredictLayers(fsys)
		if err != nil {
			return err
		}
		q.NumNextNPredictLayers = nextn
	}

	if q.RopeTheta == 0 {
		q.RopeTheta = q.RopeParameters.RopeTheta
	}
	if q.PartialRotaryFactor == 0 {
		q.PartialRotaryFactor = q.RopeParameters.PartialRotaryFactor
	}

	if q.RopeScaling.Type == "" && q.RopeParameters.RopeType != "" {
		q.RopeScaling.Type = q.RopeParameters.RopeType
	}

	// Pull vision preprocessing fields when present.
	if q.VisionModel.Depth > 0 {
		if bts, err := fs.ReadFile(fsys, "preprocessor_config.json"); err == nil {
			var pre struct {
				Size struct {
					ShortestEdge uint32 `json:"shortest_edge"`
					LongestEdge  uint32 `json:"longest_edge"`
				} `json:"size"`
				PatchSize         uint32    `json:"patch_size"`
				TemporalPatchSize uint32    `json:"temporal_patch_size"`
				MergeSize         uint32    `json:"merge_size"`
				ImageMean         []float32 `json:"image_mean"`
				ImageStd          []float32 `json:"image_std"`
			}
			if json.Unmarshal(bts, &pre) == nil {
				if q.VisionModel.PatchSize == 0 {
					q.VisionModel.PatchSize = pre.PatchSize
				}
				if q.VisionModel.TemporalPatchSize == 0 {
					q.VisionModel.TemporalPatchSize = pre.TemporalPatchSize
				}
				if q.VisionModel.SpatialMergeSize == 0 {
					q.VisionModel.SpatialMergeSize = pre.MergeSize
				}
				if q.VisionModel.Size.ShortestEdge == 0 {
					q.VisionModel.Size.ShortestEdge = pre.Size.ShortestEdge
				}
				if q.VisionModel.Size.LongestEdge == 0 {
					q.VisionModel.Size.LongestEdge = pre.Size.LongestEdge
				}
				if len(q.VisionModel.ImageMean) == 0 {
					q.VisionModel.ImageMean = pre.ImageMean
				}
				if len(q.VisionModel.ImageStd) == 0 {
					q.VisionModel.ImageStd = pre.ImageStd
				}
			}
		}
	}

	if q.NumHiddenLayers == 0 {
		return fmt.Errorf("qwen3next: num_hidden_layers must be set")
	}
	if q.NumAttentionHeads == 0 {
		return fmt.Errorf("qwen3next: num_attention_heads must be set")
	}
	if q.NumKeyValueHeads == 0 {
		return fmt.Errorf("qwen3next: num_key_value_heads must be set")
	}
	if q.HeadDim == 0 {
		return fmt.Errorf("qwen3next: head_dim must be set")
	}
	if q.RopeTheta == 0 {
		return fmt.Errorf("qwen3next: rope_theta must be set")
	}
	if q.PartialRotaryFactor <= 0 || q.PartialRotaryFactor > 1 {
		return fmt.Errorf("qwen3next: partial_rotary_factor must be in (0,1], got %v", q.PartialRotaryFactor)
	}
	if q.LinearNumKeyHeads == 0 || q.LinearNumValueHeads == 0 || q.LinearKeyHeadDim == 0 || q.LinearValueHeadDim == 0 {
		return fmt.Errorf("qwen3next: linear attention config must be set (linear_num_key_heads, linear_num_value_heads, linear_key_head_dim, linear_value_head_dim)")
	}
	if _, err := q.kvHeadCounts(); err != nil {
		return err
	}

	return nil
}

func qwen3NextInferNextNPredictLayers(fsys fs.FS) (uint32, error) {
	paths, err := fs.Glob(fsys, "*.safetensors")
	if err != nil {
		return 0, err
	}

	maxLayer := -1
	hasMTP := false
	for _, p := range paths {
		f, err := fsys.Open(p)
		if err != nil {
			return 0, err
		}

		var n int64
		if err := binary.Read(f, binary.LittleEndian, &n); err != nil {
			f.Close()
			return 0, err
		}

		b := bytes.NewBuffer(make([]byte, 0, n))
		if _, err = io.CopyN(b, f, n); err != nil {
			f.Close()
			return 0, err
		}
		f.Close()

		var headers map[string]safetensorMetadata
		if err := json.NewDecoder(b).Decode(&headers); err != nil {
			return 0, err
		}

		for name, value := range headers {
			if value.Type == "" || !strings.HasPrefix(name, "mtp.") {
				continue
			}
			hasMTP = true
			rest := strings.TrimPrefix(name, "mtp.layers.")
			layer, suffix, ok := strings.Cut(rest, ".")
			if !ok {
				continue
			}
			n, err := strconv.Atoi(layer)
			if err == nil && n > maxLayer && suffix != "" {
				maxLayer = n
			}
		}
	}

	if maxLayer >= 0 {
		return uint32(maxLayer + 1), nil
	}
	if hasMTP {
		return 1, nil
	}
	return 0, nil
}

func ConvertQwen35MTPDraft(fsys fs.FS, f *os.File, baseKV ggml.KV, baseTensors []*ggml.Tensor) error {
	arch := baseKV.Architecture()
	if arch != "qwen35" && arch != "qwen35moe" {
		return fmt.Errorf("MTP draft safetensors require a qwen3.5 base model, got %q", arch)
	}

	baseBlocks := baseKV.Uint("block_count")
	if baseBlocks == 0 {
		return fmt.Errorf("MTP draft safetensors require a base model with block_count")
	}
	if baseKV.Uint("nextn_predict_layers") > 0 {
		return fmt.Errorf("MTP draft safetensors require a base model without embedded MTP layers")
	}

	nextn, err := qwen3NextInferNextNPredictLayers(fsys)
	if err != nil {
		return err
	}
	if nextn == 0 {
		return fmt.Errorf("MTP draft safetensors did not contain mtp tensors")
	}

	q := &qwen3NextModel{
		qwen3NextTextConfig: qwen3NextTextConfig{
			NumHiddenLayers:       baseBlocks,
			NumNextNPredictLayers: nextn,
		},
	}
	ts, err := parseTensors(fsys, strings.NewReplacer(q.Replacements()...))
	if err != nil {
		return err
	}
	if err := ensureUniqueTensorNames(ts); err != nil {
		return err
	}

	mtpTensors := q.Tensors(ts)
	if len(mtpTensors) == 0 {
		return fmt.Errorf("MTP draft safetensors did not produce GGUF tensors")
	}
	for _, tensor := range mtpTensors {
		if !qwen35MTPDraftTensorName(tensor.Name, baseBlocks, nextn) {
			return fmt.Errorf("MTP draft safetensors produced unexpected tensor %q", tensor.Name)
		}
		tensor.Shape = slices.Clone(tensor.Shape)
		slices.Reverse(tensor.Shape)
	}

	kv := maps.Clone(baseKV)
	qwen35RemoveSplitMetadata(kv, arch)
	kv[arch+".block_count"] = baseBlocks + nextn
	kv[arch+".nextn_predict_layers"] = nextn

	tensors := make([]*ggml.Tensor, 0, len(baseTensors)+len(mtpTensors))
	tensors = append(tensors, baseTensors...)
	tensors = append(tensors, mtpTensors...)

	var parameters uint64
	for _, tensor := range tensors {
		parameters += tensor.Elements()
	}
	kv["general.parameter_count"] = parameters

	return ggml.WriteGGUF(f, kv, tensors)
}

func qwen35RemoveSplitMetadata(kv ggml.KV, arch string) {
	for _, key := range []string{
		"split.no",
		"split.count",
		"split.tensors.count",
	} {
		delete(kv, key)
		delete(kv, arch+"."+key)
	}
}

func qwen35MTPDraftTensorName(name string, base, nextn uint32) bool {
	for i := range nextn {
		if strings.HasPrefix(name, fmt.Sprintf("blk.%d.", base+i)) {
			return true
		}
	}
	return false
}

func (q *qwen3NextModel) kvHeadCounts() ([]uint32, error) {
	if len(q.LayerTypes) > 0 {
		kv := make([]uint32, q.NumHiddenLayers)
		hasFull := false
		hasRecurrent := false
		for i := range q.NumHiddenLayers {
			layerType := ""
			if i < uint32(len(q.LayerTypes)) {
				layerType = q.LayerTypes[i]
			}
			if layerType == "full_attention" {
				kv[i] = q.NumKeyValueHeads
				hasFull = true
			} else {
				hasRecurrent = true
			}
		}
		if !hasFull || !hasRecurrent {
			return nil, fmt.Errorf("qwen3next: layer_types must include both full_attention and linear_attention")
		}
		return kv, nil
	}

	if q.FullAttentionInterval == 0 {
		return nil, fmt.Errorf("qwen3next: full_attention_interval must be set")
	}
	if q.FullAttentionInterval > q.NumHiddenLayers {
		return nil, fmt.Errorf("qwen3next: full_attention_interval (%d) exceeds num_hidden_layers (%d)", q.FullAttentionInterval, q.NumHiddenLayers)
	}

	kv := make([]uint32, q.NumHiddenLayers)
	hasFull := false
	for i := range q.NumHiddenLayers {
		if (i+1)%q.FullAttentionInterval == 0 {
			kv[i] = q.NumKeyValueHeads
			hasFull = true
		}
	}
	if !hasFull {
		return nil, fmt.Errorf("qwen3next: head_count_kv would be all zeros (full_attention_interval=%d, num_hidden_layers=%d)", q.FullAttentionInterval, q.NumHiddenLayers)
	}
	return kv, nil
}

func (q *qwen3NextModel) ropeSections() []int32 {
	if len(q.RopeParameters.MropeSection) > 0 {
		return q.RopeParameters.MropeSection
	}
	return q.RopeScaling.MropeSection
}

func (q *qwen3NextModel) shouldReorderVHeads() bool {
	modelType := strings.ToLower(q.ModelType)
	if strings.Contains(modelType, "qwen3_next") || strings.Contains(modelType, "qwen3next") {
		return false
	}

	for _, arch := range q.Architectures {
		arch = strings.ToLower(arch)
		if strings.Contains(arch, "qwen3next") || strings.Contains(arch, "qwen3_next") {
			return false
		}
	}

	// Default to qwen3.5 layout for all other qwen3next-family imports.
	return true
}

func (q *qwen3NextModel) KV(t *Tokenizer) KV {
	kv := q.ModelParameters.KV(t)

	arch := "qwen35"
	if q.NumExperts > 0 {
		arch = "qwen35moe"
	}
	kv["general.architecture"] = arch
	kv["tokenizer.ggml.pre"] = "qwen35"
	kv["block_count"] = q.NumHiddenLayers + q.NumNextNPredictLayers
	if q.NumNextNPredictLayers > 0 {
		kv["nextn_predict_layers"] = q.NumNextNPredictLayers
	}
	kv["context_length"] = q.MaxPositionEmbeddings
	kv["embedding_length"] = q.HiddenSize
	kv["feed_forward_length"] = q.IntermediateSize
	kv["attention.head_count"] = q.NumAttentionHeads

	headDim := q.HeadDim
	if headDim == 0 && q.NumAttentionHeads > 0 {
		headDim = q.HiddenSize / q.NumAttentionHeads
	}
	kv["attention.key_length"] = headDim
	kv["attention.value_length"] = headDim
	kv["attention.layer_norm_rms_epsilon"] = q.RMSNormEPS
	kv["rope.freq_base"] = q.RopeTheta

	partialRotary := q.PartialRotaryFactor
	if partialRotary > 0 && partialRotary <= 1 {
		kv["rope.dimension_count"] = uint32(float32(headDim) * partialRotary)
	}

	if sections := q.ropeSections(); len(sections) > 0 {
		kv["mrope_sections"] = sections
		kv["rope.mrope_section"] = sections
		dimensionSections := append([]int32(nil), sections...)
		if len(dimensionSections) == 3 {
			dimensionSections = append(dimensionSections, 0)
		}
		kv["rope.dimension_sections"] = dimensionSections
	}
	if q.RopeParameters.MRopeInterleaved {
		kv["rope.mrope_interleaved"] = true
	}

	if q.RopeScaling.Type != "" && q.RopeScaling.Type != "default" {
		kv["rope.scaling.type"] = q.RopeScaling.Type
		kv["rope.scaling.factor"] = q.RopeScaling.Factor
	}

	if q.NumExperts > 0 {
		kv["expert_count"] = q.NumExperts
		kv["expert_used_count"] = q.NumExpertsPerToken
		if q.NormTopkProb != nil {
			kv["norm_top_k_prob"] = *q.NormTopkProb
		}
		if q.MoEIntermediateSize > 0 {
			kv["expert_feed_forward_length"] = q.MoEIntermediateSize
		}
		if q.SharedExpertIntermSize > 0 {
			kv["expert_shared_feed_forward_length"] = q.SharedExpertIntermSize
		}
	}

	dInner := q.LinearValueHeadDim * q.LinearNumValueHeads
	kv["ssm.inner_size"] = dInner
	kv["ssm.state_size"] = q.LinearKeyHeadDim
	kv["ssm.group_count"] = q.LinearNumKeyHeads
	kv["ssm.time_step_rank"] = q.LinearNumValueHeads
	kv["ssm.conv_kernel"] = q.LinearConvKernelDim
	if q.shouldReorderVHeads() {
		kv["ssm.v_head_reordered"] = true
	}
	if q.FullAttentionInterval > 0 {
		kv["full_attention_interval"] = q.FullAttentionInterval
	}

	if headCounts, err := q.kvHeadCounts(); err == nil {
		var maxKV uint32
		for _, count := range headCounts {
			if count > maxKV {
				maxKV = count
			}
		}
		kv["attention.head_count_kv"] = maxKV
	}

	if q.VisionModel.Depth > 0 {
		kv["vision.block_count"] = q.VisionModel.Depth
		kv["vision.embedding_length"] = q.VisionModel.HiddenSize
		if q.VisionModel.IntermediateSize > 0 {
			kv["vision.feed_forward_length"] = q.VisionModel.IntermediateSize
		}
		kv["vision.attention.head_count"] = q.VisionModel.NumHeads
		kv["vision.num_channels"] = q.VisionModel.InChannels
		if q.VisionModel.PatchSize > 0 {
			kv["vision.patch_size"] = q.VisionModel.PatchSize
		}
		if q.VisionModel.SpatialMergeSize > 0 {
			kv["vision.spatial_merge_size"] = q.VisionModel.SpatialMergeSize
		}
		if q.VisionModel.RMSNormEps > 0 {
			kv["vision.attention.layer_norm_epsilon"] = q.VisionModel.RMSNormEps
		}
		if q.VisionModel.RopeTheta > 0 {
			kv["vision.rope.freq_base"] = q.VisionModel.RopeTheta
		}
		if q.VisionModel.TemporalPatchSize > 0 {
			kv["vision.temporal_patch_size"] = q.VisionModel.TemporalPatchSize
		}
		kv["vision.deepstack_visual_indexes"] = q.VisionModel.DeepstackVisualIndexes
		if q.VisionModel.Size.ShortestEdge > 0 {
			kv["vision.shortest_edge"] = q.VisionModel.Size.ShortestEdge
		}
		if q.VisionModel.Size.LongestEdge > 0 {
			kv["vision.longest_edge"] = q.VisionModel.Size.LongestEdge
		}
		if len(q.VisionModel.ImageMean) > 0 {
			kv["vision.image_mean"] = q.VisionModel.ImageMean
		}
		if len(q.VisionModel.ImageStd) > 0 {
			kv["vision.image_std"] = q.VisionModel.ImageStd
		}
	}

	if q.ImageTokenID > 0 {
		kv["image_token_id"] = q.ImageTokenID
	}
	if q.VisionStartTokenID > 0 {
		kv["vision_start_token_id"] = q.VisionStartTokenID
	}
	if q.VisionEndTokenID > 0 {
		kv["vision_end_token_id"] = q.VisionEndTokenID
	}

	return kv
}

func (q *qwen3NextModel) TextKV(t *Tokenizer) KV {
	kv := q.KV(t)

	for _, key := range []string{
		"vision.block_count",
		"vision.embedding_length",
		"vision.feed_forward_length",
		"vision.attention.head_count",
		"vision.num_channels",
		"vision.patch_size",
		"vision.spatial_merge_size",
		"vision.attention.layer_norm_epsilon",
		"vision.rope.freq_base",
		"vision.temporal_patch_size",
		"vision.deepstack_visual_indexes",
		"vision.shortest_edge",
		"vision.longest_edge",
		"vision.image_mean",
		"vision.image_std",
		"image_token_id",
		"vision_start_token_id",
		"vision_end_token_id",
		"mrope_sections",
		"rope.mrope_section",
		"rope.mrope_interleaved",
		"ssm.v_head_reordered",
	} {
		delete(kv, key)
	}

	return kv
}

func (q *qwen3NextModel) ProjectorKV(*Tokenizer) KV {
	depth := q.VisionModel.Depth
	deepstack := make([]bool, depth)
	for _, idx := range q.VisionModel.DeepstackVisualIndexes {
		if idx >= 0 && uint32(idx) < depth {
			deepstack[idx] = true
		}
	}

	imageSize := uint32(768)
	if q.VisionModel.NumPositionEmbeddings > 0 && q.VisionModel.PatchSize > 0 {
		root := uint32(math.Sqrt(float64(q.VisionModel.NumPositionEmbeddings)))
		if root*root == q.VisionModel.NumPositionEmbeddings {
			imageSize = root * q.VisionModel.PatchSize
		}
	}

	projectionDim := q.VisionModel.OutHiddenSize
	if projectionDim == 0 {
		projectionDim = q.HiddenSize
	}
	layerNormEps := q.VisionModel.RMSNormEps
	if layerNormEps == 0 {
		layerNormEps = 1e-6
	}

	kv := KV{
		"general.architecture":                     "clip",
		"general.type":                             "mmproj",
		"general.file_type":                        uint32(1),
		"general.quantization_version":             uint32(2),
		"clip.has_vision_encoder":                  true,
		"clip.projector_type":                      "qwen3vl_merger",
		"clip.use_gelu":                            true,
		"clip.vision.block_count":                  depth,
		"clip.vision.embedding_length":             q.VisionModel.HiddenSize,
		"clip.vision.feed_forward_length":          q.VisionModel.IntermediateSize,
		"clip.vision.attention.head_count":         q.VisionModel.NumHeads,
		"clip.vision.image_size":                   imageSize,
		"clip.vision.patch_size":                   q.VisionModel.PatchSize,
		"clip.vision.projection_dim":               projectionDim,
		"clip.vision.spatial_merge_size":           q.VisionModel.SpatialMergeSize,
		"clip.vision.attention.layer_norm_epsilon": layerNormEps,
		"clip.vision.is_deepstack_layers":          deepstack,
	}
	if len(q.VisionModel.ImageMean) > 0 {
		kv["clip.vision.image_mean"] = q.VisionModel.ImageMean
	}
	if len(q.VisionModel.ImageStd) > 0 {
		kv["clip.vision.image_std"] = q.VisionModel.ImageStd
	}

	return kv
}

func (q *qwen3NextModel) TextTensors(ts []Tensor, _ *Tokenizer) []*ggml.Tensor {
	var text []Tensor
	for _, t := range ts {
		if qwen3NextVisionTensor(t.Name()) {
			continue
		}
		text = append(text, t)
	}

	return q.Tensors(text)
}

func (q *qwen3NextModel) ProjectorTensors(ts []Tensor) []*ggml.Tensor {
	if q.VisionModel.Depth == 0 {
		return nil
	}

	rename := strings.NewReplacer(
		"v.pos_embed", "v.position_embd",
		"v.patch_embed", "v.patch_embd",
		"v.merger.norm", "v.post_ln",
		"v.merger.linear_fc1", "mm.0",
		"v.merger.linear_fc2", "mm.2",
		".mlp.linear_fc1", ".ffn_up",
		".mlp.linear_fc2", ".ffn_down",
		".norm1", ".ln1",
		".norm2", ".ln2",
	)

	var out []*ggml.Tensor
	for _, t := range ts {
		name := t.Name()
		if !qwen3NextVisionTensor(name) {
			continue
		}

		if name == "v.patch_embed.weight" {
			out = append(out, q.qwen35PatchEmbedTensors(t)...)
			continue
		}

		outName := rename.Replace(name)
		kind := t.Kind()
		writer := io.WriterTo(t)
		if outName == "v.position_embd.weight" {
			kind = tensorKindFP32
			writer = tensorFloat32Writer{tensor: t}
		} else if sourceDType(t) == "BF16" && kind == tensorKindFP16 {
			kind = tensorKindBF16
			writer = tensorBF16Writer{tensor: t}
		}
		out = append(out, &ggml.Tensor{
			Name:     outName,
			Kind:     kind,
			Shape:    slices.Clone(t.Shape()),
			WriterTo: writer,
		})
	}

	return out
}

func qwen3NextVisionTensor(name string) bool {
	return strings.HasPrefix(name, "v.")
}

func (q *qwen3NextModel) qwen35PatchEmbedTensors(t Tensor) []*ggml.Tensor {
	shape := t.Shape()
	if len(shape) != 5 || shape[2] != 2 {
		return nil
	}

	outShape := []uint64{shape[0], shape[1], shape[3], shape[4]}
	return []*ggml.Tensor{
		{
			Name:     "v.patch_embd.weight",
			Kind:     tensorKindFP32,
			Shape:    slices.Clone(outShape),
			WriterTo: tensorFloat32Writer{tensor: t, repacker: q.qwen35PatchEmbedSlice(0)},
		},
		{
			Name:     "v.patch_embd.weight.1",
			Kind:     tensorKindFP32,
			Shape:    slices.Clone(outShape),
			WriterTo: tensorFloat32Writer{tensor: t, repacker: q.qwen35PatchEmbedSlice(1)},
		},
	}
}

func (q *qwen3NextModel) qwen35PatchEmbedSlice(slice int) Repacker {
	return func(_ string, data []float32, shape []uint64) ([]float32, error) {
		if len(shape) != 5 || shape[2] != 2 {
			return nil, fmt.Errorf("qwen3next: unexpected patch_embed shape %v", shape)
		}

		outChannels := int(shape[0])
		inChannels := int(shape[1])
		frames := int(shape[2])
		height := int(shape[3])
		width := int(shape[4])
		if slice < 0 || slice >= frames {
			return nil, fmt.Errorf("qwen3next: patch_embed slice %d out of range", slice)
		}

		expected := outChannels * inChannels * frames * height * width
		if len(data) != expected {
			return nil, fmt.Errorf("qwen3next: patch_embed data size %d, expected %d", len(data), expected)
		}

		out := make([]float32, outChannels*inChannels*height*width)
		for oc := range outChannels {
			for ic := range inChannels {
				for y := range height {
					for x := range width {
						src := ((((oc*inChannels+ic)*frames+slice)*height + y) * width) + x
						dst := (((oc*inChannels+ic)*height + y) * width) + x
						out[dst] = data[src]
					}
				}
			}
		}

		return out, nil
	}
}

type tensorBF16Writer struct {
	tensor   Tensor
	repacker Repacker
}

func (w tensorBF16Writer) WriteTo(dst io.Writer) (int64, error) {
	data, err := tensorFloat32Data(w.tensor)
	if err != nil {
		return 0, err
	}
	if w.repacker != nil {
		data, err = w.repacker(w.tensor.Name(), data, w.tensor.Shape())
		if err != nil {
			return 0, err
		}
	}

	u8s := bfloat16.EncodeFloat32(data)
	if _, err := dst.Write(u8s); err != nil {
		return 0, err
	}
	return int64(len(u8s)), nil
}

type tensorFloat32Writer struct {
	tensor   Tensor
	repacker Repacker
}

func (w tensorFloat32Writer) WriteTo(dst io.Writer) (int64, error) {
	data, err := tensorFloat32Data(w.tensor)
	if err != nil {
		return 0, err
	}
	if w.repacker != nil {
		data, err = w.repacker(w.tensor.Name(), data, w.tensor.Shape())
		if err != nil {
			return 0, err
		}
	}
	if err := binary.Write(dst, binary.LittleEndian, data); err != nil {
		return 0, err
	}
	return int64(len(data) * 4), nil
}

func tensorFloat32Data(t Tensor) ([]float32, error) {
	if st, ok := tensorSafetensor(t); ok {
		return safetensorFloat32Data(st)
	}

	var buf bytes.Buffer
	if _, err := t.WriteTo(&buf); err != nil {
		return nil, err
	}

	switch t.Kind() {
	case tensorKindFP32:
		out := make([]float32, buf.Len()/4)
		if err := binary.Read(bytes.NewReader(buf.Bytes()), binary.LittleEndian, out); err != nil {
			return nil, err
		}
		return out, nil
	case tensorKindFP16:
		raw := make([]uint16, buf.Len()/2)
		if err := binary.Read(bytes.NewReader(buf.Bytes()), binary.LittleEndian, raw); err != nil {
			return nil, err
		}
		out := make([]float32, len(raw))
		for i, v := range raw {
			out[i] = float16.Frombits(v).Float32()
		}
		return out, nil
	case tensorKindBF16:
		return bfloat16.DecodeFloat32(buf.Bytes()), nil
	default:
		return nil, fmt.Errorf("unsupported tensor kind %d for F32 writer", t.Kind())
	}
}

func tensorSafetensor(t Tensor) (safetensor, bool) {
	switch t := t.(type) {
	case safetensor:
		return t, true
	case *safetensor:
		return *t, true
	default:
		return safetensor{}, false
	}
}

func safetensorFloat32Data(st safetensor) ([]float32, error) {
	f, err := st.fs.Open(st.path)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	var r io.Reader
	if readerAt, ok := f.(io.ReaderAt); ok {
		r = io.NewSectionReader(readerAt, st.offset, st.size)
	} else if seeker, ok := f.(io.Seeker); ok {
		if _, err := seeker.Seek(st.offset, io.SeekStart); err != nil {
			return nil, err
		}
		r = f
	} else {
		if _, err := io.CopyN(io.Discard, f, st.offset); err != nil {
			return nil, err
		}
		r = f
	}

	br := bufio.NewReaderSize(r, min(32<<10, int(st.size)))
	var out []float32
	switch st.dtype {
	case "F32":
		out = make([]float32, st.size/4)
		if err := binary.Read(br, binary.LittleEndian, out); err != nil {
			return nil, err
		}
	case "F16":
		raw := make([]uint16, st.size/2)
		if err := binary.Read(br, binary.LittleEndian, raw); err != nil {
			return nil, err
		}
		out = make([]float32, len(raw))
		for i, v := range raw {
			out[i] = float16.Frombits(v).Float32()
		}
	case "BF16":
		raw := make([]uint8, st.size)
		if err := binary.Read(br, binary.LittleEndian, raw); err != nil {
			return nil, err
		}
		out = bfloat16.DecodeFloat32(raw)
	case "F8_E4M3":
		raw := make([]uint8, st.size)
		if err := binary.Read(br, binary.LittleEndian, raw); err != nil {
			return nil, err
		}
		out, err = st.decodeFP8E4M3(raw)
		if err != nil {
			return nil, err
		}
	default:
		return nil, fmt.Errorf("unsupported safetensor dtype %q", st.dtype)
	}

	if st.repacker != nil {
		out, err = st.repacker(st.Name(), out, st.Shape())
		if err != nil {
			return nil, err
		}
	}
	return out, nil
}

func (q *qwen3NextModel) Tensors(ts []Tensor) []*ggml.Tensor {
	var out []*ggml.Tensor

	merges := make([]merge, q.NumHiddenLayers*3)
	for i := range q.NumHiddenLayers {
		merges[i*3+0] = merge{
			fmt.Sprintf("blk.%d.mlp.experts.*.gate_proj.weight", i),
			fmt.Sprintf("blk.%d.ffn_gate_exps.weight", i),
		}
		merges[i*3+1] = merge{
			fmt.Sprintf("blk.%d.mlp.experts.*.up_proj.weight", i),
			fmt.Sprintf("blk.%d.ffn_up_exps.weight", i),
		}
		merges[i*3+2] = merge{
			fmt.Sprintf("blk.%d.mlp.experts.*.down_proj.weight", i),
			fmt.Sprintf("blk.%d.ffn_down_exps.weight", i),
		}
	}

	merged, remaining := mergeTensors(ts, merges...)
	out = append(out, merged...)

	for _, t := range remaining {
		name := t.Name()
		shape := t.Shape()

		if names := q.mtpTensorNames(name); len(names) > 0 {
			for _, name := range names {
				out = q.appendDirectTensor(out, t, name)
			}
			continue
		}

		if strings.HasSuffix(name, ".ssm_in.weight") {
			if qkv, gate, ok := q.splitQKVZTensor(t); ok {
				out = append(out, qkv, gate)
				continue
			}
			panic(fmt.Sprintf("qwen3next: failed to split %s into attn_qkv/attn_gate (shape=%v)", name, shape))
		}

		switch {
		case strings.Contains(name, ".mlp.experts.gate_up_proj"):
			out = append(out, slices.Collect(splitDim(t, 1,
				split{Replacer: strings.NewReplacer(".mlp.experts.gate_up_proj", ".ffn_gate_exps.weight")},
				split{Replacer: strings.NewReplacer(".mlp.experts.gate_up_proj", ".ffn_up_exps.weight")},
			))...)

		case strings.Contains(name, ".mlp.experts.down_proj"):
			out = append(out, &ggml.Tensor{
				Name:     strings.NewReplacer(".mlp.experts.down_proj", ".ffn_down_exps.weight").Replace(name),
				Kind:     t.Kind(),
				Shape:    slices.Clone(shape),
				WriterTo: t,
			})

		case strings.HasPrefix(name, "v.blk.") && strings.Contains(name, ".attn_qkv"):
			out = append(out, slices.Collect(splitDim(t, 0,
				split{Replacer: strings.NewReplacer("attn_qkv", "attn_q")},
				split{Replacer: strings.NewReplacer("attn_qkv", "attn_k")},
				split{Replacer: strings.NewReplacer("attn_qkv", "attn_v")},
			))...)

		case strings.Contains(name, "patch_embed") && strings.HasSuffix(name, "weight"):
			out = append(out, &ggml.Tensor{
				Name:     name,
				Kind:     t.Kind(),
				Shape:    append([]uint64{shape[0] * shape[1]}, shape[2:]...),
				WriterTo: t,
			})

		case strings.HasSuffix(name, "_norm.weight") && !strings.HasSuffix(name, ".ssm_norm.weight"):
			t.SetRepacker(q.addOne)
			out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})

		case strings.HasSuffix(name, ".ssm_a"):
			t.SetRepacker(q.repackSSMA())
			out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})

		case strings.HasSuffix(name, ".attn_qkv.weight"):
			if q.shouldReorderVHeads() {
				t.SetRepacker(q.repackAttnQKV())
			}
			out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})

		case strings.HasSuffix(name, ".attn_gate.weight"):
			if q.shouldReorderVHeads() {
				// HF tensor layout is [out_features, in_features]; reorder rows.
				t.SetRepacker(q.repackReorderDim(0, int(q.LinearValueHeadDim)))
			}
			out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})

		case strings.HasSuffix(name, ".ssm_beta.weight"), strings.HasSuffix(name, ".ssm_alpha.weight"):
			if q.shouldReorderVHeads() {
				// HF tensor layout is [out_features, in_features]; reorder rows.
				t.SetRepacker(q.repackReorderDim(0, 1))
			}
			out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})

		case strings.HasSuffix(name, ".ssm_dt"), strings.HasSuffix(name, ".ssm_dt.bias"):
			if q.shouldReorderVHeads() {
				t.SetRepacker(q.repackReorderDim(0, 1))
			}
			out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})

		case strings.HasSuffix(name, ".ssm_out.weight"):
			if q.shouldReorderVHeads() {
				// HF out_proj layout is [out_features, in_features]; reorder columns.
				t.SetRepacker(q.repackReorderDim(1, int(q.LinearValueHeadDim)))
			}
			out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})

		case strings.HasSuffix(name, ".ssm_conv1d.weight"):
			newShape := slices.Clone(shape)
			if len(shape) == 3 {
				if shape[0] == 1 {
					newShape = []uint64{shape[1], shape[2]}
				} else if shape[1] == 1 {
					newShape = []uint64{shape[0], shape[2]}
				}
			}
			if q.shouldReorderVHeads() {
				t.SetRepacker(q.repackConv1D())
			}
			out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: newShape, WriterTo: t})

		default:
			out = append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(shape), WriterTo: t})
		}
	}

	return out
}

func (q *qwen3NextModel) appendDirectTensor(out []*ggml.Tensor, t Tensor, name string) []*ggml.Tensor {
	if qwen3NextShouldShiftNorm(name) {
		t = t.Clone()
		t.SetRepacker(q.addOne)
	}
	return append(out, &ggml.Tensor{Name: name, Kind: t.Kind(), Shape: slices.Clone(t.Shape()), WriterTo: t})
}

func qwen3NextShouldShiftNorm(name string) bool {
	if strings.HasSuffix(name, ".ssm_norm.weight") {
		return false
	}
	return strings.HasSuffix(name, "_norm.weight") ||
		strings.HasSuffix(name, ".nextn.enorm.weight") ||
		strings.HasSuffix(name, ".nextn.hnorm.weight")
}

func (q *qwen3NextModel) mtpTensorNames(name string) []string {
	if !strings.HasPrefix(name, "mtp.") {
		return nil
	}

	base := q.NumHiddenLayers
	nextn := q.NumNextNPredictLayers
	if nextn == 0 {
		nextn = 1
	}

	if rest := strings.TrimPrefix(name, "mtp.layers."); rest != name {
		layer, suffix, ok := strings.Cut(rest, ".")
		if !ok {
			return nil
		}
		idx, err := strconv.ParseUint(layer, 10, 32)
		if err != nil {
			return nil
		}
		return []string{fmt.Sprintf("blk.%d.%s", base+uint32(idx), suffix)}
	}

	var suffix string
	switch name {
	case "mtp.fc.weight":
		suffix = "nextn.eh_proj.weight"
	case "mtp.pre_fc_norm_embedding.weight":
		suffix = "nextn.enorm.weight"
	case "mtp.pre_fc_norm_hidden.weight":
		suffix = "nextn.hnorm.weight"
	case "mtp.norm.weight":
		suffix = "nextn.shared_head_norm.weight"
	case "mtp.embed_tokens.weight":
		suffix = "nextn.embed_tokens.weight"
	case "mtp.shared_head.head.weight":
		suffix = "nextn.shared_head_head.weight"
	case "mtp.shared_head.norm.weight":
		suffix = "nextn.shared_head_norm.weight"
	default:
		return nil
	}

	names := make([]string, 0, nextn)
	for i := range nextn {
		names = append(names, fmt.Sprintf("blk.%d.%s", base+i, suffix))
	}
	return names
}

func (q *qwen3NextModel) repackReorderDim(dim, headDim int) Repacker {
	return func(_ string, data []float32, shape []uint64) ([]float32, error) {
		if !q.shouldReorderVHeads() {
			return data, nil
		}
		numK := int(q.LinearNumKeyHeads)
		numVPerK := int(q.LinearNumValueHeads / q.LinearNumKeyHeads)
		return reorderHeadLayout(data, shape, dim, numK, numVPerK, headDim)
	}
}

func (q *qwen3NextModel) repackAttnQKV() Repacker {
	return func(_ string, data []float32, shape []uint64) ([]float32, error) {
		if !q.shouldReorderVHeads() || len(shape) != 2 {
			return data, nil
		}

		rows := int(shape[0])
		cols := int(shape[1])
		numK := int(q.LinearNumKeyHeads)
		numV := int(q.LinearNumValueHeads)
		headK := int(q.LinearKeyHeadDim)
		headV := int(q.LinearValueHeadDim)
		qDim := headK * numK
		kDim := headK * numK
		vDim := headV * numV
		qkvDim := qDim + kDim + vDim

		switch {
		case rows == qkvDim:
			// HF layout: [out_features, in_features]. Keep Q/K rows unchanged and
			// reorder only V rows from grouped -> tiled head layout.
			out := make([]float32, len(data))
			qkRows := qDim + kDim
			qkSize := qkRows * cols
			copy(out[:qkSize], data[:qkSize])

			vStart := qkSize
			vEnd := vStart + vDim*cols
			reorderedV, err := reorderHeadLayout(data[vStart:vEnd], []uint64{uint64(vDim), uint64(cols)}, 0, numK, numV/numK, headV)
			if err != nil {
				return nil, err
			}
			copy(out[vStart:vEnd], reorderedV)
			copy(out[vEnd:], data[vEnd:])
			return out, nil

		case cols == qkvDim:
			// Fallback for already-transposed [in_features, out_features] tensors.
			out := make([]float32, len(data))
			copy(out, data)
			for r := range rows {
				base := r * cols
				vStart := base + qDim + kDim
				vEnd := vStart + vDim
				reorderedV, err := reorderHeadLayout(out[vStart:vEnd], []uint64{uint64(vDim)}, 0, numK, numV/numK, headV)
				if err != nil {
					return nil, err
				}
				copy(out[vStart:vEnd], reorderedV)
			}
			return out, nil

		default:
			return data, nil
		}
	}
}

func (q *qwen3NextModel) repackConv1D() Repacker {
	return func(_ string, data []float32, shape []uint64) ([]float32, error) {
		if !q.shouldReorderVHeads() {
			return data, nil
		}

		normShape := slices.Clone(shape)
		if len(shape) == 3 {
			if shape[0] == 1 {
				normShape = []uint64{shape[1], shape[2]}
			} else if shape[1] == 1 {
				normShape = []uint64{shape[0], shape[2]}
			}
		}
		if len(normShape) != 2 {
			return data, nil
		}

		rows := int(normShape[0])
		cols := int(normShape[1])
		numK := int(q.LinearNumKeyHeads)
		numV := int(q.LinearNumValueHeads)
		headK := int(q.LinearKeyHeadDim)
		headV := int(q.LinearValueHeadDim)
		qkChannels := 2 * headK * numK
		totalChannels := qkChannels + headV*numV
		if qkChannels <= 0 {
			return data, nil
		}

		switch {
		case rows == totalChannels:
			// HF layout after squeeze: [channels, kernel]
			out := make([]float32, len(data))
			prefix := qkChannels * cols
			copy(out[:prefix], data[:prefix])
			reorderedV, err := reorderHeadLayout(data[prefix:], []uint64{uint64(totalChannels - qkChannels), uint64(cols)}, 0, numK, numV/numK, headV)
			if err != nil {
				return nil, err
			}
			copy(out[prefix:], reorderedV)
			return out, nil
		case cols == totalChannels:
			// Fallback for transposed [kernel, channels]
			out := make([]float32, len(data))
			copy(out, data)
			vChannels := totalChannels - qkChannels
			for r := range rows {
				base := r * cols
				vStart := base + qkChannels
				vEnd := vStart + vChannels
				reorderedV, err := reorderHeadLayout(out[vStart:vEnd], []uint64{uint64(vChannels)}, 0, numK, numV/numK, headV)
				if err != nil {
					return nil, err
				}
				copy(out[vStart:vEnd], reorderedV)
			}
			return out, nil
		default:
			return data, nil
		}
	}
}

func (q *qwen3NextModel) repackSSMA() Repacker {
	return func(_ string, data []float32, shape []uint64) ([]float32, error) {
		result := make([]float32, len(data))
		for i, v := range data {
			result[i] = -float32(math.Exp(float64(v)))
		}
		if !q.shouldReorderVHeads() {
			return result, nil
		}
		numK := int(q.LinearNumKeyHeads)
		numVPerK := int(q.LinearNumValueHeads / q.LinearNumKeyHeads)
		return reorderHeadLayout(result, shape, 0, numK, numVPerK, 1)
	}
}

func reorderHeadLayout(data []float32, shape []uint64, dim int, numKHeads, numVPerK, headDim int) ([]float32, error) {
	if len(shape) == 0 || numKHeads <= 0 || numVPerK <= 0 || headDim <= 0 {
		return data, nil
	}

	dims := make([]int, len(shape))
	for i := range shape {
		dims[i] = int(shape[i])
	}
	if dim < 0 {
		dim += len(dims)
	}
	if dim < 0 || dim >= len(dims) {
		return data, nil
	}

	expected := numKHeads * numVPerK * headDim
	if dims[dim] != expected {
		return data, nil
	}

	newShape := make([]int, 0, len(dims)+2)
	newShape = append(newShape, dims[:dim]...)
	newShape = append(newShape, numKHeads, numVPerK, headDim)
	newShape = append(newShape, dims[dim+1:]...)

	var tt tensor.Tensor = tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
	if err := tt.Reshape(newShape...); err != nil {
		return nil, err
	}

	perm := make([]int, len(newShape))
	for i := range perm {
		perm[i] = i
	}
	perm[dim], perm[dim+1] = perm[dim+1], perm[dim]

	tt, err := tensor.Transpose(tt, perm...)
	if err != nil {
		return nil, err
	}
	tt = tensor.Materialize(tt)

	total := 1
	for _, d := range dims {
		total *= d
	}
	if err := tt.Reshape(total); err != nil {
		return nil, err
	}
	return native.VectorF32(tt.(*tensor.Dense))
}

type qkvzSplitSpec struct {
	hidden    int
	headKDim  int
	headVDim  int
	numKHeads int
	numVHeads int
	qkvzDim   int
	qkvOut    int
	gateOut   int
}

func (q *qwen3NextModel) qkvzSpec(shape []uint64) (qkvzSplitSpec, bool) {
	if len(shape) != 2 {
		return qkvzSplitSpec{}, false
	}

	numKHeads := int(q.LinearNumKeyHeads)
	numVHeads := int(q.LinearNumValueHeads)
	headKDim := int(q.LinearKeyHeadDim)
	headVDim := int(q.LinearValueHeadDim)
	if numKHeads == 0 || numVHeads == 0 || headKDim == 0 || headVDim == 0 {
		return qkvzSplitSpec{}, false
	}
	if numVHeads%numKHeads != 0 {
		return qkvzSplitSpec{}, false
	}

	hidden := int(shape[1])
	vPerHead := headVDim * (numVHeads / numKHeads)
	qkvzDim := 2*headKDim + 2*vPerHead
	expectedOut := qkvzDim * numKHeads
	if int(shape[0]) != expectedOut {
		return qkvzSplitSpec{}, false
	}

	return qkvzSplitSpec{
		hidden:    hidden,
		headKDim:  headKDim,
		headVDim:  headVDim,
		numKHeads: numKHeads,
		numVHeads: numVHeads,
		qkvzDim:   qkvzDim,
		qkvOut:    2*headKDim*numKHeads + headVDim*numVHeads,
		gateOut:   headVDim * numVHeads,
	}, true
}

func (q *qwen3NextModel) splitQKVZTensor(t Tensor) (*ggml.Tensor, *ggml.Tensor, bool) {
	spec, ok := q.qkvzSpec(t.Shape())
	if !ok {
		return nil, nil, false
	}

	qkvTensor := t.Clone()
	qkvTensor.SetRepacker(q.repackQKVZ(spec, false))

	gateTensor := t.Clone()
	gateTensor.SetRepacker(q.repackQKVZ(spec, true))

	qkvName := strings.Replace(t.Name(), "ssm_in", "attn_qkv", 1)
	gateName := strings.Replace(t.Name(), "ssm_in", "attn_gate", 1)

	return &ggml.Tensor{
			Name:     qkvName,
			Kind:     t.Kind(),
			Shape:    []uint64{uint64(spec.qkvOut), uint64(spec.hidden)},
			WriterTo: qkvTensor,
		}, &ggml.Tensor{
			Name:     gateName,
			Kind:     t.Kind(),
			Shape:    []uint64{uint64(spec.gateOut), uint64(spec.hidden)},
			WriterTo: gateTensor,
		}, true
}

func (q *qwen3NextModel) repackQKVZ(spec qkvzSplitSpec, extractGate bool) Repacker {
	vPerHead := spec.headVDim * (spec.numVHeads / spec.numKHeads)

	return func(_ string, data []float32, shape []uint64) ([]float32, error) {
		dims := make([]int, len(shape))
		for i := range shape {
			dims[i] = int(shape[i])
		}

		var tt tensor.Tensor = tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
		var err error

		tt, err = tensor.Transpose(tt, 1, 0)
		if err != nil {
			return nil, err
		}
		tt = tensor.Materialize(tt)

		if err := tt.Reshape(spec.hidden, spec.numKHeads, spec.qkvzDim); err != nil {
			return nil, err
		}

		offset := 0
		qSlice, err := tt.Slice(nil, nil, tensor.S(offset, offset+spec.headKDim))
		if err != nil {
			return nil, err
		}
		offset += spec.headKDim
		kSlice, err := tt.Slice(nil, nil, tensor.S(offset, offset+spec.headKDim))
		if err != nil {
			return nil, err
		}
		offset += spec.headKDim
		vSlice, err := tt.Slice(nil, nil, tensor.S(offset, offset+vPerHead))
		if err != nil {
			return nil, err
		}
		offset += vPerHead
		zSlice, err := tt.Slice(nil, nil, tensor.S(offset, offset+vPerHead))
		if err != nil {
			return nil, err
		}

		qMat := tensor.Materialize(qSlice).(*tensor.Dense)
		kMat := tensor.Materialize(kSlice).(*tensor.Dense)
		vMat := tensor.Materialize(vSlice).(*tensor.Dense)
		zMat := tensor.Materialize(zSlice).(*tensor.Dense)

		if err := qMat.Reshape(spec.hidden, spec.numKHeads*spec.headKDim); err != nil {
			return nil, err
		}
		if err := kMat.Reshape(spec.hidden, spec.numKHeads*spec.headKDim); err != nil {
			return nil, err
		}
		if err := vMat.Reshape(spec.hidden, spec.numKHeads*vPerHead); err != nil {
			return nil, err
		}
		if err := zMat.Reshape(spec.hidden, spec.numKHeads*vPerHead); err != nil {
			return nil, err
		}

		var out tensor.Tensor
		if extractGate {
			out = zMat
		} else {
			out, err = tensor.Concat(1, qMat, kMat, vMat)
			if err != nil {
				return nil, err
			}
		}

		out = tensor.Materialize(out)
		out, err = tensor.Transpose(out, 1, 0)
		if err != nil {
			return nil, err
		}
		out = tensor.Materialize(out)

		if err := out.Reshape(out.Shape().TotalSize()); err != nil {
			return nil, err
		}

		return native.VectorF32(out.(*tensor.Dense))
	}
}

func (*qwen3NextModel) addOne(_ string, data []float32, shape []uint64) ([]float32, error) {
	n := tensor.New(tensor.WithShape(int(shape[0])), tensor.WithBacking(data))
	ones := tensor.Ones(tensor.Float32, int(shape[0]))

	n, err := n.Add(ones)
	if err != nil {
		return nil, err
	}

	ts, err := native.SelectF32(n, 0)
	if err != nil {
		return nil, err
	}

	var f32s []float32
	for _, t := range ts {
		f32s = append(f32s, t...)
	}

	return f32s, nil
}

func (q *qwen3NextModel) Replacements() []string {
	return []string{
		// Embeddings and output
		"lm_head", "output",
		"model.language_model.embed_tokens", "token_embd",
		"model.language_model.norm", "output_norm",
		"model.language_model.layers", "blk",
		"model.embed_tokens", "token_embd",
		"model.norm", "output_norm",
		"model.layers", "blk",

		// Vision
		"model.visual", "v",
		"patch_embed.proj", "patch_embed",
		"blocks", "blk",
		"attn.qkv", "attn_qkv",
		"attn.proj", "attn_out",
		"deepstack_merger_list", "deepstack_merger",

		// Layer norms
		"input_layernorm", "attn_norm",
		"post_attention_layernorm", "post_attention_norm",

		// Full attention (self_attn)
		"self_attn.q_proj", "attn_q",
		"self_attn.q_norm", "attn_q_norm",
		"self_attn.k_proj", "attn_k",
		"self_attn.k_norm", "attn_k_norm",
		"self_attn.v_proj", "attn_v",
		"self_attn.o_proj", "attn_output",

		// Linear attention (legacy qwen3next)
		"linear_attn.in_proj_qkvz", "ssm_in",
		"linear_attn.in_proj_ba", "ssm_ba",

		// Linear attention (qwen35)
		"linear_attn.in_proj_qkv", "attn_qkv",
		"linear_attn.in_proj_z", "attn_gate",
		"linear_attn.in_proj_a", "ssm_alpha",
		"linear_attn.in_proj_b", "ssm_beta",

		"linear_attn.conv1d", "ssm_conv1d",
		"linear_attn.dt_bias", "ssm_dt.bias",
		"linear_attn.dt_proj", "ssm_dt",
		"linear_attn.A_log", "ssm_a",
		"linear_attn.norm", "ssm_norm",
		"linear_attn.out_proj", "ssm_out",

		// MoE
		"mlp.gate.weight", "ffn_gate_inp.weight",
		"mlp.shared_expert.down_proj", "ffn_down_shexp",
		"mlp.shared_expert.gate_proj", "ffn_gate_shexp",
		"mlp.shared_expert.up_proj", "ffn_up_shexp",
		"mlp.shared_expert_gate", "ffn_gate_inp_shexp",

		// Dense FFN
		"mlp.down_proj", "ffn_down",
		"mlp.gate_proj", "ffn_gate",
		"mlp.up_proj", "ffn_up",
	}
}
