package internal

import (
	"github.com/pkg/errors"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"
)

// annotation spex from https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys
const (
	separator = `(?:[-._:@+]|--)`
	alphanum  = `(?:[A-Za-z0-9]+)`
	component = `(?:` + alphanum + `(?:` + separator + alphanum + `)*)`
)

var refRegexp = regexp.MustCompile(`^` + component + `(?:/` + component + `)*$`)
var windowsRefRegexp = regexp.MustCompile(`^([a-zA-Z]:\\.+?):(.*)$`)

// ValidateImageName returns nil if the image name is empty or matches the open-containers image name specs.
// In any other case an error is returned.
func ValidateImageName(image string) error {
	if len(image) == 0 {
		return nil
	}

	var err error
	if !refRegexp.MatchString(image) {
		err = errors.Errorf("Invalid image %s", image)
	}
	return err
}

// SplitPathAndImage tries to split the provided OCI reference into the OCI path and image.
// Neither path nor image parts are validated at this stage.
func SplitPathAndImage(reference string) (string, string) {
	if runtime.GOOS == "windows" {
		return splitPathAndImageWindows(reference)
	}
	return splitPathAndImageNonWindows(reference)
}

func splitPathAndImageWindows(reference string) (string, string) {
	groups := windowsRefRegexp.FindStringSubmatch(reference)
	// nil group means no match
	if groups == nil {
		return reference, ""
	}

	// we expect three elements. First one full match, second the capture group for the path and
	// the third the capture group for the image
	if len(groups) != 3 {
		return reference, ""
	}
	return groups[1], groups[2]
}

func splitPathAndImageNonWindows(reference string) (string, string) {
	sep := strings.SplitN(reference, ":", 2)
	path := sep[0]

	var image string
	if len(sep) == 2 {
		image = sep[1]
	}
	return path, image
}

// ValidateOCIPath takes the OCI path and validates it.
func ValidateOCIPath(path string) error {
	if runtime.GOOS == "windows" {
		// On Windows we must allow for a ':' as part of the path
		if strings.Count(path, ":") > 1 {
			return errors.Errorf("Invalid OCI reference: path %s contains more than one colon", path)
		}
	} else {
		if strings.Contains(path, ":") {
			return errors.Errorf("Invalid OCI reference: path %s contains a colon", path)
		}
	}
	return nil
}

// ValidateScope validates a policy configuration scope for an OCI transport.
func ValidateScope(scope string) error {
	var err error
	if runtime.GOOS == "windows" {
		err = validateScopeWindows(scope)
	} else {
		err = validateScopeNonWindows(scope)
	}
	if err != nil {
		return err
	}

	cleaned := filepath.Clean(scope)
	if cleaned != scope {
		return errors.Errorf(`Invalid scope %s: Uses non-canonical path format, perhaps try with path %s`, scope, cleaned)
	}

	return nil
}

func validateScopeWindows(scope string) error {
	matched, _ := regexp.Match(`^[a-zA-Z]:\\`, []byte(scope))
	if !matched {
		return errors.Errorf("Invalid scope '%s'. Must be an absolute path", scope)
	}

	return nil
}

func validateScopeNonWindows(scope string) error {
	if !strings.HasPrefix(scope, "/") {
		return errors.Errorf("Invalid scope %s: must be an absolute path", scope)
	}

	// Refuse also "/", otherwise "/" and "" would have the same semantics,
	// and "" could be unexpectedly shadowed by the "/" entry.
	if scope == "/" {
		return errors.New(`Invalid scope "/": Use the generic default scope ""`)
	}

	return nil
}
