package mongo

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"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/service/parser"
)

const collectionIndexingRuns = "indexing_runs"

// IncrementProcessedCodeFiles records a file outcome exactly once per snapshot.
func (s *Store) IncrementProcessedCodeFiles(ctx context.Context, repoID, snapshotID, filePath string) error {
	if !s.indexingRunsEnabled {
		return nil
	}
	if err := ctx.Err(); err != nil {
		return err
	}

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

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

	var processed int32
	if v := existing.Lookup("processed_code_files"); v.Type != bson.TypeNull {
		processed, _ = v.Int32OK()
	}

	update := bson.M{
		"$addToSet": bson.M{"processed_code_file_paths": filePath},
	}
	incUpdate := bson.M{"$inc": bson.M{"processed_code_files": 1}}
	if processed == 0 {
		incUpdate["$set"] = bson.M{"stages.code_parse": "running"}
	}
	for op, value := range incUpdate {
		update[op] = value
	}

	// Only increment when this file path has not been recorded yet.
	match := bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
		"processed_code_file_paths": bson.M{"$ne": filePath},
	}

	result, err := col.UpdateOne(ctx, match, update)
	if err != nil {
		return fmt.Errorf("increment processed_code_files: %w", err)
	}
	if result.MatchedCount == 0 {
		// Idempotent retry for an already-counted file.
		return nil
	}

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

	if run.ExpectedCodeFiles > 0 && run.ProcessedCodeFiles >= run.ExpectedCodeFiles {
		_, err = col.UpdateOne(ctx, filter, bson.M{"$set": bson.M{"stages.code_parse": "completed"}})
		if err != nil {
			return fmt.Errorf("mark code_parse completed: %w", err)
		}
	}
	return nil
}

// IncrementSkippedCodeFiles records a code file that counts toward the fan-in but
// produces no code_graph_artifact (ignored / deleted / unsupported-language),
// idempotently per file path. The finalize artifact-completeness gate subtracts
// these so a repo with skipped code files can still seal.
func (s *Store) IncrementSkippedCodeFiles(ctx context.Context, repoID, snapshotID, filePath string) error {
	if !s.indexingRunsEnabled {
		return nil
	}
	if err := ctx.Err(); err != nil {
		return err
	}

	// Only add when this path is not already recorded as skipped (idempotent).
	match := bson.M{
		"repo_id":                 repoID,
		"snapshot_id":             snapshotID,
		"skipped_code_file_paths": bson.M{"$ne": filePath},
	}
	update := bson.M{"$addToSet": bson.M{"skipped_code_file_paths": filePath}}

	if _, err := s.indexingRunsCollection(collectionIndexingRuns).UpdateOne(ctx, match, update); err != nil {
		return fmt.Errorf("increment skipped_code_files: %w", err)
	}
	return nil
}

// IncrementFailedCodeFiles records a permanent code-parse failure exactly once per file path.
func (s *Store) IncrementFailedCodeFiles(ctx context.Context, repoID, snapshotID, filePath string) error {
	if !s.indexingRunsEnabled {
		return nil
	}
	if err := ctx.Err(); err != nil {
		return err
	}

	filter := bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
	}
	match := bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
		"failed_code_file_paths": bson.M{"$ne": filePath},
	}

	col := s.indexingRunsCollection(collectionIndexingRuns)
	result, err := col.UpdateOne(ctx, match, bson.M{
		"$addToSet": bson.M{"failed_code_file_paths": filePath},
		"$inc":      bson.M{"failed_files": 1},
	})
	if err != nil {
		return fmt.Errorf("increment failed_files: %w", err)
	}
	if result.MatchedCount == 0 {
		return nil
	}

	var run struct {
		FailedFiles       int `bson:"failed_files"`
		ExpectedCodeFiles int `bson:"expected_code_files"`
	}
	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 after failed increment: %w", err)
	}

	failureRate := parser.CodeParseFailureRate(run.FailedFiles, run.ExpectedCodeFiles)
	_, err = col.UpdateOne(ctx, filter, bson.M{
		"$set": bson.M{"code_parse_failure_rate": failureRate},
	})
	if err != nil {
		return fmt.Errorf("update code_parse_failure_rate: %w", err)
	}
	return nil
}

