package utils

import (
	"fmt"
	"io/fs"
	"path/filepath"
	"strings"
	"sync"
)

type FSPermission uint8

const (
	AdminOnlyPermission FSPermission = iota
	AdminOnlyExecPermission
	PublicReadPermission
	PublicReadExecPermission
	PublicWritePermission
	PublicWriteExecPermission
)

// AsUnixDirExecPermission return the corresponding unix permission for a directory or executable.
func (perm FSPermission) AsUnixPermission() fs.FileMode {
	switch perm {
	case AdminOnlyPermission:
		return 0o600
	case AdminOnlyExecPermission:
		return 0o700
	case PublicReadPermission:
		return 0o644
	case PublicReadExecPermission:
		return 0o755
	case PublicWritePermission:
		return 0o666
	case PublicWriteExecPermission:
		return 0o777
	}

	return 0
}

func (perm FSPermission) IsExecPermission() bool {
	switch perm {
	case AdminOnlyExecPermission, PublicReadExecPermission, PublicWriteExecPermission:
		return true
	}
	return false
}

// DirStructure represents a directory structure with permissions that should be enforced.
type DirStructure struct {
	sync.Mutex

	Path     string
	Dir      string
	Perm     FSPermission
	Parent   *DirStructure
	Children map[string]*DirStructure
}

// NewDirStructure returns a new DirStructure.
func NewDirStructure(path string, perm FSPermission) *DirStructure {
	return &DirStructure{
		Path:     path,
		Perm:     perm,
		Children: make(map[string]*DirStructure),
	}
}

// ChildDir adds a new child DirStructure and returns it. Should the child already exist, the existing child is returned and the permissions are updated.
func (ds *DirStructure) ChildDir(dirName string, perm FSPermission) (child *DirStructure) {
	ds.Lock()
	defer ds.Unlock()

	// if exists, update
	child, ok := ds.Children[dirName]
	if ok {
		child.Perm = perm
		return child
	}

	// create new
	newDir := &DirStructure{
		Path:     filepath.Join(ds.Path, dirName),
		Dir:      dirName,
		Perm:     perm,
		Parent:   ds,
		Children: make(map[string]*DirStructure),
	}
	ds.Children[dirName] = newDir
	return newDir
}

// Ensure ensures that the specified directory structure (from the first parent on) exists.
func (ds *DirStructure) Ensure() error {
	return ds.EnsureAbsPath(ds.Path)
}

// EnsureRelPath ensures that the specified directory structure (from the first parent on) and the given relative path (to the DirStructure) exists.
func (ds *DirStructure) EnsureRelPath(dirPath string) error {
	return ds.EnsureAbsPath(filepath.Join(ds.Path, dirPath))
}

// EnsureRelDir ensures that the specified directory structure (from the first parent on) and the given relative path (to the DirStructure) exists.
func (ds *DirStructure) EnsureRelDir(dirNames ...string) error {
	return ds.EnsureAbsPath(filepath.Join(append([]string{ds.Path}, dirNames...)...))
}

// EnsureAbsPath ensures that the specified directory structure (from the first parent on) and the given absolute path exists.
// If the given path is outside the DirStructure, an error will be returned.
func (ds *DirStructure) EnsureAbsPath(dirPath string) error {
	// always start at the top
	if ds.Parent != nil {
		return ds.Parent.EnsureAbsPath(dirPath)
	}

	// check if root
	if dirPath == ds.Path {
		return ds.ensure(nil)
	}

	// check scope
	slashedPath := ds.Path
	// add slash to end
	if !strings.HasSuffix(slashedPath, string(filepath.Separator)) {
		slashedPath += string(filepath.Separator)
	}
	// check if given path is in scope
	if !strings.HasPrefix(dirPath, slashedPath) {
		return fmt.Errorf(`path "%s" is outside of DirStructure scope`, dirPath)
	}

	// get relative path
	relPath, err := filepath.Rel(ds.Path, dirPath)
	if err != nil {
		return fmt.Errorf("failed to get relative path: %w", err)
	}

	// split to path elements
	pathDirs := strings.Split(filepath.ToSlash(relPath), "/")

	// start checking
	return ds.ensure(pathDirs)
}

func (ds *DirStructure) ensure(pathDirs []string) error {
	ds.Lock()
	defer ds.Unlock()

	// check current dir
	err := EnsureDirectory(ds.Path, ds.Perm)
	if err != nil {
		return err
	}

	if len(pathDirs) == 0 {
		// we reached the end!
		return nil
	}

	child, ok := ds.Children[pathDirs[0]]
	if !ok {
		// we have reached the end of the defined dir structure
		// ensure all remaining dirs
		dirPath := ds.Path
		for _, dir := range pathDirs {
			dirPath = filepath.Join(dirPath, dir)
			err := EnsureDirectory(dirPath, ds.Perm)
			if err != nil {
				return err
			}
		}
		return nil
	}

	// we got a child, continue
	return child.ensure(pathDirs[1:])
}
