package restic

import (
	"io"
	"io/fs"
	"sort"
	"strings"
	"time"
)

// Compile-time interface checks.
var (
	_ fs.FS        = (*SnapshotFS)(nil)
	_ fs.ReadDirFS = (*SnapshotFS)(nil)
	_ fs.StatFS    = (*SnapshotFS)(nil)
)

// fsNode is the internal tree node used by SnapshotFS. It holds
// metadata for one file or directory and, for directories, an index of
// immediate children keyed by name.
type fsNode struct {
	name     string
	isDir    bool
	size     int64
	mode     fs.FileMode
	modTime  time.Time
	children map[string]*fsNode // nil for files
}

// SnapshotFS is a read-only, metadata-only fs.FS built from a flat
// list of LsNode values (as produced by ParseLsNodes / RunLs).
//
// Paths inside the FS are slash-separated and relative; the root is ".".
// No file content is available — Open returns files whose Read always
// returns io.EOF. This is a browsing-only filesystem for the file picker.
type SnapshotFS struct {
	root *fsNode
}

// NewSnapshotFS constructs a SnapshotFS from a flat slice of LsNode
// values. Absolute paths (as restic produces) are converted to
// relative paths. Intermediate directories that lack explicit node
// entries are synthesized with conservative metadata.
func NewSnapshotFS(nodes []LsNode) *SnapshotFS {
	root := &fsNode{
		name:     ".",
		isDir:    true,
		mode:     fs.ModeDir | 0o555,
		children: make(map[string]*fsNode),
	}

	sfs := &SnapshotFS{root: root}

	for _, n := range nodes {
		// Strip leading slash to make paths relative.
		rel := strings.TrimPrefix(n.Path, "/")
		if rel == "" {
			continue
		}

		isDir := n.Type == "dir"
		mode := fs.FileMode(n.Mode) & fs.ModePerm
		if isDir {
			mode |= fs.ModeDir
		}

		leaf := &fsNode{
			name:    n.Name,
			isDir:   isDir,
			size:    n.Size,
			mode:    mode,
			modTime: n.Mtime,
		}
		if isDir {
			leaf.children = make(map[string]*fsNode)
		}

		sfs.insert(rel, leaf)
	}

	return sfs
}

// insert places leaf at the given relative path, synthesizing any
// missing intermediate directories. If a synthesized directory already
// exists at the target path (created as a parent of an earlier node),
// the explicit metadata from leaf replaces it while preserving existing
// children.
func (sfs *SnapshotFS) insert(rel string, leaf *fsNode) {
	parts := strings.Split(rel, "/")
	cur := sfs.root

	// Walk/create all intermediate directories.
	for _, seg := range parts[:len(parts)-1] {
		child, ok := cur.children[seg]
		if !ok {
			child = &fsNode{
				name:     seg,
				isDir:    true,
				mode:     fs.ModeDir | 0o555,
				children: make(map[string]*fsNode),
			}
			cur.children[seg] = child
		}
		cur = child
	}

	// Place the leaf. If a synthesized dir already occupies this slot,
	// preserve its children but upgrade metadata.
	name := parts[len(parts)-1]
	if existing, ok := cur.children[name]; ok && existing.isDir && leaf.isDir {
		existing.name = leaf.name
		existing.mode = leaf.mode
		existing.modTime = leaf.modTime
		existing.size = leaf.size
	} else {
		cur.children[name] = leaf
	}
}

// Open opens the named file or directory. The name must be a valid
// fs.FS path (slash-separated, no leading slash, root is ".").
func (sfs *SnapshotFS) Open(name string) (fs.File, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
	}

	node := sfs.lookup(name)
	if node == nil {
		return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
	}

	return &snapshotFile{node: node, name: name}, nil
}

// ReadDir reads the named directory and returns a list of directory
// entries sorted by name.
func (sfs *SnapshotFS) ReadDir(name string) ([]fs.DirEntry, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
	}

	node := sfs.lookup(name)
	if node == nil {
		return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist}
	}
	if !node.isDir {
		return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrInvalid}
	}

	return sortedEntries(node), nil
}

