package promptfs

import (
	"bytes"
	"fmt"
	"io"
	"io/fs"
	"path"
	"sort"
	"strings"
	"time"
)

// 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(promptMapFS, len(files)+len(sharedFiles))
	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)
		}
		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["assets/prompts/"+cleanModuleDir+"/"+name] = append([]byte(nil), data...)
	}

	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)
		}
		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["assets/prompts/"+cleanModuleDir+"/sharedassets/"+name] = append([]byte(nil), data...)
	}
	return assets, nil
}

type promptMapFS map[string][]byte

func (m promptMapFS) Open(name string) (fs.File, error) {
	cleaned, err := cleanPromptFSPath(name)
	if err != nil {
		return nil, &fs.PathError{Op: "open", Path: name, Err: err}
	}
	if data, ok := m[cleaned]; ok {
		return &promptFile{
			reader: bytes.NewReader(data),
			info:   promptFileInfo{name: path.Base(cleaned), size: int64(len(data))},
		}, nil
	}
	entries := m.dirEntries(cleaned)
	if entries != nil {
		return &promptDir{name: path.Base(cleaned), entries: entries}, nil
	}
	return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}

func (m promptMapFS) ReadDir(name string) ([]fs.DirEntry, error) {
	cleaned, err := cleanPromptFSPath(name)
	if err != nil {
		return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
	}
	entries := m.dirEntries(cleaned)
	if entries == nil {
		return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist}
	}
	return entries, nil
}

func (m promptMapFS) dirEntries(dir string) []fs.DirEntry {
	children := map[string]promptDirEntry{}
	prefix := ""
	if dir != "." {
		prefix = dir + "/"
	}
	for name, data := range m {
		if !strings.HasPrefix(name, prefix) {
			continue
		}
		rest := strings.TrimPrefix(name, prefix)
		if rest == "" {
			continue
		}
		childName, _, hasSlash := strings.Cut(rest, "/")
		entry := promptDirEntry{name: childName, dir: hasSlash}
		if !hasSlash {
			entry.size = int64(len(data))
		}
		children[childName] = entry
	}
	if len(children) == 0 {
		return nil
	}
	names := make([]string, 0, len(children))
	for name := range children {
		names = append(names, name)
	}
	sort.Strings(names)
	entries := make([]fs.DirEntry, 0, len(names))
	for _, name := range names {
		entries = append(entries, children[name])
	}
	return entries
}

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
}

func cleanPromptFSPath(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 == "." {
		return cleaned, nil
	}
	if !fs.ValidPath(cleaned) {
		return "", fmt.Errorf("invalid path %q", name)
	}
	return cleaned, nil
}

type promptFile struct {
	reader *bytes.Reader
	info   promptFileInfo
}

func (f *promptFile) Stat() (fs.FileInfo, error) { return f.info, nil }
func (f *promptFile) Read(p []byte) (int, error) { return f.reader.Read(p) }
func (f *promptFile) Close() error               { return nil }

type promptDir struct {
	name    string
	offset  int
	entries []fs.DirEntry
}

func (d *promptDir) Stat() (fs.FileInfo, error) { return promptFileInfo{name: d.name, dir: true}, nil }
func (d *promptDir) Read([]byte) (int, error)   { return 0, io.EOF }
func (d *promptDir) Close() error               { return nil }
func (d *promptDir) ReadDir(n int) ([]fs.DirEntry, error) {
	if d.offset >= len(d.entries) {
		return nil, io.EOF
	}
	end := len(d.entries)
	if n > 0 && d.offset+n < end {
		end = d.offset + n
	}
	out := append([]fs.DirEntry(nil), d.entries[d.offset:end]...)
	d.offset = end
	return out, nil
}

type promptDirEntry struct {
	name string
	dir  bool
	size int64
}

func (e promptDirEntry) Name() string      { return e.name }
func (e promptDirEntry) IsDir() bool       { return e.dir }
func (e promptDirEntry) Type() fs.FileMode { return e.fileInfoMode().Type() }
func (e promptDirEntry) Info() (fs.FileInfo, error) {
	return promptFileInfo{name: e.name, dir: e.dir, size: e.size}, nil
}
func (e promptDirEntry) fileInfoMode() fs.FileMode {
	if e.dir {
		return fs.ModeDir | 0o555
	}
	return 0o444
}

type promptFileInfo struct {
	name string
	dir  bool
	size int64
}

func (i promptFileInfo) Name() string       { return i.name }
func (i promptFileInfo) Size() int64        { return i.size }
func (i promptFileInfo) Mode() fs.FileMode  { return promptDirEntry{dir: i.dir}.fileInfoMode() }
func (i promptFileInfo) ModTime() time.Time { return time.Time{} }
func (i promptFileInfo) IsDir() bool        { return i.dir }
func (i promptFileInfo) Sys() any           { return nil }
