package cli

import (
	"context"
	"strings"

	"github.com/neilotoole/sq/libsq/core/lg/lga"

	"golang.org/x/exp/slog"

	"github.com/spf13/cobra"

	"github.com/neilotoole/sq/libsq/core/errz"
	"github.com/neilotoole/sq/libsq/core/options"
	"github.com/neilotoole/sq/libsq/driver"
	"github.com/neilotoole/sq/libsq/source"
)

// determineSources figures out what the active source is
// from any combination of stdin, flags or cfg. It will
// mutate rc.Config.Sources as necessary. If no error
// is returned, it is guaranteed that there's an active
// source on the source set.
func determineSources(ctx context.Context, rc *RunContext) error {
	cmd, srcs := rc.Cmd, rc.Config.Sources
	activeSrc, err := activeSrcFromFlagsOrConfig(cmd, srcs)
	if err != nil {
		return err
	}
	// Note: ^ activeSrc could still be nil

	// check if there's input on stdin
	stdinSrc, err := checkStdinSource(ctx, rc)
	if err != nil {
		return err
	}

	if stdinSrc != nil {
		// We have a valid source on stdin.

		// Add the stdin source to the set.
		err = srcs.Add(stdinSrc)
		if err != nil {
			return err
		}

		if !cmdFlagChanged(cmd, flagActiveSrc) {
			// If the user has not explicitly set an active
			// source via flag, then we set the stdin pipe data
			// source as the active source.
			// We do this because the @stdin src is commonly the
			// only data source the user cares about in a pipe
			// situation.
			_, err = srcs.SetActive(stdinSrc.Handle, false)
			if err != nil {
				return err
			}
			activeSrc = stdinSrc
		}
	}

	if activeSrc == nil {
		return errz.New(msgNoActiveSrc)
	}

	return nil
}

// activeSrcFromFlagsOrConfig gets the active source, either
// from flagActiveSrc or from srcs.Active. An error is returned
// if the flag src is not found: if the flag src is found,
// it is set as the active src on srcs. If the flag was not
// set and there is no active src in srcs, (nil, nil) is
// returned.
func activeSrcFromFlagsOrConfig(cmd *cobra.Command, srcs *source.Set) (*source.Source, error) {
	var activeSrc *source.Source

	if cmdFlagChanged(cmd, flagActiveSrc) {
		// The user explicitly wants to set an active source
		// just for this query.

		handle, _ := cmd.Flags().GetString(flagActiveSrc)
		s, err := srcs.Get(handle)
		if err != nil {
			return nil, errz.Wrapf(err, "flag --%s", flagActiveSrc)
		}

		activeSrc, err = srcs.SetActive(s.Handle, false)
		if err != nil {
			return nil, err
		}
	} else {
		activeSrc = srcs.Active()
	}
	return activeSrc, nil
}

// checkStdinSource checks if there's stdin data (on pipe/redirect).
// If there is, that pipe is inspected, and if it has recognizable
// input, a new source instance with handle @stdin is constructed
// and returned. If the pipe has no data (size is zero),
// then (nil,nil) is returned.
func checkStdinSource(ctx context.Context, rc *RunContext) (*source.Source, error) {
	cmd := rc.Cmd

	f := rc.Stdin
	info, err := f.Stat()
	if err != nil {
		return nil, errz.Wrap(err, "failed to get stat on stdin")
	}

	if info.Size() <= 0 {
		// Doesn't make sense to have zero-data pipe? just ignore.
		return nil, nil //nolint:nilnil
	}

	// If we got this far, we have pipe input

	// It's possible the user supplied source options
	var opts options.Options
	if cmd.Flags().Changed(flagSrcOptions) {
		val, _ := cmd.Flags().GetString(flagSrcOptions)
		val = strings.TrimSpace(val)

		if val != "" {
			opts, err = options.ParseOptions(val)
			if err != nil {
				return nil, err
			}
		}
	}

	typ := source.TypeNone
	if cmd.Flags().Changed(flagDriver) {
		val, _ := cmd.Flags().GetString(flagDriver)
		typ = source.Type(val)
		if rc.registry.ProviderFor(typ) == nil {
			return nil, errz.Errorf("unknown driver type: %s", typ)
		}
	}

	err = rc.files.AddStdin(f)
	if err != nil {
		return nil, err
	}

	if typ == source.TypeNone {
		typ, err = rc.files.TypeStdin(ctx)
		if err != nil {
			return nil, err
		}
		if typ == source.TypeNone {
			return nil, errz.New("unable to detect type of stdin: use flag --driver")
		}
	}

	return newSource(rc.Log, rc.registry, typ, source.StdinHandle, source.StdinHandle, opts)
}

// newSource creates a new Source instance where the
// driver type is known. Opts may be nil.
func newSource(log *slog.Logger, dp driver.Provider, typ source.Type, handle, loc string,
	opts options.Options,
) (*source.Source, error) {
	if opts == nil {
		log.Debug("Create new data source",
			lga.Handle, handle,
			lga.Driver, typ,
			lga.Loc, source.RedactLocation(loc),
		)
	} else {
		log.Debug("Create new data source with opts",
			lga.Handle, handle,
			lga.Driver, typ,
			lga.Loc, source.RedactLocation(loc),
			lga.Opts, opts.Encode(),
		)
	}

	err := source.ValidHandle(handle)
	if err != nil {
		return nil, err
	}

	drvr, err := dp.DriverFor(typ)
	if err != nil {
		return nil, err
	}

	src := &source.Source{Handle: handle, Location: loc, Type: typ, Options: opts}

	log.Debug("Validating provisional new data source", lga.Src, src)
	canonicalSrc, err := drvr.ValidateSource(src)
	if err != nil {
		return nil, err
	}
	return canonicalSrc, nil
}