package mongo

import (
	"context"
	"errors"
	"fmt"

	"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-repo-sync.com/internal/domain"
	"bit.admedia.com/scm/ad/adpilot-indexing-repo-sync.com/internal/store"
)

var _ store.MetadataStore = (*Store)(nil)

// UpsertRepository inserts or updates repository metadata by repo_id.
func (s *Store) UpsertRepository(ctx context.Context, repo domain.Repository) error {
	filter := bson.M{"repo_id": repo.RepoID}
	update := bson.M{"$set": repo}
	_, err := s.collection(collectionRepositories).UpdateOne(ctx, filter, update, options.Update().SetUpsert(true))
	if err != nil {
		return fmt.Errorf("upsert repository: %w", err)
	}
	return nil
}

// GetRepository returns repository metadata by repo_id.
func (s *Store) GetRepository(ctx context.Context, repoID string) (domain.Repository, bool, error) {
	var repo domain.Repository
	err := s.collection(collectionRepositories).FindOne(ctx, bson.M{"repo_id": repoID}).Decode(&repo)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return domain.Repository{}, false, nil
	}
	if err != nil {
		return domain.Repository{}, false, fmt.Errorf("get repository: %w", err)
	}
	return repo, true, nil
}

// ListRepositories returns all tracked repositories sorted by updated_at descending.
func (s *Store) ListRepositories(ctx context.Context) ([]domain.Repository, error) {
	opts := options.Find().SetSort(bson.D{{Key: "updated_at", Value: -1}})
	cursor, err := s.collection(collectionRepositories).Find(ctx, bson.M{}, opts)
	if err != nil {
		return nil, fmt.Errorf("list repositories: %w", err)
	}
	defer cursor.Close(ctx)

	var repos []domain.Repository
	if err := cursor.All(ctx, &repos); err != nil {
		return nil, fmt.Errorf("decode repositories: %w", err)
	}
	return repos, nil
}

// SaveSnapshot inserts snapshot metadata.
func (s *Store) SaveSnapshot(ctx context.Context, snap domain.Snapshot) error {
	_, err := s.collection(collectionSnapshots).InsertOne(ctx, snap)
	if err != nil {
		return fmt.Errorf("save snapshot: %w", err)
	}
	return nil
}

// GetSnapshot returns snapshot metadata by repo_id and snapshot_id.
func (s *Store) GetSnapshot(ctx context.Context, repoID, snapshotID string) (domain.Snapshot, bool, error) {
	var snap domain.Snapshot
	filter := bson.M{"repo_id": repoID, "snapshot_id": snapshotID}
	err := s.collection(collectionSnapshots).FindOne(ctx, filter).Decode(&snap)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return domain.Snapshot{}, false, nil
	}
	if err != nil {
		return domain.Snapshot{}, false, fmt.Errorf("get snapshot: %w", err)
	}
	return snap, true, nil
}

// GetLatestSnapshot returns the most recent snapshot for a repository.
func (s *Store) GetLatestSnapshot(ctx context.Context, repoID string) (domain.Snapshot, bool, error) {
	opts := options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}})
	var snap domain.Snapshot
	err := s.collection(collectionSnapshots).FindOne(ctx, bson.M{"repo_id": repoID}, opts).Decode(&snap)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return domain.Snapshot{}, false, nil
	}
	if err != nil {
		return domain.Snapshot{}, false, fmt.Errorf("get latest snapshot: %w", err)
	}
	return snap, true, nil
}

// CreateSyncJob inserts a sync job record.
func (s *Store) CreateSyncJob(ctx context.Context, job domain.SyncJob) error {
	_, err := s.collection(collectionSyncJobs).InsertOne(ctx, job)
	if err != nil {
		return fmt.Errorf("create sync job: %w", err)
	}
	return nil
}

