package provider

import (
	"errors"
	"fmt"
	"strings"

	"github.com/projectdiscovery/gologger"
	"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
	"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/openapi"
	"github.com/projectdiscovery/nuclei/v3/pkg/input/formats/swagger"
	"github.com/projectdiscovery/nuclei/v3/pkg/input/provider/http"
	"github.com/projectdiscovery/nuclei/v3/pkg/input/provider/list"
	"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/generators"
	"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate"
	configTypes "github.com/projectdiscovery/nuclei/v3/pkg/types"
	"github.com/projectdiscovery/retryablehttp-go"
	"github.com/projectdiscovery/utils/errkit"
	stringsutil "github.com/projectdiscovery/utils/strings"
)

var (
	ErrNotImplemented = errkit.New("provider does not implement method")
	ErrInactiveInput  = fmt.Errorf("input is inactive")
)

const (
	MultiFormatInputProvider = "MultiFormatInputProvider"
	ListInputProvider        = "ListInputProvider"
	SimpleListInputProvider  = "SimpleInputProvider"
)

// IsErrNotImplemented checks if an error is a not implemented error
func IsErrNotImplemented(err error) bool {
	if err == nil {
		return false
	}
	if stringsutil.ContainsAll(err.Error(), "provider", "does not implement") {
		return true
	}
	return false
}

// Validate all Implementations
var (
	// SimpleInputProvider is more like a No-Op and returns given list of urls as input
	_ InputProvider = &SimpleInputProvider{}
	// HttpInputProvider provides support for formats that contain complete request/response
	// like burp, openapi, postman,proxify, etc.
	_ InputProvider = &http.HttpInputProvider{}
	// ListInputProvider provides support for simple list of urls or files etc
	_ InputProvider = &list.ListInputProvider{}
)

// InputProvider is unified input provider interface that provides
// processed inputs to nuclei by parsing and providing different
// formats such as list,openapi,postman,proxify,burp etc.
type InputProvider interface {
	// Count returns total targets for input provider
	Count() int64
	// Iterate over all inputs in order
	Iterate(callback func(value *contextargs.MetaInput) bool)
	// Set adds item to input provider
	Set(executionId string, value string)
	// SetWithProbe adds item to input provider with http probing
	SetWithProbe(executionId string, value string, probe types.InputLivenessProbe) error
	// SetWithExclusions adds item to input provider if it doesn't match any of the exclusions
	SetWithExclusions(executionId string, value string) error
	// InputType returns the type of input provider
	InputType() string
	// Close the input provider and cleanup any resources
	Close()
}

// InputOptions contains options for input provider
type InputOptions struct {
	// Options for global config
	Options *configTypes.Options
	// TempDir is the temporary directory for storing files
	TempDir string
	// NotFoundCallback is the callback to call when input is not found
	// only supported in list input provider
	NotFoundCallback func(template string) bool
}

// NewInputProvider creates a new input provider based on the options
// and returns it
func NewInputProvider(opts InputOptions) (InputProvider, error) {
	// optionally load generated vars values if available
	val, err := formats.ReadOpenAPIVarDumpFile()
	if err != nil && !errors.Is(err, formats.ErrNoVarsDumpFile) {
		// log error and continue
		gologger.Error().Msgf("Could not read vars dump file: %s\n", err)
	}
	extraVars := make(map[string]interface{})
	if val != nil {
		for _, v := range val.Var {
			v = strings.TrimSpace(v)
			// split into key value
			parts := strings.SplitN(v, "=", 2)
			if len(parts) == 2 {
				extraVars[parts[0]] = parts[1]
			}
		}
	}

	// check if input provider is supported
	if strings.EqualFold(opts.Options.InputFileMode, "list") {
		// create a new list input provider
		return list.New(&list.Options{
			Options:          opts.Options,
			NotFoundCallback: opts.NotFoundCallback,
		})
	} else if len(opts.Options.Targets) > 0 &&
		(strings.EqualFold(opts.Options.InputFileMode, "openapi") || strings.EqualFold(opts.Options.InputFileMode, "swagger")) {

		if len(opts.Options.Targets) > 1 {
			return nil, fmt.Errorf("only one target URL is supported in %s input mode", opts.Options.InputFileMode)
		}

		target := opts.Options.Targets[0]
		if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") {
			var downloader formats.SpecDownloader
			var tempFile string
			var err error

			// Get HttpClient from protocolstate if available
			var httpClient *retryablehttp.Client
			if opts.Options.ExecutionId != "" {
				dialers := protocolstate.GetDialersWithId(opts.Options.ExecutionId)
				if dialers != nil {
					httpClient = dialers.DefaultHTTPClient
				}
			}

			switch strings.ToLower(opts.Options.InputFileMode) {
			case "openapi":
				downloader = openapi.NewDownloader()
				tempFile, err = downloader.Download(target, opts.TempDir, httpClient)
			case "swagger":
				downloader = swagger.NewDownloader()
				tempFile, err = downloader.Download(target, opts.TempDir, httpClient)
			default:
				return nil, fmt.Errorf("unsupported input mode: %s", opts.Options.InputFileMode)
			}

			if err != nil {
				return nil, fmt.Errorf("failed to download %s spec from url %s: %w", opts.Options.InputFileMode, target, err)
			}

			opts.Options.TargetsFilePath = tempFile
		}
	}

	return http.NewHttpInputProvider(&http.HttpMultiFormatOptions{
		InputFile: opts.Options.TargetsFilePath,
		InputMode: opts.Options.InputFileMode,
		Options: formats.InputFormatOptions{
			Variables:            generators.MergeMaps(extraVars, opts.Options.Vars.AsMap()),
			SkipFormatValidation: opts.Options.SkipFormatValidation,
			RequiredOnly:         opts.Options.FormatUseRequiredOnly,
			VarsTextTemplating:   opts.Options.VarsTextTemplating,
			VarsFilePaths:        opts.Options.VarsFilePaths,
		},
	})
}

// SupportedInputFormats returns all supported input formats of nuclei
func SupportedInputFormats() string {
	return "list, " + http.SupportedFormats()
}
