package mongo

import (
	"fmt"

	"go.mongodb.org/mongo-driver/bson"

	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/graph"
)

// deltaPayloadPart is one persisted chunk of file-level graph deltas.
type deltaPayloadPart struct {
	FileDeltas []graph.FileDelta
}

// splitDeltaPayload splits file deltas into BSON-safe parts.
func splitDeltaPayload(fileDeltas []graph.FileDelta, maxPartBytes int) ([]deltaPayloadPart, error) {
	maxPartBytes = effectiveMaxPartBytes(maxPartBytes)

	single := deltaPayloadPart{FileDeltas: fileDeltas}
	if size, err := estimateDeltaPartBSONSize(single); err == nil && size <= maxPartBytes {
		return []deltaPayloadPart{single}, nil
	}

	parts := make([]deltaPayloadPart, 0, len(fileDeltas))
	current := deltaPayloadPart{FileDeltas: make([]graph.FileDelta, 0)}

	flushCurrent := func() {
		if len(current.FileDeltas) == 0 {
			return
		}
		parts = append(parts, current)
		current = deltaPayloadPart{FileDeltas: make([]graph.FileDelta, 0)}
	}

	for _, fd := range fileDeltas {
		trial := append(append([]graph.FileDelta(nil), current.FileDeltas...), fd)
		size, err := estimateDeltaPartBSONSize(deltaPayloadPart{FileDeltas: trial})
		if err != nil {
			size = maxPartBytes + 1
		}

		if size <= maxPartBytes {
			current.FileDeltas = trial
			continue
		}

		if len(current.FileDeltas) > 0 {
			flushCurrent()
			size, err = estimateDeltaPartBSONSize(deltaPayloadPart{FileDeltas: []graph.FileDelta{fd}})
			if err != nil {
				size = maxPartBytes + 1
			}
			if size <= maxPartBytes {
				current.FileDeltas = []graph.FileDelta{fd}
				continue
			}
		}

		shards, err := splitFileDelta(fd, maxPartBytes)
		if err != nil {
			return nil, err
		}
		for _, shard := range shards {
			trial = append(append([]graph.FileDelta(nil), current.FileDeltas...), shard)
			size, err = estimateDeltaPartBSONSize(deltaPayloadPart{FileDeltas: trial})
			if err != nil {
				size = maxPartBytes + 1
			}
			if size > maxPartBytes && len(current.FileDeltas) > 0 {
				flushCurrent()
				trial = []graph.FileDelta{shard}
			}
			current.FileDeltas = trial
		}
	}

	if len(current.FileDeltas) > 0 {
		parts = append(parts, current)
	}

	if len(parts) == 0 {
		return []deltaPayloadPart{{FileDeltas: fileDeltas}}, nil
	}
	return parts, nil
}

// splitFileDelta shards a single oversized FileDelta into partial FileDelta values.
func splitFileDelta(fd graph.FileDelta, maxPartBytes int) ([]graph.FileDelta, error) {
	maxPartBytes = effectiveMaxPartBytes(maxPartBytes)

	if size, err := estimateDeltaPartBSONSize(deltaPayloadPart{FileDeltas: []graph.FileDelta{fd}}); err == nil && size <= maxPartBytes {
		return []graph.FileDelta{fd}, nil
	}

	parts := splitFileDeltaByEdges(fd, maxPartBytes)
	if len(parts) > 1 {
		return parts, nil
	}

	parts = splitFileDeltaByEdges(fd, maxPartBytes/2)
	if len(parts) > 1 {
		return parts, nil
	}

	if size, err := estimateDeltaPartBSONSize(deltaPayloadPart{FileDeltas: parts}); err == nil && size <= maxPartBytes {
		return parts, nil
	}

	return nil, fmt.Errorf("file delta for %q exceeds mongodb document limit", fd.FilePath)
}

