package parser

import (
	"context"
	"fmt"
	"strings"

	contractevents "bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/contracts/events"
	"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/validate"
	"bit.admedia.com/scm/ad/adpilot-indexing-code-parser.com/internal/store"
)

type snapshotContext struct {
	repoID    string
	commitSHA string
	fileCount int
}

// ProcessRepoSnapshotReady registers snapshot metadata and attempts finalization.
func (s *Service) ProcessRepoSnapshotReady(ctx context.Context, event contractevents.RepoSnapshotReadyEvent) error {
	s.registerSnapshot(event.SnapshotID, snapshotContext{
		repoID:    event.RepoID,
		commitSHA: event.CommitSHA,
		fileCount: event.FileCount,
	})
	return s.maybeFinalizeSnapshot(ctx, event.RepoID, event.SnapshotID)
}

// ProcessDocsParseCompleted retries snapshot finalization after docs fan-in completes.
func (s *Service) ProcessDocsParseCompleted(ctx context.Context, event contractevents.DocsParseCompletedEvent) error {
	return s.maybeFinalizeSnapshot(ctx, event.RepoID, event.SnapshotID)
}

func (s *Service) registerSnapshot(snapshotID string, ctx snapshotContext) {
	s.snapshotsMu.Lock()
	defer s.snapshotsMu.Unlock()
	if s.snapshots == nil {
		s.snapshots = make(map[string]snapshotContext)
	}
	s.snapshots[snapshotID] = ctx
}

func (s *Service) maybeFinalizeSnapshot(ctx context.Context, repoID, snapshotID string) error {
	if s.finalize == nil {
		return nil
	}
	if err := ctx.Err(); err != nil {
		return err
	}

	_, artifactID, exists, err := s.finalize.GetSnapshotGraphBySnapshot(ctx, repoID, snapshotID)
	if err != nil {
		return fmt.Errorf("check snapshot graph: %w", err)
	}
	if exists {
		s.logger().Info("snapshot graph already finalized",
			"repo_id", repoID,
			"snapshot_id", snapshotID,
			"artifact_id", artifactID,
		)
		return nil
	}

	run, err := s.finalize.GetIndexingRun(ctx, repoID, snapshotID)
	if err != nil {
		// Fan-in requires indexing run tracking from repo sync.
		s.logger().Debug("finalization waiting for indexing run",
			"repo_id", repoID,
			"snapshot_id", snapshotID,
			"error", err,
		)
		return nil
	}

	codeReady, err := s.codeParseReady(ctx, repoID, snapshotID)
	if err != nil {
		return err
	}
	if !codeReady {
		return nil
	}

	run, err = s.finalize.GetIndexingRun(ctx, repoID, snapshotID)
	if err != nil {
		return nil
	}

	// Artifact-completeness gate.
	//
	// codeReady is driven by the fan-in path/skip accounting
	// (processed_code_file_paths), which reaches ExpectedCodeFiles as soon as every
	// file has reached SOME terminal outcome. But the per-file code_graph_artifacts
	// that buildMergedSnapshotGraph actually merges are written on a separate path
	// and lag behind: skip/duplicate file events record a path (advancing fan-in)
	// without producing an artifact, so codeReady can go true while only a handful
	// of artifacts exist. Sealing then froze a truncated graph that the terminal
	// guard above made permanent, and embeddings — triggered once at seal — captured
	// only the few chunks that existed at that instant (the root cause of repos
	// indexing at a few % of their files).
	//
	// Gate the seal on the artifacts having caught up to the parseable target
	// (expected minus permanently-failed files, which never produce an artifact).
	// SaveArtifact runs synchronously BEFORE the per-file finalize trigger, so the
	// event that parses the final file observes the complete artifact set — this
	// seals exactly once, with the full graph.
	if run.ExpectedCodeFiles > 0 {
		artifactCount, err := s.finalize.CountCodeArtifacts(ctx, repoID, snapshotID)
		if err != nil {
			return err
		}
		// Files that count toward ExpectedCodeFiles but never produce an artifact:
		//  - failed_files: permanent parse failures
		//  - skipped_code_files: ignored / deleted / unsupported-language files
		// Both are accounted in the fan-in but yield no code_graph_artifact, so the
		// artifact target must exclude them. Otherwise a repo with any skipped code
		// file (e.g. an unsupported extension) can never reach ExpectedCodeFiles
		// artifacts and finalize hangs forever (graph never sealed, no embeddings).
		skipped, err := s.finalize.CountSkippedCodeFilePaths(ctx, repoID, snapshotID)
		if err != nil {
			return err
		}
		target := run.ExpectedCodeFiles - run.FailedFiles - skipped
		if artifactCount < target {
			s.logger().Debug("deferring snapshot finalize until code artifacts catch up",
				"repo_id", repoID,
				"snapshot_id", snapshotID,
				"artifact_count", artifactCount,
				"expected_code_files", run.ExpectedCodeFiles,
				"failed_files", run.FailedFiles,
				"skipped_code_files", skipped,
				"target", target,
			)
			return nil
		}
	}

	if run.ExpectedDocsFiles > 0 && run.ProcessedDocsFiles < run.ExpectedDocsFiles {
		completed, countErr := s.finalize.CountCompletedDocParseRuns(ctx, repoID, snapshotID)
		if countErr == nil && completed >= run.ExpectedDocsFiles {
			if syncErr := s.finalize.SyncDocsParseCounter(ctx, repoID, snapshotID, completed); syncErr != nil {
				return syncErr
			}
			run.ProcessedDocsFiles = completed
		} else {
			// Do NOT block code-graph finalization on the docs stage.
			// docs-ingestion is a separate, sometimes-unavailable service (P0);
			// its nodes are purely additive to the merged graph. Previously this
			// returned nil and stalled the snapshot forever (no graph, no
			// embeddings) whenever docs never completed. Proceed with a
			// docs-degraded, code-only finalize so the snapshot still produces a
			// graph + embeddings; docs merge in on a later re-index once available.
			s.logger().Warn("finalizing snapshot with incomplete docs stage",
				"repo_id", repoID,
				"snapshot_id", snapshotID,
				"expected_docs", run.ExpectedDocsFiles,
				"processed_docs", run.ProcessedDocsFiles,
			)
		}
	}

	merged, commitSHA, err := s.buildMergedSnapshotGraph(ctx, run)
	if err != nil {
		return err
	}

	artifactID = store.BuildMergedSnapshotArtifactID(snapshotID)
	savedID, artifactURI, err := s.finalize.SaveSnapshotGraph(ctx, merged, artifactID)
	if err != nil {
		return fmt.Errorf("save snapshot graph: %w", err)
	}

	if err := s.publishMergedGraphArtifactReady(ctx, run, savedID, artifactURI, merged); err != nil {
		return err
	}

	if err := s.finalize.SetGraphFinalizeCompleted(ctx, repoID, snapshotID); err != nil {
		return fmt.Errorf("update indexing run graph_finalize: %w", err)
	}

	s.logger().Info("snapshot graph finalized",
		"repo_id", repoID,
		"snapshot_id", snapshotID,
		"commit_sha", commitSHA,
		"artifact_id", savedID,
		"artifact_uri", artifactURI,
		"node_count", len(merged.Nodes),
		"edge_count", len(merged.Edges),
	)
	return nil
}

