package restic

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os/exec"
	"path"
	"strings"
)

// LsEntry is one node from `restic ls --json`. Restic emits these as
// line-delimited JSON; we keep only the fields the restore wizard
// needs.
type LsEntry struct {
	Name   string `json:"name"`
	Type   string `json:"type"`
	Path   string `json:"path"`
	Size   int64  `json:"size,omitempty"`
	Struct string `json:"struct_type,omitempty"`
}

// ListTreeChildren runs `restic ls --json <snapshot> <dirPath>` and
// returns only the direct children of dirPath. Restic ls is recursive
// by default, so we filter post-hoc — for a typical interactive
// drill-down ("expand /etc/nginx") the subtree is small (a few KB of
// JSON); for huge subtrees this is suboptimal but correct.
//
// The first emitted line is restic's "snapshot" preamble (struct_type
// = "snapshot") which we discard. Subsequent lines are nodes; we
// match on path equal to dirPath + "/" + name (with normalisation so
// trailing slashes don't break the comparison).
//
// dirPath="" or "/" lists the snapshot root.
func (e Env) ListTreeChildren(ctx context.Context, snapshotID, dirPath string) ([]LsEntry, error) {
	if snapshotID == "" {
		return nil, fmt.Errorf("restic ls: snapshot id required")
	}
	parent := normalizeTreePath(dirPath)

	args := []string{"ls", "--json", snapshotID}
	if parent != "/" {
		args = append(args, parent)
	}
	cmd := e.resticCmd(ctx, args...)

	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, fmt.Errorf("restic ls: stdout pipe: %w", err)
	}
	if err := cmd.Start(); err != nil {
		return nil, fmt.Errorf("restic ls: start: %w", err)
	}

	out, parseErr := parseLsChildren(stdout, parent)

	werr := cmd.Wait()
	if werr != nil {
		var ee *exec.ExitError
		if errors.As(werr, &ee) {
			return nil, fmt.Errorf("restic ls: exit %d: %s",
				ee.ExitCode(), strings.TrimSpace(stderr.String()))
		}
		return nil, fmt.Errorf("restic ls: %w", werr)
	}
	if parseErr != nil {
		return nil, parseErr
	}
	return out, nil
}

// parseLsChildren reads line-delimited JSON from r and returns nodes
// whose Path is a direct child of parent. Exposed for testing.
func parseLsChildren(r io.Reader, parent string) ([]LsEntry, error) {
	scanner := bufio.NewScanner(r)
	scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	var out []LsEntry
	for scanner.Scan() {
		line := scanner.Bytes()
		if len(line) == 0 {
			continue
		}
		var entry LsEntry
		if err := json.Unmarshal(line, &entry); err != nil {
			return nil, fmt.Errorf("restic ls: parse line: %w", err)
		}
		// Skip the snapshot preamble and any future struct_type
		// entries we don't care about.
		if entry.Struct == "snapshot" || entry.Path == "" {
			continue
		}
		if isDirectChild(entry.Path, parent) {
			out = append(out, entry)
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("restic ls: read output: %w", err)
	}
	return out, nil
}

// normalizeTreePath turns "" / "/" / "/etc/" / "etc" all into a
// canonical absolute form with a leading slash and no trailing slash
// (except the root, which is "/" alone).
func normalizeTreePath(p string) string {
	p = strings.TrimSpace(p)
	if p == "" || p == "/" {
		return "/"
	}
	if !strings.HasPrefix(p, "/") {
		p = "/" + p
	}
	cleaned := path.Clean(p)
	return cleaned
}

// isDirectChild reports whether childPath is a direct child of parent.
// "/etc/nginx" is a direct child of "/etc"; "/etc/nginx/conf" is not.
// "/etc" is a direct child of "/".
func isDirectChild(childPath, parent string) bool {
	cp := normalizeTreePath(childPath)
	pp := normalizeTreePath(parent)
	if pp == "/" {
		// Direct children of root: exactly one slash-delimited segment.
		return cp != "/" && strings.Count(cp, "/") == 1
	}
	// Must start with parent + "/" and have no further slashes.
	prefix := pp + "/"
	if !strings.HasPrefix(cp, prefix) {
		return false
	}
	rest := cp[len(prefix):]
	return rest != "" && !strings.Contains(rest, "/")
}
