package genericresource

import (
	"errors"
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"strings"

	utilerrors "k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/cli-runtime/pkg/resource"
)

const defaultHTTPGetAttempts int = 3

var defaultNewFunc = func() interface{} {
	return map[string]interface{}{}
}

var errMissingResource = fmt.Errorf(`you must provide one or more resources`)

// Builder provides convenience functions for taking arguments and parameters
// from the command line and converting them to a list of resources to iterate
// over using the Visitor interface.
type Builder struct {
	errs       []error
	paths      []Visitor
	stdinInUse bool
	mapper     *mapper
	schema     resource.ContentValidator
}

// NewBuilder returns a Builder.
func NewBuilder() *Builder {
	return &Builder{
		mapper: &mapper{
			newFunc: defaultNewFunc,
		},
	}
}

// Schema set the schema to validate data in files.
func (b *Builder) Schema(schema resource.ContentValidator) *Builder {
	b.schema = schema
	return b
}

// Constructor tells wanted type of object.
func (b *Builder) Constructor(newFunc func() interface{}) *Builder {
	b.mapper.newFunc = newFunc
	return b
}

// Filename groups input in two categories: URLs and files (files, directories, STDIN)
func (b *Builder) Filename(recursive bool, filenames ...string) *Builder {
	for _, s := range filenames {
		switch {
		case s == "-":
			b.Stdin()
		case strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://"):
			u, err := url.Parse(s)
			if err != nil {
				b.errs = append(b.errs, fmt.Errorf("the URL passed to filename %q is not valid: %v", s, err))
				continue
			}
			b.URL(defaultHTTPGetAttempts, u)
		default:
			matches, err := expandIfFilePattern(s)
			if err != nil {
				b.errs = append(b.errs, err)
				continue
			}
			b.Path(recursive, matches...)
		}
	}
	return b
}

// Stdin will read objects from the standard input.
func (b *Builder) Stdin() *Builder {
	if b.stdinInUse {
		b.errs = append(b.errs, resource.StdinMultiUseError)
	}
	b.stdinInUse = true
	b.paths = append(b.paths, FileVisitorForSTDIN(b.mapper, b.schema))
	return b
}

// URL accepts a number of URLs directly.
func (b *Builder) URL(httpAttemptCount int, urls ...*url.URL) *Builder {
	for _, u := range urls {
		b.paths = append(b.paths, NewURLVisitor(b.mapper, httpAttemptCount, u, b.schema))
	}
	return b
}

// Path accepts a set of paths that may be files, directories (all can contain
// one or more resources). Creates a FileVisitor for each file and then each
// FileVisitor is streaming the content to a StreamVisitor.
func (b *Builder) Path(recursive bool, paths ...string) *Builder {
	for _, p := range paths {
		_, err := os.Stat(p)
		if os.IsNotExist(err) {
			b.errs = append(b.errs, fmt.Errorf("the path %q does not exist", p))
			continue
		}
		if err != nil {
			b.errs = append(b.errs, fmt.Errorf("the path %q cannot be accessed: %v", p, err))
			continue
		}

		visitors, err := ExpandPathsToFileVisitors(b.mapper, p, recursive, resource.FileExtensions, b.schema)
		if err != nil {
			b.errs = append(b.errs, fmt.Errorf("error reading %q: %v", p, err))
		}

		b.paths = append(b.paths, visitors...)
	}
	if len(b.paths) == 0 && len(b.errs) == 0 {
		b.errs = append(b.errs, fmt.Errorf("error reading %v: recognized file extensions are %v", paths, resource.FileExtensions))
	}
	return b
}

// Do returns a Result object with a Visitor for the resources identified by the Builder. Note that stream
// inputs are consumed by the first execution - use Infos() or Objects() on the Result to capture a list
// for further iteration.
func (b *Builder) Do() *Result {
	r := b.visitorResult()
	return r
}

func (b *Builder) visitorResult() *Result {
	if len(b.errs) > 0 {
		return &Result{err: utilerrors.NewAggregate(b.errs)}
	}

	// visit items specified by paths
	if len(b.paths) != 0 {
		return b.visitByPaths()
	}
	return &Result{err: errMissingResource}
}

func (b *Builder) visitByPaths() *Result {
	result := &Result{}

	result.visitor = VisitorList(b.paths)
	return result
}

// expandIfFilePattern returns all the filenames that match the input pattern
// or the filename if it is a specific filename and not a pattern.
// If the input is a pattern and it yields no result it will result in an error.
func expandIfFilePattern(pattern string) ([]string, error) {
	if _, err := os.Stat(pattern); os.IsNotExist(err) {
		matches, err := filepath.Glob(pattern)
		if err == nil && len(matches) == 0 {
			return nil, fmt.Errorf("the path %q does not exist", pattern)
		}
		if errors.Is(err, filepath.ErrBadPattern) {
			return nil, fmt.Errorf("pattern %q is not valid: %v", pattern, err)
		}
		return matches, err
	}
	return []string{pattern}, nil
}
