package mongo

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

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

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

const collectionGraphDeltaParts = "graph_delta_parts"

type graphDeltaDoc struct {
	DeltaID          string                     `bson:"delta_id"`
	RepoID           string                     `bson:"repo_id"`
	SnapshotID       string                     `bson:"snapshot_id"`
	BaseCommitSHA    string                     `bson:"base_commit_sha"`
	TargetCommitSHA  string                     `bson:"target_commit_sha"`
	SchemaVersion    string                     `bson:"schema_version"`
	ChangedFiles     []string                   `bson:"changed_files,omitempty"`
	FileDeltas       []graph.FileDelta          `bson:"file_deltas,omitempty"`
	GitCommit        metadata.GitCommitMetadata `bson:"git_commit,omitempty"`
	Chunked          bool                       `bson:"chunked,omitempty"`
	PartCount        int                        `bson:"part_count,omitempty"`
	FileDeltaCount   int                        `bson:"file_delta_count,omitempty"`
	AddedNodeCount   int                        `bson:"added_node_count,omitempty"`
	RemovedNodeCount int                        `bson:"removed_node_count,omitempty"`
	ChangedEdgeCount int                        `bson:"changed_edge_count,omitempty"`
	UpdatedAt        time.Time                  `bson:"updated_at"`
}

type graphDeltaPartDoc struct {
	DeltaID    string            `bson:"delta_id"`
	PartIndex  int               `bson:"part_index"`
	FileDeltas []graph.FileDelta `bson:"file_deltas,omitempty"`
	UpdatedAt  time.Time         `bson:"updated_at"`
}

// SaveDelta upserts a graph delta artifact, chunking when the payload exceeds MongoDB limits.
func (s *Store) SaveDelta(ctx context.Context, delta graph.Delta) (string, string, error) {
	if err := ctx.Err(); err != nil {
		return "", "", err
	}
	if strings.TrimSpace(delta.RepoID) == "" {
		return "", "", errors.New("delta repo_id is required")
	}
	if strings.TrimSpace(delta.BaseCommitSHA) == "" || strings.TrimSpace(delta.TargetCommitSHA) == "" {
		return "", "", errors.New("delta base and target commit_sha are required")
	}
	if delta.SchemaVersion == "" {
		delta.SchemaVersion = graph.DeltaSchemaVersionV1
	}

	id := store.BuildDeltaID(delta.RepoID, delta.BaseCommitSHA, delta.TargetCommitSHA)
	parts, err := splitDeltaPayload(delta.FileDeltas, s.maxPartBytes)
	if err != nil {
		return "", "", fmt.Errorf("split graph delta: %w", err)
	}

	if len(parts) == 1 {
		doc := buildGraphDeltaManifest(id, delta, delta.FileDeltas, false, 0)
		if err := s.upsertGraphDeltaManifest(ctx, doc); err != nil {
			if mongoIsDocumentTooLarge(err) {
				return s.saveChunkedGraphDelta(ctx, id, delta, parts)
			}
			return "", "", fmt.Errorf("upsert graph delta: %w", err)
		}
		if err := s.deleteDeltaParts(ctx, id); err != nil {
			return "", "", err
		}
		return id, store.MongoURI(collectionGraphDeltas, id), nil
	}

	return s.saveChunkedGraphDelta(ctx, id, delta, parts)
}

func (s *Store) saveChunkedGraphDelta(ctx context.Context, id string, delta graph.Delta, parts []deltaPayloadPart) (string, string, error) {
	if len(parts) == 1 {
		parts, _ = splitDeltaPayload(delta.FileDeltas, s.maxPartBytes/2)
	}
	if len(parts) == 1 {
		if size, err := estimateDeltaPartBSONSize(parts[0]); err == nil && size > effectiveMaxPartBytes(s.maxPartBytes) {
			return "", "", fmt.Errorf("upsert graph delta: graph delta payload exceeds mongodb document limit")
		}
	}

	fileDeltas := reassembleDeltaPayload(parts)
	doc := buildGraphDeltaManifest(id, delta, fileDeltas, true, len(parts))
	doc.FileDeltas = nil

	if err := s.upsertGraphDeltaManifest(ctx, doc); err != nil {
		return "", "", fmt.Errorf("upsert graph delta manifest: %w", err)
	}
	if err := s.saveDeltaParts(ctx, id, parts); err != nil {
		return "", "", fmt.Errorf("save graph delta parts: %w", err)
	}
	return id, store.MongoURI(collectionGraphDeltas, id), nil
}

func buildGraphDeltaManifest(id string, delta graph.Delta, fileDeltas []graph.FileDelta, chunked bool, partCount int) graphDeltaDoc {
	added, removed, edges := deltaCounts(fileDeltas)
	return graphDeltaDoc{
		DeltaID:          id,
		RepoID:           delta.RepoID,
		SnapshotID:       delta.SnapshotID,
		BaseCommitSHA:    delta.BaseCommitSHA,
		TargetCommitSHA:  delta.TargetCommitSHA,
		SchemaVersion:    delta.SchemaVersion,
		ChangedFiles:     delta.ChangedFiles,
		FileDeltas:       fileDeltas,
		GitCommit:        delta.GitCommit,
		Chunked:          chunked,
		PartCount:        partCount,
		FileDeltaCount:   len(fileDeltas),
		AddedNodeCount:   added,
		RemovedNodeCount: removed,
		ChangedEdgeCount: edges,
		UpdatedAt:        time.Now().UTC(),
	}
}

