package revision

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/fs"
	"log"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

// revisionsDirName is the on-disk directory under a workspace that holds
// persisted revision metadata and per-revision file snapshots.
const revisionsDirName = ".revisions"

// metadataFileName is the JSON file listing revisions + pointer + next_id.
const metadataFileName = "metadata.json"

// Snapshot scopes. Revisions persisted before the workspace-wide scope only
// captured src/ (the React app tree); they restore with src-only deletion
// semantics so a legacy restore can never delete root files it never tracked.
const (
	// ScopeSrc is the legacy scope: snapshots covered only src/.
	ScopeSrc = "src"
	// ScopeWorkspace covers the whole workspace minus development/internal
	// directories — required for WordPress themes, whose files live at the
	// workspace root, and for website root files (index.html, public/).
	ScopeWorkspace = "workspace"
)

// excludedDirNames are directories never captured nor touched by restores,
// at any depth. Dot-prefixed entries (.git, .webby, .revisions, …) are also
// excluded by name prefix.
var excludedDirNames = map[string]bool{
	"node_modules": true,
	"dist":         true,
}

// persistedMetadata is the on-disk shape for metadata.json.
type persistedMetadata struct {
	Pointer   int              `json:"pointer"`
	NextID    int              `json:"next_id"`
	Revisions []persistedEntry `json:"revisions"`
}

type persistedEntry struct {
	ID        int       `json:"id"`
	Label     string    `json:"label"`
	FileCount int       `json:"file_count"`
	Timestamp time.Time `json:"timestamp"`
	// Scope is empty for legacy (src-only) revisions.
	Scope string `json:"scope,omitempty"`
}

// FileSnapshot captures a single file's identity within a revision. Content
// bytes live on disk under .revisions/{id}/ — they are NOT retained in
// memory (a builder hosting many workspaces would otherwise hold every
// revision of every src tree in RAM).
type FileSnapshot struct {
	Path string `json:"path"`
	Size int64  `json:"size"`
}

// Revision represents a workspace state at a point in time
type Revision struct {
	ID        int            `json:"id"`
	Label     string         `json:"label"`
	Files     []FileSnapshot `json:"-"`
	FileCount int            `json:"file_count"`
	Timestamp time.Time      `json:"timestamp"`
	Scope     string         `json:"scope,omitempty"`
}

// RevisionInfo is the API-safe representation (no file content)
type RevisionInfo struct {
	ID        int       `json:"id"`
	Label     string    `json:"label"`
	FileCount int       `json:"file_count"`
	Timestamp time.Time `json:"timestamp"`
}

func (r *Revision) toInfo() RevisionInfo {
	return RevisionInfo{
		ID:        r.ID,
		Label:     r.Label,
		FileCount: r.FileCount,
		Timestamp: r.Timestamp,
	}
}

// scopeOrLegacy normalizes an entry's scope: empty means legacy src-only.
func scopeOrLegacy(scope string) string {
	if scope == "" {
		return ScopeSrc
	}
	return scope
}

// Manager manages revision snapshots for a workspace.
//
// Concurrency: mu is an RWMutex — methods that mutate revision state
// (CreateSnapshot, Undo, Redo, RestoreTo, LoadFromDisk) take the write lock;
// read-only methods (List, ListPaginated) take the read lock so concurrent
// panel reloads don't serialize behind each other or behind the long
// file-write phase of CreateSnapshot.
type Manager struct {
	workspacePath string
	revisions     []Revision
	pointer       int // Current position in revision stack (-1 = no revisions)
	mu            sync.RWMutex
	maxRevisions  int
	nextID        int
}

// NewManager creates a revision manager for the given workspace
func NewManager(workspacePath string) *Manager {
	return &Manager{
		workspacePath: workspacePath,
		pointer:       -1,
		maxRevisions:  20,
		nextID:        1,
	}
}

// CreateSnapshot captures the current workspace state (ScopeWorkspace).
// Call this BEFORE a change (agent run, style edit, design apply) to save
// the "before" state.
//
// If the live state is byte-identical to the revision at the pointer, no new
// entry is appended — but any redo branch is still truncated, so a new
// checkpoint always makes history linear again.
func (m *Manager) CreateSnapshot(label string) error {
	m.mu.Lock()
	defer m.mu.Unlock()
	return m.createSnapshotLocked(label)
}

