// Package fileio provides confined, atomic artifact writes.
package fileio

import (
	"encoding/json"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"
)

// EncodePathComponent returns a filesystem-safe, injective representation of
// one logical path component.
func EncodePathComponent(value string) string {
	if value == "" {
		return "%"
	}

	const hexadecimal = "0123456789ABCDEF"
	var out strings.Builder
	for index := 0; index < len(value); index++ {
		byteValue := value[index]
		switch {
		case byteValue >= 'a' && byteValue <= 'z', byteValue >= 'A' && byteValue <= 'Z', byteValue >= '0' && byteValue <= '9', byteValue == '-', byteValue == '_':
			out.WriteByte(byteValue)
		case byteValue == '.' && safePathDot(value, index):
			out.WriteByte(byteValue)
		default:
			out.WriteByte('%')
			out.WriteByte(hexadecimal[byteValue>>4])
			out.WriteByte(hexadecimal[byteValue&0x0f])
		}
	}
	return out.String()
}

func safePathDot(value string, index int) bool {
	if value == "." || value == ".." {
		return false
	}
	return (index == 0 || value[index-1] != '.') && (index+1 == len(value) || value[index+1] != '.')
}

func SafePath(root, name string) (string, error) {
	root = strings.TrimSpace(root)
	if root == "" {
		return "", fmt.Errorf("file root must not be empty")
	}
	name = strings.TrimSpace(name)
	if name == "" {
		return "", fmt.Errorf("artifact name must not be empty")
	}
	if strings.ContainsRune(name, '\\') {
		return "", fmt.Errorf("artifact name %q must use slash-separated relative paths", name)
	}
	if path.IsAbs(name) || filepath.IsAbs(name) {
		return "", fmt.Errorf("artifact name %q must be relative", name)
	}
	if name == "." || strings.Contains(name, "..") {
		return "", fmt.Errorf("artifact name %q must not contain ..", name)
	}
	if path.Clean(name) != name {
		return "", fmt.Errorf("artifact name %q must be clean", name)
	}
	absRoot, err := filepath.Abs(root)
	if err != nil {
		return "", fmt.Errorf("resolve file root %q: %w", root, err)
	}
	target, err := filepath.Abs(filepath.Join(absRoot, filepath.FromSlash(name)))
	if err != nil {
		return "", fmt.Errorf("resolve artifact %q: %w", name, err)
	}
	rel, err := filepath.Rel(absRoot, target)
	if err != nil {
		return "", fmt.Errorf("resolve artifact %q: %w", name, err)
	}
	if rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
		return "", fmt.Errorf("artifact name %q resolves outside file root", name)
	}
	if err := rejectSymlinkComponents(absRoot, name); err != nil {
		return "", err
	}
	return target, nil
}

func WriteJSON(root, name string, payload any, dirMode, fileMode os.FileMode) error {
	data, err := json.MarshalIndent(payload, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal artifact %q: %w", name, err)
	}
	return WriteBytes(root, name, append(data, '\n'), dirMode, fileMode)
}

func WriteBytes(root, name string, data []byte, dirMode, fileMode os.FileMode) error {
	target, err := SafePath(root, name)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(target), dirMode); err != nil {
		return fmt.Errorf("write artifact %q: %w", name, err)
	}
	if err := rejectSymlinkComponents(root, name); err != nil {
		return err
	}
	if err := writeAtomic(target, data, fileMode); err != nil {
		return fmt.Errorf("write artifact %q: %w", name, err)
	}
	return nil
}

func rejectSymlinkComponents(root, name string) error {
	absRoot, err := filepath.Abs(root)
	if err != nil {
		return fmt.Errorf("resolve file root %q: %w", root, err)
	}
	current := absRoot
	for _, component := range strings.Split(filepath.FromSlash(name), string(filepath.Separator)) {
		if component == "" || component == "." {
			continue
		}
		current = filepath.Join(current, component)
		info, err := os.Lstat(current)
		if err != nil {
			if os.IsNotExist(err) {
				return nil
			}
			return fmt.Errorf("inspect artifact path %q: %w", name, err)
		}
		if info.Mode()&os.ModeSymlink != 0 {
			return fmt.Errorf("artifact path %q must not traverse symbolic links", name)
		}
	}
	return nil
}

func writeAtomic(target string, data []byte, fileMode os.FileMode) error {
	temp, err := os.CreateTemp(filepath.Dir(target), "."+filepath.Base(target)+".tmp-*")
	if err != nil {
		return err
	}
	tempPath := temp.Name()
	keep := true
	defer func() {
		if keep {
			_ = os.Remove(tempPath)
		}
	}()
	if _, err := temp.Write(data); err != nil {
		_ = temp.Close()
		return err
	}
	if err := temp.Chmod(fileMode); err != nil {
		_ = temp.Close()
		return err
	}
	if err := temp.Close(); err != nil {
		return err
	}
	if err := os.Rename(tempPath, target); err != nil {
		return err
	}
	keep = false
	return nil
}
