package mongo

import (
	"context"
	"errors"
	"fmt"

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

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

const collectionSnapshotFiles = "snapshot_files"

// CountProcessedCodeFilePaths returns distinct code file paths with recorded outcomes.
func (s *Store) CountProcessedCodeFilePaths(ctx context.Context, repoID, snapshotID string) (int, error) {
	if !s.indexingRunsEnabled {
		return 0, nil
	}
	if err := ctx.Err(); err != nil {
		return 0, err
	}

	filter := bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
	}

	doc, err := s.indexingRunsCollection(collectionIndexingRuns).FindOne(ctx, filter).DecodeBytes()
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return 0, parser.ErrIndexingRunNotReady
		}
		return 0, fmt.Errorf("load indexing run paths: %w", err)
	}

	paths, ok := doc.Lookup("processed_code_file_paths").ArrayOK()
	if !ok {
		return 0, nil
	}
	return len(paths), nil
}

// CountSkippedCodeFilePaths returns the number of code files that counted toward
// the fan-in but produced no artifact (ignored / deleted / unsupported-language).
// The finalize artifact-completeness gate subtracts these from its target so a
// repo with skipped code files can still seal (they never yield an artifact).
func (s *Store) CountSkippedCodeFilePaths(ctx context.Context, repoID, snapshotID string) (int, error) {
	if !s.indexingRunsEnabled {
		return 0, nil
	}
	if err := ctx.Err(); err != nil {
		return 0, err
	}

	filter := bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
	}

	doc, err := s.indexingRunsCollection(collectionIndexingRuns).FindOne(ctx, filter).DecodeBytes()
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return 0, nil
		}
		return 0, fmt.Errorf("load skipped code file paths: %w", err)
	}

	paths, ok := doc.Lookup("skipped_code_file_paths").ArrayOK()
	if !ok {
		return 0, nil
	}
	return len(paths), nil
}

// CountSnapshotCodeFiles counts code files in the repo-sync snapshot inventory.
func (s *Store) CountSnapshotCodeFiles(ctx context.Context, repoID, snapshotID string) (int, error) {
	if !s.indexingRunsEnabled {
		return 0, nil
	}
	if err := ctx.Err(); err != nil {
		return 0, err
	}

	count, err := s.indexingRunsCollection(collectionSnapshotFiles).CountDocuments(ctx, bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
		"file_kind":   "code",
	})
	if err != nil {
		return 0, fmt.Errorf("count snapshot code files: %w", err)
	}
	return int(count), nil
}

// SyncCodeParseFanIn aligns counters and stage state when fan-in is complete.
func (s *Store) SyncCodeParseFanIn(ctx context.Context, repoID, snapshotID string, failureThreshold float64) error {
	if !s.indexingRunsEnabled {
		return nil
	}
	if err := ctx.Err(); err != nil {
		return err
	}

	filter := bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
	}

	var run struct {
		ProcessedCodeFiles int `bson:"processed_code_files"`
		ExpectedCodeFiles  int `bson:"expected_code_files"`
		ExpectedDocsFiles  int `bson:"expected_docs_files"`
		FailedFiles        int `bson:"failed_files"`
	}
	col := s.indexingRunsCollection(collectionIndexingRuns)
	if err := col.FindOne(ctx, filter).Decode(&run); err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return parser.ErrIndexingRunNotReady
		}
		return fmt.Errorf("load indexing run: %w", err)
	}

	pathCount, err := s.CountProcessedCodeFilePaths(ctx, repoID, snapshotID)
	if err != nil {
		return err
	}
	snapshotCodeFiles, err := s.CountSnapshotCodeFiles(ctx, repoID, snapshotID)
	if err != nil {
		return err
	}

	if !parser.CodeParseFanInComplete(run.ExpectedCodeFiles, run.ProcessedCodeFiles, pathCount, snapshotCodeFiles) {
		return nil
	}

	// The distinct processed-path set (processed_code_file_paths) is the single
	// source of truth for how many code files have been accounted for.
	// Reconcile the scalar counter to pathCount UNCONDITIONALLY. Previously this
	// pinned processed_code_files to ExpectedCodeFiles, which let the guarded
	// per-file $inc updates stack on top of the pinned value and inflate the
	// counter to ~2x the real distinct count (tripping premature finalize).
	// Setting it to pathCount also corrects an already-inflated counter downward.
	syncTo := pathCount

	stageStatus := parser.CodeParseStageStatus(run.FailedFiles, run.ExpectedCodeFiles, failureThreshold)
	failureRate := parser.CodeParseFailureRate(run.FailedFiles, run.ExpectedCodeFiles)

	set := bson.M{
		"stages.code_parse":        stageStatus,
		"code_parse_failure_rate":  failureRate,
		"processed_code_files":     syncTo,
	}
	if run.ExpectedDocsFiles <= 0 {
		set["stages.docs_parse"] = "completed"
	}

	_, err = col.UpdateOne(ctx, filter, bson.M{"$set": set})
	if err != nil {
		return fmt.Errorf("sync code parse fan-in: %w", err)
	}
	return nil
}