// liveFile pairs a captured path with its content during snapshot/compare.
// Content is transient — it is written to .revisions/{id}/ and dropped.
type liveFile struct {
	path    string
	content []byte
}

func (m *Manager) createSnapshotLocked(label string) error {
	files, err := m.captureLiveFiles(ScopeWorkspace)
	if err != nil {
		return fmt.Errorf("failed to snapshot files: %w", err)
	}

	// Duplicate detection: when the live bytes equal the current revision,
	// appending would only pollute history. Still truncate any redo branch —
	// the caller is checkpointing because a change is about to happen, and a
	// surviving redo branch could silently clobber it later.
	if m.pointer >= 0 && m.pointer < len(m.revisions) && m.liveEqualsRevision(files, &m.revisions[m.pointer]) {
		if m.pointer < len(m.revisions)-1 {
			for _, dropped := range m.revisions[m.pointer+1:] {
				m.removeRevisionDir(dropped.ID)
			}
			m.revisions = m.revisions[:m.pointer+1]
			if err := m.persistMetadata(); err != nil {
				return fmt.Errorf("failed to persist metadata: %w", err)
			}
		}
		return nil
	}

	// If we're not at the end of the stack, truncate future revisions —
	// both in memory and on disk.
	if m.pointer < len(m.revisions)-1 {
		for _, dropped := range m.revisions[m.pointer+1:] {
			m.removeRevisionDir(dropped.ID)
		}
		m.revisions = m.revisions[:m.pointer+1]
	}

	rev := Revision{
		ID:        m.nextID,
		Label:     label,
		FileCount: len(files),
		Timestamp: time.Now(),
		Scope:     ScopeWorkspace,
	}
	rev.Files = make([]FileSnapshot, 0, len(files))
	for _, f := range files {
		rev.Files = append(rev.Files, FileSnapshot{Path: f.path, Size: int64(len(f.content))})
	}

	// Persist BEFORE committing to in-memory state so a disk failure leaves
	// the manager exactly as it was — no ghost revision. If writeRevisionFiles
	// partially succeeds (some files written), the whole {id}/ tree is removed
	// so we don't leave orphan bytes on disk either. nextID stays untouched.
	if err := m.writeRevisionFiles(rev.ID, files); err != nil {
		m.removeRevisionDir(rev.ID)
		return fmt.Errorf("failed to persist revision files: %w", err)
	}

	// Commit in-memory state now that disk has the bytes.
	m.nextID++
	m.revisions = append(m.revisions, rev)
	m.pointer = len(m.revisions) - 1

	// Enforce max revisions
	if len(m.revisions) > m.maxRevisions {
		excess := len(m.revisions) - m.maxRevisions
		for _, dropped := range m.revisions[:excess] {
			m.removeRevisionDir(dropped.ID)
		}
		m.revisions = m.revisions[excess:]
		m.pointer -= excess
		if m.pointer < 0 {
			m.pointer = 0
		}
	}

	// Persist metadata. On failure, roll back the in-memory append so future
	// loads stay consistent with what's on disk. The revision dir we wrote
	// above is cleaned up to match. Note: if the maxRevisions eviction above
	// already ran, the rollback keeps the trimmed slice — the evicted dirs
	// are gone and cannot be undeleted; on restart LoadFromDisk skips their
	// missing entries and clamps the pointer.
	if err := m.persistMetadata(); err != nil {
		m.removeRevisionDir(rev.ID)
		m.revisions = m.revisions[:len(m.revisions)-1]
		m.pointer = len(m.revisions) - 1
		m.nextID--
		return fmt.Errorf("failed to persist metadata: %w", err)
	}

	return nil
}

