// Package store owns all MongoDB access: connection lifecycle, index
// bootstrap, optimistic-concurrency updates, and (via internal/files) GridFS.
// No other package talks to Mongo directly.
package store

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
	"go.mongodb.org/mongo-driver/v2/mongo/readpref"

	"bountyboard/internal/config"
)

type Store struct {
	Client *mongo.Client
	DB     *mongo.Database
}

// Connect builds the client (the driver dials lazily) and verifies
// connectivity with a bounded retry so a compose race at boot does not kill
// the app, then ensures all indexes.
func Connect(ctx context.Context, cfg *config.Config, log *slog.Logger) (*Store, error) {
	opts := options.Client().
		ApplyURI(cfg.MongoURI).
		SetAppName("bountyboard").
		SetServerSelectionTimeout(5 * time.Second)
	if cfg.MongoUsername != "" {
		opts.SetAuth(options.Credential{Username: cfg.MongoUsername, Password: cfg.MongoPassword})
	}
	client, err := mongo.Connect(opts)
	if err != nil {
		return nil, fmt.Errorf("mongo connect: %w", err)
	}
	s := &Store{Client: client, DB: client.Database(cfg.MongoDB)}

	const attempts = 6
	for i := 1; ; i++ {
		err = s.Ping(ctx)
		if err == nil {
			break
		}
		if i == attempts || ctx.Err() != nil {
			return nil, fmt.Errorf("mongo ping (%d attempts): %w", i, err)
		}
		log.Warn("mongo not ready, retrying", "attempt", i, "err", err)
		select {
		case <-time.After(time.Duration(i) * time.Second):
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}

	if err := EnsureIndexes(ctx, s.DB); err != nil {
		return nil, fmt.Errorf("ensure indexes: %w", err)
	}
	log.Info("mongo connected", "db", cfg.MongoDB)
	return s, nil
}

func (s *Store) Ping(ctx context.Context) error {
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()
	return s.Client.Ping(ctx, readpref.Primary())
}

func (s *Store) Close(ctx context.Context) error {
	return s.Client.Disconnect(ctx)
}
