package graph

import "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/metadata"

const DeltaSchemaVersionV1 = "v1"

// Delta captures graph changes between two commits for downstream analysis.
type Delta struct {
	SchemaVersion   string                   `json:"schema_version"`
	RepoID          string                   `json:"repo_id"`
	SnapshotID      string                   `json:"snapshot_id"`
	BaseCommitSHA   string                   `json:"base_commit_sha"`
	TargetCommitSHA string                   `json:"target_commit_sha"`
	ChangedFiles    []string                 `json:"changed_files"`
	FileDeltas      []FileDelta              `json:"file_deltas"`
	GitCommit       metadata.GitCommitMetadata `json:"git_commit,omitempty"`
}

// FileDelta describes graph changes for a single file between base and target artifacts.
type FileDelta struct {
	FilePath         string `json:"file_path"`
	BaseArtifactID   string `json:"base_artifact_id,omitempty"`
	TargetArtifactID string `json:"target_artifact_id,omitempty"`
	AddedNodes       []Node `json:"added_nodes,omitempty"`
	RemovedNodes     []Node `json:"removed_nodes,omitempty"`
	AddedEdges       []Edge `json:"added_edges,omitempty"`
	RemovedEdges     []Edge `json:"removed_edges,omitempty"`
}

// ComputeFileDelta compares two file-level graph artifacts.
func ComputeFileDelta(base, target Artifact, filePath, baseArtifactID, targetArtifactID string) FileDelta {
	baseNodeIDs := nodeIDSet(base.Nodes)
	targetNodeIDs := nodeIDSet(target.Nodes)
	baseEdgeIDs := edgeIDSet(base.Edges)
	targetEdgeIDs := edgeIDSet(target.Edges)

	return FileDelta{
		FilePath:         filePath,
		BaseArtifactID:   baseArtifactID,
		TargetArtifactID: targetArtifactID,
		AddedNodes:       nodesWithIDs(target.Nodes, diffAdded(baseNodeIDs, targetNodeIDs)),
		RemovedNodes:     nodesWithIDs(base.Nodes, diffRemoved(baseNodeIDs, targetNodeIDs)),
		AddedEdges:       edgesWithIDs(target.Edges, diffAdded(baseEdgeIDs, targetEdgeIDs)),
		RemovedEdges:     edgesWithIDs(base.Edges, diffRemoved(baseEdgeIDs, targetEdgeIDs)),
	}
}

// AggregateDelta builds a commit-level delta from per-file deltas.
func AggregateDelta(repoID, snapshotID, baseSHA, targetSHA string, changedFiles []string, fileDeltas []FileDelta) Delta {
	return Delta{
		SchemaVersion:   DeltaSchemaVersionV1,
		RepoID:          repoID,
		SnapshotID:      snapshotID,
		BaseCommitSHA:   baseSHA,
		TargetCommitSHA: targetSHA,
		ChangedFiles:    append([]string(nil), changedFiles...),
		FileDeltas:      append([]FileDelta(nil), fileDeltas...),
	}
}

// Counts returns aggregate node/edge change counts across all file deltas.
func (d Delta) Counts() (addedNodes, removedNodes, changedEdges int) {
	for _, fileDelta := range d.FileDeltas {
		addedNodes += len(fileDelta.AddedNodes)
		removedNodes += len(fileDelta.RemovedNodes)
		changedEdges += len(fileDelta.AddedEdges) + len(fileDelta.RemovedEdges)
	}
	return addedNodes, removedNodes, changedEdges
}

func nodeIDSet(nodes []Node) map[string]struct{} {
	set := make(map[string]struct{}, len(nodes))
	for _, node := range nodes {
		if node.ID == "" {
			continue
		}
		set[node.ID] = struct{}{}
	}
	return set
}

func edgeIDSet(edges []Edge) map[string]struct{} {
	set := make(map[string]struct{}, len(edges))
	for _, edge := range edges {
		if edge.ID == "" {
			continue
		}
		set[edge.ID] = struct{}{}
	}
	return set
}

func diffAdded(base, target map[string]struct{}) map[string]struct{} {
	added := make(map[string]struct{})
	for id := range target {
		if _, ok := base[id]; !ok {
			added[id] = struct{}{}
		}
	}
	return added
}

func diffRemoved(base, target map[string]struct{}) map[string]struct{} {
	removed := make(map[string]struct{})
	for id := range base {
		if _, ok := target[id]; !ok {
			removed[id] = struct{}{}
		}
	}
	return removed
}

func nodesWithIDs(nodes []Node, ids map[string]struct{}) []Node {
	if len(ids) == 0 {
		return nil
	}
	out := make([]Node, 0, len(ids))
	for _, node := range nodes {
		if _, ok := ids[node.ID]; ok {
			out = append(out, node)
		}
	}
	return out
}

func edgesWithIDs(edges []Edge, ids map[string]struct{}) []Edge {
	if len(ids) == 0 {
		return nil
	}
	out := make([]Edge, 0, len(ids))
	for _, edge := range edges {
		if _, ok := ids[edge.ID]; ok {
			out = append(out, edge)
		}
	}
	return out
}