// captureLiveCurrentLocked checkpoints un-snapshotted live changes before a
// pointer move. The production flow only snapshots BEFORE each agent run, so
// when the pointer sits at the top of the stack the live workspace usually
// holds the last run's (un-checkpointed) output — without this capture, the
// first undo would skip one user-visible state and the newest state would be
// unrecoverable. createSnapshotLocked's duplicate detection makes this a
// no-op when the live state already matches the top revision.
//
// Mid-stack dirty states are intentionally NOT preserved: capturing there
// would have to truncate the redo branch (history is linear), destroying the
// states the user just undid past. Manual file-editor changes made while
// mid-stack are therefore discarded by the next Undo/Redo/RestoreTo — the
// same trade-off the pre-existing Undo behavior made.
func (m *Manager) captureLiveCurrentLocked() error {
	if m.pointer != len(m.revisions)-1 {
		return nil
	}
	return m.createSnapshotLocked("Current state")
}

// Undo reverts workspace to the previous revision state.
// Returns info about the restored revision, or error if at the beginning.
func (m *Manager) Undo() (*RevisionInfo, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if len(m.revisions) == 0 || m.pointer < 0 {
		return nil, fmt.Errorf("nothing to undo")
	}

	// Preserve live changes so redo can return to them. Failing to capture
	// would make the undo destructive — abort instead.
	if err := m.captureLiveCurrentLocked(); err != nil {
		return nil, fmt.Errorf("failed to preserve current state: %w", err)
	}

	if m.pointer <= 0 {
		return nil, fmt.Errorf("nothing to undo")
	}

	m.pointer--
	rev := &m.revisions[m.pointer]
	if err := m.restoreRevision(rev); err != nil {
		m.pointer++ // rollback on failure
		return nil, fmt.Errorf("failed to restore revision: %w", err)
	}

	// Pointer moved on disk via the restored files, but the pointer position
	// itself is only durable once metadata.json is re-written. A disk error
	// here means the in-memory pointer is ahead of what will be reloaded on
	// the next restart — surface it in the log so it isn't silent.
	if err := m.persistMetadata(); err != nil {
		log.Printf("revision: persistMetadata after undo failed: %v", err)
	}

	info := rev.toInfo()
	return &info, nil
}

// Redo moves forward to the next revision.
// Returns info about the restored revision, or error if at the end.
func (m *Manager) Redo() (*RevisionInfo, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.pointer >= len(m.revisions)-1 {
		return nil, fmt.Errorf("nothing to redo")
	}

	m.pointer++
	rev := &m.revisions[m.pointer]
	if err := m.restoreRevision(rev); err != nil {
		m.pointer-- // rollback pointer on failure
		return nil, fmt.Errorf("failed to restore revision: %w", err)
	}

	if err := m.persistMetadata(); err != nil {
		log.Printf("revision: persistMetadata after redo failed: %v", err)
	}

	info := rev.toInfo()
	return &info, nil
}

// RestoreTo jumps directly to the revision with the given id (the Revision
// History panel's "Restore" action). Un-checkpointed live changes are
// captured first so the jump is always reversible.
func (m *Manager) RestoreTo(id int) (*RevisionInfo, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	idx := -1
	for i := range m.revisions {
		if m.revisions[i].ID == id {
			idx = i
			break
		}
	}
	if idx == -1 {
		return nil, fmt.Errorf("revision %d not found", id)
	}

	if err := m.captureLiveCurrentLocked(); err != nil {
		return nil, fmt.Errorf("failed to preserve current state: %w", err)
	}

	prevPointer := m.pointer
	m.pointer = idx
	rev := &m.revisions[idx]
	if err := m.restoreRevision(rev); err != nil {
		m.pointer = prevPointer // rollback on failure
		return nil, fmt.Errorf("failed to restore revision: %w", err)
	}

	if err := m.persistMetadata(); err != nil {
		log.Printf("revision: persistMetadata after restore failed: %v", err)
	}

	info := rev.toInfo()
	return &info, nil
}

// List returns all revisions with the current pointer position.
func (m *Manager) List() ([]RevisionInfo, int) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	infos := make([]RevisionInfo, len(m.revisions))
	for i, rev := range m.revisions {
		infos[i] = rev.toInfo()
	}
	return infos, m.pointer
}