// SyncCodeParseCounter aligns fan-in counters when artifacts prove code parse is complete.
func (s *Store) SyncCodeParseCounter(ctx context.Context, repoID, snapshotID string, processed int) error {
	if !s.indexingRunsEnabled || processed <= 0 {
		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"`
	}
	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)
	}
	if run.ProcessedCodeFiles >= processed {
		return nil
	}

	set := bson.M{"processed_code_files": processed}
	if run.ExpectedCodeFiles > 0 && processed >= run.ExpectedCodeFiles {
		set["stages.code_parse"] = "completed"
	}

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

const collectionDocParseRuns = "doc_parse_runs"

// CountCompletedDocParseRuns counts completed docs-ingestion runs for a snapshot.
func (s *Store) CountCompletedDocParseRuns(ctx context.Context, repoID, snapshotID string) (int, error) {
	if err := ctx.Err(); err != nil {
		return 0, err
	}

	count, err := s.docsCollection(collectionDocParseRuns).CountDocuments(ctx, bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
		"status":      "completed",
	})
	if err != nil {
		return 0, fmt.Errorf("count completed doc parse runs: %w", err)
	}
	return int(count), nil
}

// SyncDocsParseCounter aligns fan-in counters when doc parse runs prove docs ingestion is complete.
func (s *Store) SyncDocsParseCounter(ctx context.Context, repoID, snapshotID string, processed int) error {
	if !s.indexingRunsEnabled || processed <= 0 {
		return nil
	}
	if err := ctx.Err(); err != nil {
		return err
	}

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

	var run struct {
		ProcessedDocsFiles int `bson:"processed_docs_files"`
		ExpectedDocsFiles  int `bson:"expected_docs_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)
	}
	if run.ProcessedDocsFiles >= processed {
		return nil
	}

	set := bson.M{"processed_docs_files": processed}
	if run.ExpectedDocsFiles > 0 && processed >= run.ExpectedDocsFiles {
		set["stages.docs_parse"] = "completed"
	}

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

// GetIndexingRun loads fan-in counters for a snapshot.
func (s *Store) GetIndexingRun(ctx context.Context, repoID, snapshotID string) (parser.IndexingRunSnapshot, error) {
	var run parser.IndexingRunSnapshot
	if err := ctx.Err(); err != nil {
		return run, err
	}

	var doc struct {
		RepoID             string `bson:"repo_id"`
		SnapshotID         string `bson:"snapshot_id"`
		CommitSHA          string `bson:"commit_sha"`
		ExpectedCodeFiles  int    `bson:"expected_code_files"`
		ExpectedDocsFiles  int    `bson:"expected_docs_files"`
		ProcessedCodeFiles int    `bson:"processed_code_files"`
		ProcessedDocsFiles int    `bson:"processed_docs_files"`
		FailedFiles        int    `bson:"failed_files"`
	}

	err := s.indexingRunsCollection(collectionIndexingRuns).FindOne(ctx, bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
	}).Decode(&doc)
	if err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			return run, fmt.Errorf("indexing run not found for repo=%s snapshot=%s", repoID, snapshotID)
		}
		return run, fmt.Errorf("load indexing run: %w", err)
	}

	return parser.IndexingRunSnapshot{
		RepoID:             doc.RepoID,
		SnapshotID:         doc.SnapshotID,
		CommitSHA:          doc.CommitSHA,
		ExpectedCodeFiles:  doc.ExpectedCodeFiles,
		ExpectedDocsFiles:  doc.ExpectedDocsFiles,
		ProcessedCodeFiles: doc.ProcessedCodeFiles,
		ProcessedDocsFiles: doc.ProcessedDocsFiles,
		FailedFiles:        doc.FailedFiles,
	}, nil
}

// RecordCommitChangedFailure records a permanent commits.changed failure and marks the run failed.
func (s *Store) RecordCommitChangedFailure(ctx context.Context, repoID, snapshotID, commitSHA, eventID, message string) error {
	if !s.indexingRunsEnabled {
		return nil
	}
	if err := ctx.Err(); err != nil {
		return err
	}

	col := s.indexingRunsCollection(collectionIndexingRuns)
	filter := bson.M{"repo_id": repoID, "snapshot_id": snapshotID}
	match := bson.M{
		"repo_id":            repoID,
		"snapshot_id":        snapshotID,
		"failed_commit_shas": bson.M{"$ne": commitSHA},
	}
	result, err := col.UpdateOne(ctx, match, bson.M{
		"$addToSet": bson.M{"failed_commit_shas": commitSHA},
		"$inc":      bson.M{"failed_commits": 1},
	})
	if err != nil {
		return fmt.Errorf("increment failed_commits: %w", err)
	}

	var run struct {
		ExpectedCommits      int `bson:"expected_commits"`
		ExpectedCommitDeltas int `bson:"expected_commit_deltas"`
		FailedCommits        int `bson:"failed_commits"`
	}
	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 after failed commit increment: %w", err)
	}

	if result.MatchedCount > 0 {
		if err := s.saveCommitFailureDiagnostic(ctx, repoID, snapshotID, commitSHA, eventID, message); err != nil {
			return err
		}
	}

	target := run.ExpectedCommitDeltas
	if target <= 0 {
		target = run.ExpectedCommits
	}
	_, err = col.UpdateOne(ctx, filter, bson.M{"$set": bson.M{
		"status":                    "failed",
		"error":                     message,
		"stages.commit_intel":       "failed",
		"commit_intel_failure_rate": commitFailureRate(run.FailedCommits, target),
	}})
	if err != nil {
		return fmt.Errorf("mark indexing run failed: %w", err)
	}
	return nil
}

func (s *Store) saveCommitFailureDiagnostic(ctx context.Context, repoID, snapshotID, commitSHA, eventID, message string) error {
	id := commitFailureDiagnosticID(repoID, snapshotID, commitSHA, eventID)
	doc := bson.M{
		"diagnostic_id": id,
		"repo_id":       repoID,
		"snapshot_id":   snapshotID,
		"stage":         "commit_intel",
		"source_type":   "commit",
		"source_id":     commitSHA,
		"event_id":      eventID,
		"code":          "commit_delta_failed",
		"severity":      "error",
		"message":       message,
		"created_at":    time.Now().UTC(),
	}
	_, err := s.indexingRunsCollection(collectionIndexingDiagnostics).UpdateOne(
		ctx,
		bson.M{"diagnostic_id": id},
		bson.M{"$set": doc},
		options.Update().SetUpsert(true),
	)
	if err != nil {
		return fmt.Errorf("save indexing diagnostic: %w", err)
	}
	return nil
}

func commitFailureDiagnosticID(parts ...string) string {
	sum := sha256.Sum256([]byte(strings.Join(parts, "|")))
	return "diag_" + hex.EncodeToString(sum[:8])
}

func commitFailureRate(failed, expected int) float64 {
	if failed <= 0 {
		return 0
	}
	if expected <= 0 {
		return 1
	}
	return float64(failed) / float64(expected)
}

// SetGraphFinalizeCompleted marks graph finalization stage complete.
func (s *Store) SetGraphFinalizeCompleted(ctx context.Context, repoID, snapshotID string) error {
	if !s.indexingRunsEnabled {
		return nil
	}
	if err := ctx.Err(); err != nil {
		return err
	}

	_, err := s.indexingRunsCollection(collectionIndexingRuns).UpdateOne(
		ctx,
		bson.M{"repo_id": repoID, "snapshot_id": snapshotID},
		bson.M{"$set": bson.M{"stages.graph_finalize": "completed"}},
	)
	if err != nil {
		return fmt.Errorf("mark graph_finalize completed: %w", err)
	}
	return nil
}

