package parser

import (
	"bytes"
	"fmt"
	"io"
	"io/fs"
	"os"
	"regexp"
	"strconv"
	"strings"

	gherkin "github.com/cucumber/gherkin/go/v42"
	messages "github.com/cucumber/messages/go/v34"

	"github.com/cucumber/godog/internal/flags"
	"github.com/cucumber/godog/internal/models"
	"github.com/cucumber/godog/internal/tags"
)

var pathLineRe = regexp.MustCompile(`:([\d]+)$`)

// ExtractFeaturePathLine ...
func ExtractFeaturePathLine(p string) (string, int) {
	line := -1
	retPath := p
	if m := pathLineRe.FindStringSubmatch(p); len(m) > 0 {
		if i, err := strconv.Atoi(m[1]); err == nil {
			line = i
			retPath = p[:strings.LastIndexByte(p, ':')]
		}
	}
	return retPath, line
}

func parseFeatureFile(fsys fs.FS, path, dialect string, newIDFunc func() string) (*models.Feature, error) {
	reader, err := fsys.Open(path)
	if err != nil {
		return nil, err
	}

	defer reader.Close()

	var buf bytes.Buffer
	gherkinDocument, err := gherkin.ParseGherkinDocumentForLanguage(io.TeeReader(reader, &buf), dialect, newIDFunc)
	if err != nil {
		return nil, fmt.Errorf("%s - %v", path, err)
	}

	gherkinDocument.Uri = path
	pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc)

	f := models.Feature{GherkinDocument: gherkinDocument, Pickles: pickles, Content: buf.Bytes()}
	return &f, nil
}

func parseBytes(path string, feature []byte, dialect string, newIDFunc func() string) (*models.Feature, error) {
	reader := bytes.NewReader(feature)

	var buf bytes.Buffer
	gherkinDocument, err := gherkin.ParseGherkinDocumentForLanguage(io.TeeReader(reader, &buf), dialect, newIDFunc)
	if err != nil {
		return nil, fmt.Errorf("%s - %v", path, err)
	}

	gherkinDocument.Uri = path
	pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc)

	f := models.Feature{GherkinDocument: gherkinDocument, Pickles: pickles, Content: buf.Bytes()}
	return &f, nil
}

func parseFeatureDir(fsys fs.FS, dir, dialect string, newIDFunc func() string) ([]*models.Feature, error) {
	var features []*models.Feature
	return features, fs.WalkDir(fsys, dir, func(p string, f fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		if f.IsDir() {
			return nil
		}

		if !strings.HasSuffix(p, ".feature") {
			return nil
		}

		feat, err := parseFeatureFile(fsys, p, dialect, newIDFunc)
		if err != nil {
			return err
		}

		features = append(features, feat)
		return nil
	})
}

func parsePath(fsys fs.FS, path, dialect string, newIDFunc func() string) ([]*models.Feature, error) {
	var features []*models.Feature

	path, line := ExtractFeaturePathLine(path)

	fi, err := func() (fs.FileInfo, error) {
		file, err := fsys.Open(path)
		if err != nil {
			return nil, err
		}
		defer file.Close()

		return file.Stat()
	}()
	if err != nil {
		return features, err
	}

	if fi.IsDir() {
		return parseFeatureDir(fsys, path, dialect, newIDFunc)
	}

	ft, err := parseFeatureFile(fsys, path, dialect, newIDFunc)
	if err != nil {
		return features, err
	}

	// filter scenario by line number
	var pickles []*messages.Pickle

	if line != -1 {
		ft.Uri += ":" + strconv.Itoa(line)
	}

	for _, pickle := range ft.Pickles {
		sc := ft.FindScenario(pickle.AstNodeIds[0])

		if line == -1 || int64(line) == sc.Location.Line {
			if line != -1 {
				pickle.Uri += ":" + strconv.Itoa(line)
			}

			pickles = append(pickles, pickle)
		}
	}
	ft.Pickles = pickles

	return append(features, ft), nil
}

// ParseFeatures ...
func ParseFeatures(fsys fs.FS, filter, dialect string, paths []string) ([]*models.Feature, error) {
	var order int

	if dialect == "" {
		dialect = gherkin.DefaultDialect
	}

	featureIdxs := make(map[string]int)
	uniqueFeatureURI := make(map[string]*models.Feature)
	newIDFunc := (&messages.Incrementing{}).NewId
	for _, path := range paths {
		feats, err := parsePath(fsys, path, dialect, newIDFunc)

		switch {
		case os.IsNotExist(err):
			return nil, fmt.Errorf(`feature path "%s" is not available`, path)
		case os.IsPermission(err):
			return nil, fmt.Errorf(`feature path "%s" is not accessible`, path)
		case err != nil:
			return nil, err
		}

		for _, ft := range feats {
			if _, duplicate := uniqueFeatureURI[ft.Uri]; duplicate {
				continue
			}

			uniqueFeatureURI[ft.Uri] = ft
			featureIdxs[ft.Uri] = order

			order++
		}
	}

	var features = make([]*models.Feature, len(uniqueFeatureURI))
	for uri, feature := range uniqueFeatureURI {
		idx := featureIdxs[uri]
		features[idx] = feature
	}

	features = filterFeatures(filter, features)

	return features, nil
}

type FeatureContent = flags.Feature

func ParseFromBytes(filter, dialect string, featuresInputs []FeatureContent) ([]*models.Feature, error) {
	var order int

	if dialect == "" {
		dialect = gherkin.DefaultDialect
	}

	featureIdxs := make(map[string]int)
	uniqueFeatureURI := make(map[string]*models.Feature)
	newIDFunc := (&messages.Incrementing{}).NewId
	for _, f := range featuresInputs {
		ft, err := parseBytes(f.Name, f.Contents, dialect, newIDFunc)
		if err != nil {
			return nil, err
		}

		if _, duplicate := uniqueFeatureURI[ft.Uri]; duplicate {
			continue
		}

		uniqueFeatureURI[ft.Uri] = ft
		featureIdxs[ft.Uri] = order

		order++
	}

	var features = make([]*models.Feature, len(uniqueFeatureURI))
	for uri, feature := range uniqueFeatureURI {
		idx := featureIdxs[uri]
		features[idx] = feature
	}

	features = filterFeatures(filter, features)

	return features, nil
}

func filterFeatures(filter string, features []*models.Feature) (result []*models.Feature) {
	for _, ft := range features {
		ft.Pickles = tags.ApplyTagFilter(filter, ft.Pickles)

		if ft.Feature != nil && len(ft.Pickles) > 0 {
			result = append(result, ft)
		}
	}

	return
}