// UpdateSyncJob updates a sync job by job_id.
func (s *Store) UpdateSyncJob(ctx context.Context, job domain.SyncJob) error {
	filter := bson.M{"job_id": job.JobID}
	update := bson.M{"$set": job}
	result, err := s.collection(collectionSyncJobs).UpdateOne(ctx, filter, update)
	if err != nil {
		return fmt.Errorf("update sync job: %w", err)
	}
	if result.MatchedCount == 0 {
		return fmt.Errorf("sync job not found: %s", job.JobID)
	}
	return nil
}

// CreateImportJob inserts an import job record. Returns ErrDuplicateImportJob on duplicate import_job_id.
func (s *Store) CreateImportJob(ctx context.Context, job domain.ImportJob) error {
	_, err := s.collection(collectionImportJobs).InsertOne(ctx, job)
	if err != nil {
		if mongo.IsDuplicateKeyError(err) {
			return store.ErrDuplicateImportJob
		}
		return fmt.Errorf("create import job: %w", err)
	}
	return nil
}

// GetImportJob returns an import job by import_job_id.
func (s *Store) GetImportJob(ctx context.Context, importJobID string) (domain.ImportJob, bool, error) {
	var job domain.ImportJob
	err := s.collection(collectionImportJobs).FindOne(ctx, bson.M{"import_job_id": importJobID}).Decode(&job)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return domain.ImportJob{}, false, nil
	}
	if err != nil {
		return domain.ImportJob{}, false, fmt.Errorf("get import job: %w", err)
	}
	return job, true, nil
}

// CreateIndexingRun inserts an indexing run record.
func (s *Store) CreateIndexingRun(ctx context.Context, run domain.IndexingRun) error {
	_, err := s.collection(collectionIndexingRuns).InsertOne(ctx, run)
	if err != nil {
		return fmt.Errorf("create indexing run: %w", err)
	}
	return nil
}

// UpdateIndexingRun persists the repo-sync-owned fields of an indexing run
// (status, the repo_sync stage, error and completed_at) by indexing_run_id.
//
// It deliberately does NOT $set the whole document. code-parser and
// docs-ingestion concurrently advance the fan-in fields (processed_code_files,
// processed_code_file_paths, stages.code_parse / graph_finalize / commit_intel,
// failed_files, ...). A full "$set: run" here would overwrite those with the
// stale in-memory copy repo-sync holds — resetting processed_code_files to 0 and
// stages.code_parse back to "pending" mid-run — which let snapshot graphs
// finalize prematurely. Scope the write to only the fields repo-sync owns.
func (s *Store) UpdateIndexingRun(ctx context.Context, run domain.IndexingRun) error {
	filter := bson.M{"indexing_run_id": run.IndexingRunID}
	set := bson.M{
		"status":           run.Status,
		"stages.repo_sync": run.Stages.RepoSync,
		"error":            run.Error,
	}
	if run.CompletedAt != nil {
		set["completed_at"] = run.CompletedAt
	}
	result, err := s.collection(collectionIndexingRuns).UpdateOne(ctx, filter, bson.M{"$set": set})
	if err != nil {
		return fmt.Errorf("update indexing run: %w", err)
	}
	if result.MatchedCount == 0 {
		return fmt.Errorf("indexing run not found: %s", run.IndexingRunID)
	}
	return nil
}

// GetIndexingRun returns an indexing run by indexing_run_id.
func (s *Store) GetIndexingRun(ctx context.Context, indexingRunID string) (domain.IndexingRun, bool, error) {
	var run domain.IndexingRun
	err := s.collection(collectionIndexingRuns).FindOne(ctx, bson.M{"indexing_run_id": indexingRunID}).Decode(&run)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return domain.IndexingRun{}, false, nil
	}
	if err != nil {
		return domain.IndexingRun{}, false, fmt.Errorf("get indexing run: %w", err)
	}
	return run, true, nil
}

