package action

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"

	"github.com/fatih/color"
	"github.com/gopasspw/gopass/internal/action/exit"
	"github.com/gopasspw/gopass/internal/config"
	"github.com/gopasspw/gopass/internal/store/leaf"
	"github.com/gopasspw/gopass/internal/tree"
	"github.com/gopasspw/gopass/pkg/ctxutil"
	shellquote "github.com/kballard/go-shellquote"
	"github.com/noborus/ov/oviewer"
	"github.com/urfave/cli/v3"
	"golang.org/x/term"
)

// List all secrets as a tree. If the filter argument is non-empty
// display only those that have this prefix.
func (s *searchHandler) List(ctx context.Context, cmd *cli.Command) error {
	ctx = ctxutil.WithGlobalFlags(ctx, cmd)
	filter := cmd.Args().First()
	flat := cmd.Bool("flat")
	stripPrefix := cmd.Bool("strip-prefix")
	folders := cmd.Bool("folders")

	// print the path if the argument is a direct hit.
	if s.Store.Exists(ctx, filter) && !s.Store.IsDir(ctx, filter) {
		if cmd.Bool("json") {
			return jsonWrite(stdout, []string{filter})
		}

		fmt.Println(filter)

		return nil
	}

	// we only support listing folders in flat mode currently.
	if folders {
		flat = true
	}

	l, err := s.Store.Tree(ctx)
	if err != nil {
		return exit.Error(exit.List, err, "failed to list store: %s", err)
	}

	// set limit to infinite by default unless it's set with the flag
	limit := tree.INF
	if cmd.IsSet("limit") {
		limit = cmd.Int("limit")
	}

	if cmd.Bool("json") {
		return s.listJSON(ctx, l, limit, folders, stripPrefix, filter)
	}

	return s.listFiltered(ctx, l, limit, flat, folders, stripPrefix, filter)
}

func (s *searchHandler) listFiltered(ctx context.Context, l *tree.Root, limit int, flat, folders, stripPrefix bool, filter string) error {
	sep := leaf.Sep

	if filter == "" || filter == sep {
		// We list all entries then.
		stripPrefix = true
	} else {
		// If the filter ends with a separator, we still want to find the content of that folder.
		if strings.HasSuffix(filter, sep) {
			filter = filter[:len(filter)-1]
		}
		// To avoid shadowing l since we need it outside of the else scope.
		var err error
		// We restrict ourselves to the filter.
		l, err = l.FindFolder(filter)
		if err != nil {
			return exit.Error(exit.NotFound, nil, "Entry %q not found", filter)
		}
		l.SetName(filter + sep)
	}

	if flat {
		listOver := l.List
		if folders {
			listOver = l.ListFolders
		}
		for _, e := range listOver(limit) {
			if stripPrefix {
				e = strings.TrimPrefix(e, filter+sep)
			}
			fmt.Fprintln(stdout, e)
		}

		return nil
	}

	// we may need to redirect stdout for the pager support.
	so, buf := redirectPager(ctx, l)

	fmt.Fprintln(so, l.Format(limit))
	if buf != nil {
		if err := s.pager(ctx, buf); err != nil {
			return exit.Error(exit.Unknown, err, "failed to invoke pager: %s", err)
		}
	}

	return nil
}

// redirectPager returns a redirected io.Writer if the output would exceed
// the terminal size.
func redirectPager(ctx context.Context, subtree *tree.Root) (io.Writer, *bytes.Buffer) {
	if config.Bool(ctx, "core.nopager") {
		return stdout, nil
	}
	_, rows, err := term.GetSize(0)
	if err != nil {
		return stdout, nil
	}
	if subtree == nil || subtree.Len() < rows {
		return stdout, nil
	}
	if pager := os.Getenv("PAGER"); pager == "" {
		return stdout, nil
	}
	color.NoColor = true
	buf := &bytes.Buffer{}

	return buf, buf
}

// pager invokes the default pager with the given content.
func (s *searchHandler) pager(ctx context.Context, buf io.Reader) error {
	if config.Bool(ctx, "output.internal-pager") {
		ov, err := oviewer.NewRoot(buf)
		if err != nil {
			return err
		}

		return ov.Run()
	}

	pager := os.Getenv("PAGER")
	if pager == "" {
		fmt.Fprintln(stdout, buf)

		return nil
	}

	args, err := shellquote.Split(pager)
	if err != nil {
		return fmt.Errorf("failed to split pager command: %w", err)
	}

	cmd := exec.CommandContext(ctx, args[0], args[1:]...)
	cmd.Stdin = buf
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	return cmd.Run()
}

func (s *searchHandler) listJSON(ctx context.Context, l *tree.Root, limit int, folders, stripPrefix bool, filter string) error {
	sep := leaf.Sep

	if filter != "" && filter != sep {
		if strings.HasSuffix(filter, sep) {
			filter = filter[:len(filter)-1]
		}
		var err error
		l, err = l.FindFolder(filter)
		if err != nil {
			return exit.Error(exit.NotFound, nil, "Entry %q not found", filter)
		}
		l.SetName(filter + sep)
	} else {
		stripPrefix = true
	}

	var entries []string
	if folders {
		entries = l.ListFolders(limit)
	} else {
		entries = l.List(limit)
	}

	if stripPrefix {
		for i, e := range entries {
			entries[i] = strings.TrimPrefix(e, filter+sep)
		}
	}

	return jsonWrite(stdout, entries)
}

// jsonWrite marshals v as indented JSON and writes it to w.
func jsonWrite(w interface{ Write([]byte) (int, error) }, v any) error {
	enc := json.NewEncoder(w)
	enc.SetIndent("", "  ")

	return enc.Encode(v)
}