// ListPaginated returns a page of revisions sorted newest-first (by id desc).
//
//   - limit clamps to [1, 50]; a non-positive value falls back to 20.
//   - before is exclusive: only revisions with id < before are returned.
//     Pass 0 (or any value >= the max id) for the first page.
//   - currentID is the id of the revision at the pointer, or 0 when empty.
//     Callers use this to render the "Current" badge correctly regardless
//     of which page the pointer happens to land in.
//   - hasMore is true when revisions older than the returned window still
//     exist. Use the last returned row's id as the next `before` cursor.
func (m *Manager) ListPaginated(limit, before int) (page []RevisionInfo, currentID int, hasMore bool) {
	m.mu.RLock()
	defer m.mu.RUnlock()

	if limit <= 0 {
		limit = 20
	} else if limit > 50 {
		limit = 50
	}

	if m.pointer >= 0 && m.pointer < len(m.revisions) {
		currentID = m.revisions[m.pointer].ID
	}

	// Walk revisions newest-first.
	for i := len(m.revisions) - 1; i >= 0; i-- {
		rev := &m.revisions[i]
		if before > 0 && rev.ID >= before {
			continue
		}
		if len(page) < limit {
			page = append(page, rev.toInfo())
			continue
		}
		// We already filled the page; one more qualifying row means has_more.
		hasMore = true
		break
	}
	return page, currentID, hasMore
}

// scopeRoot resolves a snapshot scope to its filesystem root.
func (m *Manager) scopeRoot(scope string) string {
	if scopeOrLegacy(scope) == ScopeSrc {
		return filepath.Join(m.workspacePath, "src")
	}
	return m.workspacePath
}

// skippableEntry reports whether a directory entry is excluded from snapshot
// and restore traversal: development dirs and anything dot-prefixed.
func skippableEntry(name string) bool {
	return excludedDirNames[name] || strings.HasPrefix(name, ".")
}

// restoreRevision writes all file snapshots from a revision (read from its
// on-disk .revisions/{id}/ mirror) into the workspace. Files that currently
// exist within the revision's scope but weren't part of the snapshot are
// removed — scoped so a legacy src-only revision never deletes root files.
func (m *Manager) restoreRevision(rev *Revision) error {
	// Build set of files in the revision
	revFiles := make(map[string]bool, len(rev.Files))
	for _, snap := range rev.Files {
		revFiles[snap.Path] = true
	}

	// Remove files that exist now but weren't in the revision
	currentFiles, _ := m.walkScopeFiles(rev.Scope)
	for _, path := range currentFiles {
		if !revFiles[path] {
			if err := os.Remove(filepath.Join(m.workspacePath, path)); err != nil && !os.IsNotExist(err) {
				return fmt.Errorf("failed to remove %s during restore: %w", path, err)
			}
		}
	}

	// Restore files from the revision's on-disk snapshot
	revRoot := m.revisionDir(rev.ID)
	for _, snap := range rev.Files {
		content, err := os.ReadFile(filepath.Join(revRoot, snap.Path))
		if err != nil {
			return fmt.Errorf("failed to read snapshot of %s: %w", snap.Path, err)
		}
		fullPath := filepath.Join(m.workspacePath, snap.Path)
		dir := filepath.Dir(fullPath)
		if err := os.MkdirAll(dir, 0755); err != nil {
			return fmt.Errorf("failed to create directory %s: %w", dir, err)
		}
		if err := os.WriteFile(fullPath, content, 0644); err != nil {
			return fmt.Errorf("failed to write %s: %w", snap.Path, err)
		}
	}

	// Clean up empty directories left by removals
	cleanEmptyDirs(m.scopeRoot(rev.Scope))

	return nil
}

// captureLiveFiles walks the given scope and captures all file contents.
func (m *Manager) captureLiveFiles(scope string) ([]liveFile, error) {
	root := m.scopeRoot(scope)
	var files []liveFile

	err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return nil // skip unreadable paths
		}
		if d.IsDir() {
			if path != root && skippableEntry(d.Name()) {
				return filepath.SkipDir
			}
			return nil
		}
		if skippableEntry(d.Name()) {
			return nil // hidden files (.webby-tsbuildinfo, .DS_Store, …)
		}

		relPath, err := filepath.Rel(m.workspacePath, path)
		if err != nil {
			return nil
		}

		content, err := os.ReadFile(path)
		if err != nil {
			return nil // skip unreadable files
		}

		files = append(files, liveFile{path: relPath, content: content})
		return nil
	})

	if err != nil && !os.IsNotExist(err) {
		return nil, err
	}

	return files, nil
}

