package leaf

import (
	"context"
	"errors"
	"fmt"
	"log"
	"os"
	"strings"
	"sync"

	"github.com/gopasspw/gopass/internal/config"
	"github.com/gopasspw/gopass/internal/out"
	"github.com/gopasspw/gopass/internal/store"
	"github.com/gopasspw/gopass/pkg/ctxutil"
	"github.com/gopasspw/gopass/pkg/debug"
	"github.com/gopasspw/gopass/pkg/termio"
)

// nolint:ifshort
// reencrypt will re-encrypt all entries for the current recipients.
func (s *Store) reencrypt(ctx context.Context) error {
	entries, err := s.List(ctx, "")
	if err != nil {
		return fmt.Errorf("failed to list store: %w", err)
	}

	// Most gnupg setups don't work well with concurrency > 1, but
	// for other backends - e.g. age - this could very well be > 1.
	conc := s.crypto.Concurrency()

	// save original value of auto push
	{
		// shadow ctx in this block only
		ctx := ctxutil.WithGitCommit(ctx, false)

		// progress bar
		bar := termio.NewProgressBar(int64(len(entries)))
		bar.Hidden = !ctxutil.IsTerminal(ctx) || ctxutil.IsHidden(ctx)

		var wg sync.WaitGroup
		jobs := make(chan string)
		// We use a logger to write without race condition on stdout
		logger := log.New(os.Stdout, "", 0)
		out.Print(ctx, "Starting reencrypt")

		for i := range conc {
			wg.Add(1) // we start a new job
			go func(workerId int) {
				// the workers are fed through an unbuffered channel
				for e := range jobs {
					content, err := s.Get(ctx, e)
					if err != nil {
						logger.Printf("Worker %d: Failed to get current value for %s: %s\n", workerId, e, err)

						continue
					}
					if err := s.Set(WithNoGitOps(ctx, conc > 1), e, content); err != nil {
						if !errors.Is(err, store.ErrMeaninglessWrite) {
							logger.Printf("Worker %d: Failed to write %s: %s\n", workerId, e, err)

							continue
						}
						logger.Printf("Worker %d: Writing secret %s is not needed\n", workerId, e)
					}
				}
				wg.Done() // report the job as finished
			}(i)
		}

		for _, e := range entries {
			// check for context cancellation
			select {
			case <-ctx.Done():
				// We close the channel, so the worker will terminate
				close(jobs)
				// we wait for all workers to have finished
				wg.Wait()

				return fmt.Errorf("context canceled")
			default:
			}

			if bar != nil {
				bar.Inc()
			}

			e = strings.TrimPrefix(e, s.alias)
			jobs <- e
		}
		// We close the channel, so the workers will terminate
		close(jobs)
		// we wait for all workers to have finished
		wg.Wait()
		bar.Done()
	}

	// if we are working concurrently, we cannot git add during the process
	// to avoid a race condition on git .index.lock file, so we do it now.
	if conc > 1 {
		for _, name := range entries {
			p := s.Passfile(name)
			if err := s.storage.TryAdd(ctx, p); err != nil {
				return fmt.Errorf("failed to add %q to git: %w", p, err)
			}

			debug.Log("added %s to git", p)
		}
	}

	if err := s.storage.TryCommit(ctx, ctxutil.GetCommitMessage(ctx)); err != nil {
		return fmt.Errorf("failed to commit changes to git: %w", err)
	}

	return s.reencryptGitPush(ctx)
}

func (s *Store) reencryptGitPush(ctx context.Context) error {
	ctx = config.WithMount(ctx, s.alias)
	if !config.Bool(ctx, "core.autopush") {
		debug.Log("not pushing to git remote, core.autopush is false")

		return nil
	}

	if err := s.storage.TryPush(ctx, "", ""); err != nil {
		return fmt.Errorf("failed to push change to git remote: %w", err)
	}

	return nil
}