// GetIndexingRunBySnapshot returns the indexing run for a repo and snapshot.
func (s *Store) GetIndexingRunBySnapshot(ctx context.Context, repoID, snapshotID string) (domain.IndexingRun, bool, error) {
	var run domain.IndexingRun
	filter := bson.M{"repo_id": repoID, "snapshot_id": snapshotID}
	err := s.collection(collectionIndexingRuns).FindOne(ctx, filter).Decode(&run)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return domain.IndexingRun{}, false, nil
	}
	if err != nil {
		return domain.IndexingRun{}, false, fmt.Errorf("get indexing run by snapshot: %w", err)
	}
	return run, true, nil
}

// GetLatestIndexingRunByRepo returns the most recent indexing run for a repository.
func (s *Store) GetLatestIndexingRunByRepo(ctx context.Context, repoID string) (domain.IndexingRun, bool, error) {
	var run domain.IndexingRun
	opts := options.FindOne().SetSort(bson.D{{Key: "started_at", Value: -1}})
	err := s.collection(collectionIndexingRuns).FindOne(ctx, bson.M{"repo_id": repoID}, opts).Decode(&run)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return domain.IndexingRun{}, false, nil
	}
	if err != nil {
		return domain.IndexingRun{}, false, fmt.Errorf("get latest indexing run: %w", err)
	}
	return run, true, nil
}

// SaveIndexingDiagnostic upserts a durable pipeline diagnostic.
func (s *Store) SaveIndexingDiagnostic(ctx context.Context, diagnostic domain.IndexingDiagnostic) error {
	filter := bson.M{"diagnostic_id": diagnostic.DiagnosticID}
	_, err := s.collection(collectionDiagnostics).UpdateOne(
		ctx,
		filter,
		bson.M{"$set": diagnostic},
		options.Update().SetUpsert(true),
	)
	if err != nil {
		return fmt.Errorf("save indexing diagnostic: %w", err)
	}
	return nil
}

// ListIndexingDiagnostics returns recent diagnostics for a snapshot.
func (s *Store) ListIndexingDiagnostics(ctx context.Context, repoID, snapshotID string, limit int) ([]domain.IndexingDiagnostic, error) {
	if limit <= 0 || limit > 500 {
		limit = 100
	}
	opts := options.Find().
		SetSort(bson.D{{Key: "created_at", Value: -1}}).
		SetLimit(int64(limit))
	cursor, err := s.collection(collectionDiagnostics).Find(ctx, bson.M{
		"repo_id":     repoID,
		"snapshot_id": snapshotID,
	}, opts)
	if err != nil {
		return nil, fmt.Errorf("list indexing diagnostics: %w", err)
	}
	defer cursor.Close(ctx)

	var diagnostics []domain.IndexingDiagnostic
	if err := cursor.All(ctx, &diagnostics); err != nil {
		return nil, fmt.Errorf("decode indexing diagnostics: %w", err)
	}
	return diagnostics, nil
}

// InsertSnapshotFiles bulk-inserts snapshot file inventory records.
func (s *Store) InsertSnapshotFiles(ctx context.Context, files []domain.SnapshotFile) error {
	if len(files) == 0 {
		return nil
	}
	docs := make([]interface{}, len(files))
	for i := range files {
		docs[i] = files[i]
	}
	_, err := s.collection(collectionSnapshotFiles).InsertMany(ctx, docs)
	if err != nil {
		return fmt.Errorf("insert snapshot files: %w", err)
	}
	return nil
}

// ListSnapshotFiles returns all snapshot file records for a repo and snapshot.
func (s *Store) ListSnapshotFiles(ctx context.Context, repoID, snapshotID string) ([]domain.SnapshotFile, error) {
	filter := bson.M{"repo_id": repoID, "snapshot_id": snapshotID}
	cursor, err := s.collection(collectionSnapshotFiles).Find(ctx, filter)
	if err != nil {
		return nil, fmt.Errorf("list snapshot files: %w", err)
	}
	defer cursor.Close(ctx)

	var files []domain.SnapshotFile
	if err := cursor.All(ctx, &files); err != nil {
		return nil, fmt.Errorf("decode snapshot files: %w", err)
	}
	return files, nil
}
