package promptfs

import (
	"fmt"
	"io/fs"
	"path"
	"strings"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/readonlyfs"
)

// ModulePromptFile maps a module-owned embedded prompt file into the registered
// module prompt directory.
type ModulePromptFile struct {
	Name string
	Path string
}

// SharedPromptFile maps a caller-owned shared prompt file into a module's
// registered sharedassets prompt subdirectory.
type SharedPromptFile struct {
	Name string
	FS   fs.FS
	Path string
}

// ModulePromptFS builds a prompt filesystem for a module directory from
// module-owned prompt files plus caller-provided shared prompt files under the
// module's sharedassets subdirectory.
func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile, sharedFiles ...SharedPromptFile) (fs.FS, error) {
	cleanModuleDir, err := cleanPromptPath(moduleDir)
	if err != nil {
		return nil, fmt.Errorf("module prompt directory: %w", err)
	}
	if moduleFS == nil {
		return nil, fmt.Errorf("module prompt filesystem must not be nil")
	}
	assets := make(map[string][]byte, len(files)+len(sharedFiles))
	moduleDestinations := make(map[string]struct{}, len(files))
	for _, file := range files {
		name, err := cleanPromptPath(file.Name)
		if err != nil {
			return nil, fmt.Errorf("module prompt file name %q: %w", file.Name, err)
		}
		if strings.Contains(name, "/") {
			return nil, fmt.Errorf("module prompt file name %q must not contain path separators", file.Name)
		}
		destination := "assets/prompts/" + cleanModuleDir + "/" + name
		if _, exists := moduleDestinations[destination]; exists {
			return nil, fmt.Errorf("duplicate module prompt destination %q", destination)
		}
		moduleDestinations[destination] = struct{}{}
		filePath, err := cleanPromptPath(file.Path)
		if err != nil {
			return nil, fmt.Errorf("module prompt file path %q: %w", file.Path, err)
		}
		data, err := fs.ReadFile(moduleFS, filePath)
		if err != nil {
			return nil, fmt.Errorf("read module prompt asset %s: %w", filePath, err)
		}
		assets[destination] = data
	}

	sharedDestinations := make(map[string]struct{}, len(sharedFiles))
	for _, file := range sharedFiles {
		name, err := cleanPromptPath(file.Name)
		if err != nil {
			return nil, fmt.Errorf("shared prompt file name %q: %w", file.Name, err)
		}
		if strings.Contains(name, "/") {
			return nil, fmt.Errorf("shared prompt file name %q must not contain path separators", file.Name)
		}
		destination := "assets/prompts/" + cleanModuleDir + "/sharedassets/" + name
		if _, exists := sharedDestinations[destination]; exists {
			return nil, fmt.Errorf("duplicate sharedassets prompt destination %q", destination)
		}
		sharedDestinations[destination] = struct{}{}
		if file.FS == nil {
			return nil, fmt.Errorf("shared prompt file %q filesystem must not be nil", file.Name)
		}
		filePath, err := cleanPromptPath(file.Path)
		if err != nil {
			return nil, fmt.Errorf("shared prompt file path %q: %w", file.Path, err)
		}
		data, err := fs.ReadFile(file.FS, filePath)
		if err != nil {
			return nil, fmt.Errorf("read shared prompt asset %s: %w", filePath, err)
		}
		assets[destination] = data
	}
	fsys, err := readonlyfs.New(assets)
	if err != nil {
		return nil, fmt.Errorf("construct prompt filesystem: %w", err)
	}
	return fsys, nil
}

func cleanPromptPath(name string) (string, error) {
	trimmed := strings.TrimSpace(name)
	if trimmed == "" {
		return "", fmt.Errorf("path must not be empty")
	}
	cleaned := path.Clean(strings.TrimPrefix(trimmed, "/"))
	if cleaned == "." || !fs.ValidPath(cleaned) {
		return "", fmt.Errorf("invalid path %q", name)
	}
	return cleaned, nil
}