func (s *Store) upsertGraphDeltaManifest(ctx context.Context, doc graphDeltaDoc) error {
	_, err := s.collection(collectionGraphDeltas).UpdateOne(
		ctx,
		bson.M{"delta_id": doc.DeltaID},
		bson.M{"$set": doc},
		options.Update().SetUpsert(true),
	)
	return err
}

func (s *Store) saveDeltaParts(ctx context.Context, deltaID string, parts []deltaPayloadPart) error {
	col := s.collection(collectionGraphDeltaParts)
	now := time.Now().UTC()

	for i, part := range parts {
		doc := graphDeltaPartDoc{
			DeltaID:    deltaID,
			PartIndex:  i,
			FileDeltas: part.FileDeltas,
			UpdatedAt:  now,
		}
		_, err := col.UpdateOne(
			ctx,
			bson.M{"delta_id": deltaID, "part_index": i},
			bson.M{"$set": doc},
			options.Update().SetUpsert(true),
		)
		if err != nil {
			return fmt.Errorf("upsert graph delta part %d: %w", i, err)
		}
	}

	return s.deleteDeltaPartsFromIndex(ctx, deltaID, len(parts))
}

func (s *Store) loadDeltaParts(ctx context.Context, deltaID string, partCount int) ([]deltaPayloadPart, error) {
	if partCount <= 0 {
		return nil, fmt.Errorf("part_count must be positive for chunked delta %s", deltaID)
	}

	col := s.collection(collectionGraphDeltaParts)
	cursor, err := col.Find(ctx, bson.M{"delta_id": deltaID}, options.Find().SetSort(bson.D{{Key: "part_index", Value: 1}}))
	if err != nil {
		return nil, fmt.Errorf("find graph delta parts: %w", err)
	}
	defer cursor.Close(ctx)

	parts := make([]deltaPayloadPart, partCount)
	loaded := 0
	for cursor.Next(ctx) {
		var doc graphDeltaPartDoc
		if err := cursor.Decode(&doc); err != nil {
			return nil, fmt.Errorf("decode graph delta part: %w", err)
		}
		if doc.PartIndex < 0 || doc.PartIndex >= partCount {
			continue
		}
		parts[doc.PartIndex] = deltaPayloadPart{FileDeltas: doc.FileDeltas}
		loaded++
	}
	if err := cursor.Err(); err != nil {
		return nil, fmt.Errorf("iterate graph delta parts: %w", err)
	}
	if loaded != partCount {
		return nil, fmt.Errorf("expected %d graph delta parts for %s, found %d", partCount, deltaID, loaded)
	}
	return parts, nil
}

func (s *Store) deleteDeltaParts(ctx context.Context, deltaID string) error {
	_, err := s.collection(collectionGraphDeltaParts).DeleteMany(ctx, bson.M{"delta_id": deltaID})
	if err != nil {
		return fmt.Errorf("delete graph delta parts for %s: %w", deltaID, err)
	}
	return nil
}

func (s *Store) deleteDeltaPartsFromIndex(ctx context.Context, deltaID string, fromIndex int) error {
	_, err := s.collection(collectionGraphDeltaParts).DeleteMany(ctx, bson.M{
		"delta_id":   deltaID,
		"part_index": bson.M{"$gte": fromIndex},
	})
	if err != nil {
		return fmt.Errorf("delete trailing graph delta parts for %s: %w", deltaID, err)
	}
	return nil
}

func (s *Store) deltaFromDoc(ctx context.Context, doc graphDeltaDoc) (graph.Delta, error) {
	fileDeltas := doc.FileDeltas
	if resolveDeltaChunked(doc.Chunked, doc.PartCount, len(doc.FileDeltas)) {
		parts, err := s.loadDeltaParts(ctx, doc.DeltaID, doc.PartCount)
		if err != nil {
			return graph.Delta{}, err
		}
		fileDeltas = reassembleDeltaPayload(parts)
	}

	schema := doc.SchemaVersion
	if schema == "" {
		schema = graph.DeltaSchemaVersionV1
	}

	return graph.Delta{
		SchemaVersion:   schema,
		RepoID:          doc.RepoID,
		SnapshotID:      doc.SnapshotID,
		BaseCommitSHA:   doc.BaseCommitSHA,
		TargetCommitSHA: doc.TargetCommitSHA,
		ChangedFiles:    doc.ChangedFiles,
		FileDeltas:      fileDeltas,
		GitCommit:       doc.GitCommit,
	}, nil
}

// LoadDeltaByID loads a graph delta by deterministic delta artifact ID.
func (s *Store) LoadDeltaByID(ctx context.Context, deltaID string) (graph.Delta, error) {
	var empty graph.Delta
	if err := ctx.Err(); err != nil {
		return empty, err
	}

	var doc graphDeltaDoc
	err := s.collection(collectionGraphDeltas).FindOne(ctx, bson.M{"delta_id": deltaID}).Decode(&doc)
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return empty, fmt.Errorf("%w: %s", store.ErrArtifactNotFound, deltaID)
		}
		return empty, fmt.Errorf("find graph delta: %w", err)
	}

	return s.deltaFromDoc(ctx, doc)
}