// liveEqualsRevision reports whether the captured live files are
// byte-identical to a revision's persisted snapshot.
//
// Called under the write lock. It short-circuits on file-count or per-file
// size mismatch (the common case right after an agent run), so the full
// O(workspace-size) disk read of .revisions/{id}/ is only paid when the
// trees genuinely look identical — i.e. the duplicate case being detected.
func (m *Manager) liveEqualsRevision(files []liveFile, rev *Revision) bool {
	if len(files) != len(rev.Files) {
		return false
	}
	revRoot := m.revisionDir(rev.ID)
	sizes := make(map[string]int64, len(rev.Files))
	for _, snap := range rev.Files {
		sizes[snap.Path] = snap.Size
	}
	for _, f := range files {
		size, ok := sizes[f.path]
		if !ok || size != int64(len(f.content)) {
			return false
		}
		persisted, err := os.ReadFile(filepath.Join(revRoot, f.path))
		if err != nil || !bytes.Equal(persisted, f.content) {
			return false
		}
	}
	return true
}

// walkScopeFiles returns all file paths within a scope, relative to the
// workspace root, honoring the same exclusions as snapshots.
func (m *Manager) walkScopeFiles(scope string) ([]string, error) {
	root := m.scopeRoot(scope)
	var paths []string

	_ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return nil
		}
		if d.IsDir() {
			if path != root && skippableEntry(d.Name()) {
				return filepath.SkipDir
			}
			return nil
		}
		if skippableEntry(d.Name()) {
			return nil
		}
		relPath, err := filepath.Rel(m.workspacePath, path)
		if err != nil {
			return nil
		}
		paths = append(paths, relPath)
		return nil
	})

	return paths, nil
}

// cleanEmptyDirs removes empty directories under the given path, never
// descending into excluded/hidden directories.
func cleanEmptyDirs(root string) {
	_ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil || !d.IsDir() || path == root {
			return nil
		}
		if skippableEntry(d.Name()) {
			return filepath.SkipDir
		}
		entries, _ := os.ReadDir(path)
		if len(entries) == 0 {
			_ = os.Remove(path)
		}
		return nil
	})
}

// --- Persistence ---

// revisionsRoot is the absolute path to the workspace's .revisions dir.
func (m *Manager) revisionsRoot() string {
	return filepath.Join(m.workspacePath, revisionsDirName)
}

// revisionDir is the per-revision subdirectory that holds file snapshots.
func (m *Manager) revisionDir(id int) string {
	return filepath.Join(m.revisionsRoot(), strconv.Itoa(id))
}

// metadataPath is the path to metadata.json.
func (m *Manager) metadataPath() string {
	return filepath.Join(m.revisionsRoot(), metadataFileName)
}

// writeRevisionFiles mirrors captured file bytes to disk under
// .revisions/{id}/<relpath>.
func (m *Manager) writeRevisionFiles(id int, files []liveFile) error {
	root := m.revisionDir(id)
	if err := os.MkdirAll(root, 0755); err != nil {
		return fmt.Errorf("mkdir %s: %w", root, err)
	}
	for _, f := range files {
		dst := filepath.Join(root, f.path)
		if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
			return fmt.Errorf("mkdir %s: %w", filepath.Dir(dst), err)
		}
		if err := os.WriteFile(dst, f.content, 0644); err != nil {
			return fmt.Errorf("write %s: %w", dst, err)
		}
	}
	return nil
}

// removeRevisionDir deletes the on-disk snapshot for a revision id.
// Tolerant of missing dirs — used on truncate/evict paths.
func (m *Manager) removeRevisionDir(id int) {
	_ = os.RemoveAll(m.revisionDir(id))
}