func (s *Service) buildMergedSnapshotGraph(ctx context.Context, run IndexingRunSnapshot) (graph.Artifact, string, error) {
	fileArtifacts, err := s.finalize.ListArtifactsBySnapshot(ctx, run.RepoID, run.SnapshotID)
	if err != nil {
		return graph.Artifact{}, "", fmt.Errorf("list code artifacts: %w", err)
	}

	commitSHA := run.CommitSHA
	s.snapshotsMu.Lock()
	if ctxEntry, ok := s.snapshots[run.SnapshotID]; ok && strings.TrimSpace(ctxEntry.commitSHA) != "" {
		commitSHA = ctxEntry.commitSHA
	}
	s.snapshotsMu.Unlock()

	merged := graph.Artifact{
		SchemaVersion: graph.SchemaVersionV1,
		RepoID:        run.RepoID,
		SnapshotID:    run.SnapshotID,
		CommitSHA:     commitSHA,
	}

	nodeIndex := make(map[string]struct{})
	edgeIndex := make(map[string]struct{})
	addNode := func(node graph.Node) {
		if _, ok := nodeIndex[node.ID]; ok {
			return
		}
		nodeIndex[node.ID] = struct{}{}
		merged.Nodes = append(merged.Nodes, node)
	}
	addEdge := func(edge graph.Edge) {
		if _, ok := edgeIndex[edge.ID]; ok {
			return
		}
		edgeIndex[edge.ID] = struct{}{}
		merged.Edges = append(merged.Edges, edge)
	}

	addNode(graph.Node{
		ID:   repositoryNodeID(run.RepoID),
		Kind: graph.NodeKindRepository,
		Name: run.RepoID,
	})
	addNode(graph.Node{
		ID:   run.SnapshotID,
		Kind: graph.NodeKindSnapshot,
		Name: run.SnapshotID,
	})
	addEdge(graph.Edge{
		ID:       fmt.Sprintf("edge_repo_snapshot_%s", run.SnapshotID),
		Kind:     graph.EdgeKindContains,
		SourceID: repositoryNodeID(run.RepoID),
		TargetID: run.SnapshotID,
	})

	for _, artifact := range fileArtifacts {
		for _, node := range artifact.Nodes {
			addNode(node)
		}
		for _, edge := range artifact.Edges {
			addEdge(edge)
		}
	}

	docSections, err := s.finalize.ListDocSections(ctx, run.RepoID, run.SnapshotID)
	if err != nil {
		return graph.Artifact{}, "", fmt.Errorf("list doc sections: %w", err)
	}
	docChunks, err := s.finalize.ListDocChunks(ctx, run.RepoID, run.SnapshotID)
	if err != nil {
		return graph.Artifact{}, "", fmt.Errorf("list doc chunks: %w", err)
	}

	docFileIDs := make(map[string]string)
	for _, section := range docSections {
		docFileID, ok := docFileIDs[section.DocPath]
		if !ok {
			docFileID = docFileNodeID(section.DocPath)
			docFileIDs[section.DocPath] = docFileID
			addNode(graph.Node{
				ID:   docFileID,
				Kind: graph.NodeKindDocFile,
				Name: section.DocPath,
				Path: section.DocPath,
			})
			addEdge(graph.Edge{
				ID:       fmt.Sprintf("edge_snapshot_doc_%s", hashKey(section.DocPath)),
				Kind:     graph.EdgeKindContains,
				SourceID: run.SnapshotID,
				TargetID: docFileID,
			})
		}

		sectionID := docSectionNodeID(section.DocPath, section.SectionTitle)
		if section.SectionID != "" {
			sectionID = fmt.Sprintf("doc_sec_%s", hashKey(section.SectionID))
		}
		addNode(graph.Node{
			ID:   sectionID,
			Kind: graph.NodeKindDocSection,
			Name: section.SectionTitle,
			Path: section.DocPath,
		})
		addEdge(graph.Edge{
			ID:       fmt.Sprintf("edge_doc_section_%s", hashKey(sectionID)),
			Kind:     graph.EdgeKindHasSection,
			SourceID: docFileID,
			TargetID: sectionID,
		})
	}

	for _, chunk := range docChunks {
		chunkID := chunk.ChunkID
		if strings.TrimSpace(chunkID) == "" {
			chunkID = fmt.Sprintf("chk_doc_%s", hashKey(chunk.DocPath, chunk.SectionTitle))
		}
		sectionID := docSectionNodeID(chunk.DocPath, chunk.SectionTitle)
		addNode(graph.Node{
			ID:   chunkID,
			Kind: graph.NodeKindDocChunk,
			Name: chunk.SectionTitle,
			Path: chunk.DocPath,
		})
		addEdge(graph.Edge{
			ID:       fmt.Sprintf("edge_doc_chunk_%s", hashKey(chunkID)),
			Kind:     graph.EdgeKindHasChunk,
			SourceID: sectionID,
			TargetID: chunkID,
		})
	}

	return merged, commitSHA, nil
}