// sortedEntries returns the children of a directory node as a
// name-sorted slice of fs.DirEntry values.
func sortedEntries(node *fsNode) []fs.DirEntry {
	entries := make([]fs.DirEntry, 0, len(node.children))
	for _, child := range node.children {
		entries = append(entries, &snapshotDirEntry{node: child})
	}
	sort.Slice(entries, func(i, j int) bool {
		return entries[i].Name() < entries[j].Name()
	})
	return entries
}

// Stat returns a FileInfo describing the named file or directory.
func (sfs *SnapshotFS) Stat(name string) (fs.FileInfo, error) {
	if !fs.ValidPath(name) {
		return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid}
	}

	node := sfs.lookup(name)
	if node == nil {
		return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrNotExist}
	}

	return &snapshotFileInfo{node: node}, nil
}

// lookup resolves a valid fs.FS path to the corresponding fsNode,
// or nil if not found.
func (sfs *SnapshotFS) lookup(name string) *fsNode {
	if name == "." {
		return sfs.root
	}

	cur := sfs.root
	for seg := range strings.SplitSeq(name, "/") {
		child, ok := cur.children[seg]
		if !ok {
			return nil
		}
		cur = child
	}
	return cur
}

// --- fs.File implementation ---

// snapshotFile is a metadata-only fs.File. Read always returns io.EOF.
// For directories, ReadDir is stateful: successive calls with n > 0
// advance through the sorted entry list per the fs.ReadDirFile contract.
type snapshotFile struct {
	node    *fsNode
	name    string
	dirRead int // offset into sorted children for ReadDir(n)
}

func (f *snapshotFile) Stat() (fs.FileInfo, error) {
	return &snapshotFileInfo{node: f.node}, nil
}

func (f *snapshotFile) Read([]byte) (int, error) {
	return 0, io.EOF
}

func (f *snapshotFile) Close() error {
	return nil
}

// ReadDir implements fs.ReadDirFile. For directories, successive calls
// with n > 0 advance through the sorted entry list. Calling with n <= 0
// returns all remaining entries.
func (f *snapshotFile) ReadDir(n int) ([]fs.DirEntry, error) {
	if !f.node.isDir {
		return nil, &fs.PathError{Op: "readdir", Path: f.name, Err: fs.ErrInvalid}
	}

	entries := sortedEntries(f.node)
	remaining := entries[f.dirRead:]

	if n <= 0 {
		f.dirRead = len(entries)
		return remaining, nil
	}

	if len(remaining) == 0 {
		return nil, io.EOF
	}
	if n > len(remaining) {
		f.dirRead = len(entries)
		return remaining, io.EOF
	}
	f.dirRead += n
	return remaining[:n], nil
}

// --- fs.FileInfo implementation ---

type snapshotFileInfo struct {
	node *fsNode
}

func (fi *snapshotFileInfo) Name() string               { return fi.node.name }
func (fi *snapshotFileInfo) Size() int64                { return fi.node.size }
func (fi *snapshotFileInfo) Mode() fs.FileMode          { return fi.node.mode }
func (fi *snapshotFileInfo) ModTime() time.Time         { return fi.node.modTime }
func (fi *snapshotFileInfo) IsDir() bool                { return fi.node.isDir }
func (fi *snapshotFileInfo) Sys() any                   { return nil }
func (fi *snapshotFileInfo) Info() (fs.FileInfo, error) { return fi, nil }

// --- fs.DirEntry implementation ---

type snapshotDirEntry struct {
	node *fsNode
}

func (de *snapshotDirEntry) Name() string               { return de.node.name }
func (de *snapshotDirEntry) IsDir() bool                { return de.node.isDir }
func (de *snapshotDirEntry) Type() fs.FileMode          { return de.node.mode.Type() }
func (de *snapshotDirEntry) Info() (fs.FileInfo, error) { return &snapshotFileInfo{node: de.node}, nil }