// persistMetadata writes the current pointer + revision list to metadata.json.
// Must be called with m.mu held by the caller.
func (m *Manager) persistMetadata() error {
	if err := os.MkdirAll(m.revisionsRoot(), 0755); err != nil {
		return err
	}
	meta := persistedMetadata{
		Pointer:   m.pointer,
		NextID:    m.nextID,
		Revisions: make([]persistedEntry, 0, len(m.revisions)),
	}
	for _, rev := range m.revisions {
		meta.Revisions = append(meta.Revisions, persistedEntry{
			ID:        rev.ID,
			Label:     rev.Label,
			FileCount: rev.FileCount,
			Timestamp: rev.Timestamp,
			Scope:     rev.Scope,
		})
	}

	// Write atomically via tmp file + rename so a mid-write crash doesn't
	// leave an unparseable metadata.json on disk.
	tmp := m.metadataPath() + ".tmp"
	data, err := json.MarshalIndent(meta, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal metadata: %w", err)
	}
	if err := os.WriteFile(tmp, data, 0644); err != nil {
		return fmt.Errorf("write tmp metadata: %w", err)
	}
	if err := os.Rename(tmp, m.metadataPath()); err != nil {
		return fmt.Errorf("rename metadata: %w", err)
	}
	return nil
}

// LoadFromDisk reads previously persisted revisions + pointer from the
// workspace's .revisions/metadata.json and repopulates the manager. No-op
// (returns nil) if nothing is persisted. Safe on a fresh manager; typically
// called once during server boot or the first time a workspace is accessed
// after restart, before any other Manager operation.
//
// Orphan .revisions/{id}/ dirs (on disk but not in metadata.json) are
// ignored — they're leftover from a crash mid-snapshot and are deleted the
// next time the truncate/evict path runs on that id. Only file paths/sizes
// are indexed; content is read from disk on restore.
func (m *Manager) LoadFromDisk() error {
	m.mu.Lock()
	defer m.mu.Unlock()

	data, err := os.ReadFile(m.metadataPath())
	if err != nil {
		if os.IsNotExist(err) {
			return nil // nothing persisted yet — fresh workspace
		}
		return fmt.Errorf("read metadata: %w", err)
	}

	var meta persistedMetadata
	if err := json.Unmarshal(data, &meta); err != nil {
		return fmt.Errorf("parse metadata: %w", err)
	}

	revs := make([]Revision, 0, len(meta.Revisions))
	for _, entry := range meta.Revisions {
		files, err := m.indexRevisionFiles(entry.ID)
		if err != nil {
			// A single unreadable revision dir shouldn't poison the whole
			// load — drop the entry; the undo/redo pointer is clamped below.
			continue
		}
		revs = append(revs, Revision{
			ID:        entry.ID,
			Label:     entry.Label,
			Files:     files,
			FileCount: entry.FileCount,
			Timestamp: entry.Timestamp,
			Scope:     scopeOrLegacy(entry.Scope),
		})
	}

	m.revisions = revs
	m.pointer = meta.Pointer
	if m.pointer >= len(m.revisions) {
		m.pointer = len(m.revisions) - 1
	}
	if len(m.revisions) == 0 {
		m.pointer = -1
	}
	m.nextID = meta.NextID
	if m.nextID < 1 {
		m.nextID = 1
	}
	// Keep nextID ahead of any ID we saw, defensively.
	for _, rev := range m.revisions {
		if rev.ID >= m.nextID {
			m.nextID = rev.ID + 1
		}
	}
	return nil
}

// indexRevisionFiles walks .revisions/{id}/ and records the snapshot's file
// paths + sizes (no content — restore streams bytes from disk). Paths are
// stored relative to the workspace root, matching the snapshot convention.
func (m *Manager) indexRevisionFiles(id int) ([]FileSnapshot, error) {
	root := m.revisionDir(id)
	if _, err := os.Stat(root); err != nil {
		return nil, err
	}
	var out []FileSnapshot
	err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
		if err != nil || d.IsDir() {
			return nil
		}
		rel, relErr := filepath.Rel(root, path)
		if relErr != nil {
			return nil
		}
		info, infoErr := d.Info()
		if infoErr != nil {
			return nil
		}
		out = append(out, FileSnapshot{Path: rel, Size: info.Size()})
		return nil
	})
	if err != nil {
		return nil, err
	}
	return out, nil
}
