package models

import (
	"fmt"
	"regexp"
	"strings"
)

type CacheEntry struct {
	Key   string   `yaml:"key"`
	Hash  []string `yaml:"hash"`
	Paths []string `yaml:"paths"`
	// 1 is fastest, 19 is smallest, 0 is the zstd default
	CompressionLevel int `yaml:"compression-level"`
	// on-success is the default, always also saves on failed runs
	When string `yaml:"when"`
}

// keys become storage paths, so no slashes
var cacheKeyRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`)

func (c CacheEntry) Validate() error {
	if !cacheKeyRe.MatchString(c.Key) {
		return fmt.Errorf("cache: invalid key %q (allowed: letters, digits, '.', '_', '-')", c.Key)
	}
	for _, f := range c.Hash {
		// rev-parse needs plain repo-relative paths, not pathspecs
		if f == "" || strings.HasPrefix(f, "/") || strings.HasPrefix(f, "..") {
			return fmt.Errorf("cache %q: hash path %q is not repo-relative", c.Key, f)
		}
		if strings.ContainsAny(f, ": \t\n\"'`$\\*?[") || strings.Contains(f, "/../") || strings.HasSuffix(f, "/..") {
			return fmt.Errorf("cache %q: hash path %q contains unsupported characters", c.Key, f)
		}
	}
	if len(c.Paths) == 0 {
		return fmt.Errorf("cache %q: no paths", c.Key)
	}
	if c.CompressionLevel < 0 || c.CompressionLevel > 19 {
		return fmt.Errorf("cache %q: compression-level %d out of range (1-19)", c.Key, c.CompressionLevel)
	}
	switch c.When {
	case "", "on-success", "always":
	default:
		return fmt.Errorf("cache %q: when %q is not one of on-success, always", c.Key, c.When)
	}
	seen := make(map[string]bool, len(c.Paths))
	for _, p := range c.Paths {
		if p == "" {
			return fmt.Errorf("cache %q: empty path", c.Key)
		}
		if strings.ContainsAny(p, " \t\n\"'`$\\") {
			return fmt.Errorf("cache %q: path %q contains unsupported characters", c.Key, p)
		}
		if strings.Contains(p, "..") {
			return fmt.Errorf("cache %q: path %q must not contain '..'", c.Key, p)
		}
		if seen[p] {
			return fmt.Errorf("cache %q: duplicate path %q", c.Key, p)
		}
		seen[p] = true
	}
	return nil
}