func splitFileDeltaByEdges(fd graph.FileDelta, maxPartBytes int) []graph.FileDelta {
	base := graph.FileDelta{
		FilePath:         fd.FilePath,
		BaseArtifactID:   fd.BaseArtifactID,
		TargetArtifactID: fd.TargetArtifactID,
		AddedNodes:       append([]graph.Node(nil), fd.AddedNodes...),
		RemovedNodes:     append([]graph.Node(nil), fd.RemovedNodes...),
	}

	parts := make([]graph.FileDelta, 0)
	current := base
	includeNodes := true

	allEdges := append(append([]graph.Edge(nil), fd.AddedEdges...), fd.RemovedEdges...)
	addedEdgeCount := len(fd.AddedEdges)

	for i, edge := range allEdges {
		isAdded := i < addedEdgeCount
		trial := current
		if isAdded {
			trial.AddedEdges = append(append([]graph.Edge(nil), current.AddedEdges...), edge)
			trial.RemovedEdges = append([]graph.Edge(nil), current.RemovedEdges...)
		} else {
			trial.AddedEdges = append([]graph.Edge(nil), current.AddedEdges...)
			trial.RemovedEdges = append(append([]graph.Edge(nil), current.RemovedEdges...), edge)
		}
		if !includeNodes {
			trial.AddedNodes = nil
			trial.RemovedNodes = nil
		}

		size, err := estimateDeltaPartBSONSize(deltaPayloadPart{FileDeltas: []graph.FileDelta{trial}})
		if err != nil {
			size = maxPartBytes + 1
		}

		hasEdges := len(current.AddedEdges)+len(current.RemovedEdges) > 0
		if size > maxPartBytes && hasEdges {
			parts = append(parts, current)
			current = graph.FileDelta{
				FilePath:         fd.FilePath,
				BaseArtifactID:   fd.BaseArtifactID,
				TargetArtifactID: fd.TargetArtifactID,
			}
			includeNodes = false
			if isAdded {
				current.AddedEdges = []graph.Edge{edge}
			} else {
				current.RemovedEdges = []graph.Edge{edge}
			}
			continue
		}

		current = trial
		if !includeNodes {
			current.AddedNodes = nil
			current.RemovedNodes = nil
		}
	}

	if len(current.AddedNodes) > 0 || len(current.RemovedNodes) > 0 ||
		len(current.AddedEdges) > 0 || len(current.RemovedEdges) > 0 {
		parts = append(parts, current)
	}

	if len(parts) == 0 {
		return []graph.FileDelta{fd}
	}
	return parts
}

func reassembleDeltaPayload(parts []deltaPayloadPart) []graph.FileDelta {
	merged := make([]graph.FileDelta, 0)
	for _, part := range parts {
		merged = append(merged, part.FileDeltas...)
	}
	return mergeFileDeltasByPath(merged)
}

func mergeFileDeltasByPath(fileDeltas []graph.FileDelta) []graph.FileDelta {
	if len(fileDeltas) == 0 {
		return nil
	}

	order := make([]string, 0, len(fileDeltas))
	index := make(map[string]int, len(fileDeltas))
	out := make([]graph.FileDelta, 0, len(fileDeltas))

	for _, fd := range fileDeltas {
		path := fd.FilePath
		if idx, ok := index[path]; ok {
			existing := &out[idx]
			existing.AddedNodes = append(existing.AddedNodes, fd.AddedNodes...)
			existing.RemovedNodes = append(existing.RemovedNodes, fd.RemovedNodes...)
			existing.AddedEdges = append(existing.AddedEdges, fd.AddedEdges...)
			existing.RemovedEdges = append(existing.RemovedEdges, fd.RemovedEdges...)
			if existing.BaseArtifactID == "" {
				existing.BaseArtifactID = fd.BaseArtifactID
			}
			if existing.TargetArtifactID == "" {
				existing.TargetArtifactID = fd.TargetArtifactID
			}
			continue
		}

		index[path] = len(out)
		order = append(order, path)
		out = append(out, graph.FileDelta{
			FilePath:         fd.FilePath,
			BaseArtifactID:   fd.BaseArtifactID,
			TargetArtifactID: fd.TargetArtifactID,
			AddedNodes:       append([]graph.Node(nil), fd.AddedNodes...),
			RemovedNodes:     append([]graph.Node(nil), fd.RemovedNodes...),
			AddedEdges:       append([]graph.Edge(nil), fd.AddedEdges...),
			RemovedEdges:     append([]graph.Edge(nil), fd.RemovedEdges...),
		})
	}

	return out
}

func resolveDeltaChunked(docChunked bool, partCount, inlineFileDeltas int) bool {
	if docChunked {
		return true
	}
	if partCount > 0 && inlineFileDeltas == 0 {
		return true
	}
	return false
}

func estimateDeltaPartBSONSize(part deltaPayloadPart) (int, error) {
	data, err := bson.Marshal(bson.M{
		"file_deltas": part.FileDeltas,
	})
	if err != nil {
		return 0, fmt.Errorf("marshal delta part: %w", err)
	}
	return len(data), nil
}

func deltaCounts(fileDeltas []graph.FileDelta) (addedNodes, removedNodes, changedEdges int) {
	for _, fd := range fileDeltas {
		addedNodes += len(fd.AddedNodes)
		removedNodes += len(fd.RemovedNodes)
		changedEdges += len(fd.AddedEdges) + len(fd.RemovedEdges)
	}
	return addedNodes, removedNodes, changedEdges
}
