package csbi

import (
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	gpb "github.com/openconfig/gnmi/proto/gnmi"
	log "github.com/sirupsen/logrus"
)

// Repository provides access to yang model files.
type Repository interface {
	// FindYANGFiles returns all YANG files matching the provided ModelData
	FindYANGFiles(models []*gpb.ModelData) ([]string, []error)
	// YANGPathsWithSuffix returns all YANG search paths with a '...' suffix
	YANGPathsWithSuffix() ([]string, error)
}

// NewRepository returns a implementation of the Repository interface.
func NewRepository(basePath string) Repository {
	return &repo{
		fs: &filesystem{
			root: basePath,
		},
	}
}

type repo struct {
	fs Filesystem
}

func (r *repo) YANGPathsWithSuffix() ([]string, error) {
	pathSet := make(map[string]struct{})
	paths, err := r.fs.Glob(".yang")
	out := make([]string, 0)
	if err != nil {
		return nil, err
	}
	for _, p := range paths {
		p = p[:strings.LastIndex(p, "/")]
		_, exist := pathSet[p]
		if !exist {
			pathSet[p] = struct{}{}
			out = append(out, filepath.Join(p, "..."))
		}
	}
	return out, nil
}

func (r *repo) FindYANGFiles(models []*gpb.ModelData) ([]string, []error) {
	filePaths := make([]string, 0)
	errs := make([]error, 0)
	for _, model := range models {
		if !(strings.Contains(model.Name, "augments") || strings.Contains(model.Name, "deviations")) {
			path, err := searchYANGFile(r.fs, model.Name, model.Organization, model.Version)
			if err != nil {
				log.Error(err)
				errs = append(errs, err)
				continue
			}
			filePaths = append(filePaths, path)
		} else {
			log.WithFields(log.Fields{
				"model": model.Name,
			}).Info("augments and deviations are ignored")
		}
	}
	return filePaths, errs
}

// The Filesystem interface defines the implementation required for any YANG
// model repository data source.
type Filesystem interface {
	fs.FS
	fs.GlobFS
}

type filesystem struct {
	root string
}

func (osfs *filesystem) Open(name string) (fs.File, error) {
	if err := osfs.fsValid(name); err != nil {
		return nil, err
	}
	return os.Open(name)
}

func (osfs *filesystem) ReadDir(name string) ([]fs.DirEntry, error) {
	if err := osfs.fsValid(name); err != nil {
		return nil, err
	}
	return os.ReadDir(name)
}

func (osfs *filesystem) ReadFile(name string) ([]byte, error) {
	if err := osfs.fsValid(name); err != nil {
		return nil, err
	}
	return os.ReadFile(name)
}

func (osfs *filesystem) Stat(name string) (fs.FileInfo, error) {
	if err := osfs.fsValid(name); err != nil {
		return nil, err
	}
	return os.Stat(name)
}

func (osfs *filesystem) Glob(pattern string) (paths []string, err error) {
	defer func() {
		if e := recover(); e != nil {
			err = e.(error) //nolint:errcheck
			paths = nil
		}
	}()
	_, err = osfs.ReadDir(osfs.root)
	if err != nil {
		return nil, err
	}

	paths = make([]string, 0)
	re := regexp.MustCompile(pattern)
	if err := filepath.WalkDir(osfs.root, func(path string, d fs.DirEntry, err error) error {
		if re.Match([]byte(d.Name())) {
			p := filepath.Join(path)
			paths = append(paths, p)
		}
		return nil
	}); err != nil {
		return nil, err
	}
	return paths, nil
}

func (osfs *filesystem) fsValid(name string) error {
	fullName := filepath.Join(osfs.root, name)
	if !fs.ValidPath(fullName) {
		return &fs.PathError{Err: fs.ErrInvalid}
	}
	return nil
}

func findBestMatch(paths []string, org, version string) (string, error) {
	out := make([]string, 0)
	for _, path := range paths {
		if strings.Contains(path, org) {
			out = append(out, path)
		}
	}
	if len(out) > 0 {
		return out[0], nil
	}
	return paths[0], nil
}

func searchYANGFile(fsys Filesystem, name, org, version string) (string, error) {
	paths, err := fsys.Glob(name + ".yang")
	if err != nil {
		return "", err
	}
	if len(paths) == 0 {
		return "", fmt.Errorf("did not find file for %v in %v", name, fsys)
	}
	return findBestMatch(paths, org, version)
}