func (s *Service) publishMergedGraphArtifactReady(
	ctx context.Context,
	run IndexingRunSnapshot,
	artifactID, artifactURI string,
	merged graph.Artifact,
) error {
	if s.events == nil {
		return nil
	}

	event := contractevents.NewGraphArtifactReadyEvent(
		fmt.Sprintf("evt_%s_graph_ready", run.SnapshotID),
		run.RepoID,
		run.SnapshotID,
		run.CommitSHA,
		artifactID,
		artifactURI,
		s.cfg.ParserVersion,
		GraphSchemaVersion,
	)
	event.NodeCount = len(merged.Nodes)
	event.EdgeCount = len(merged.Edges)

	if err := validate.GraphArtifactReady(event); err != nil {
		return fmt.Errorf("validate graph.artifact.ready: %w", err)
	}
	if err := s.events.PublishGraphArtifactReady(ctx, event); err != nil {
		return fmt.Errorf("publish graph.artifact.ready: %w", err)
	}
	return nil
}

func repositoryNodeID(repoID string) string {
	return fmt.Sprintf("repo_%s", strings.ReplaceAll(repoID, "/", "_"))
}

func docFileNodeID(docPath string) string {
	return fmt.Sprintf("doc_%s", hashKey(docPath))
}

func docSectionNodeID(docPath, sectionTitle string) string {
	return fmt.Sprintf("doc_sec_%s", hashKey(docPath, sectionTitle))
}
