package restic

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"os/exec"
	"time"

	"git.secluded.site/keld/internal/config"
)

// LsNode represents a single file or directory entry from
// `restic ls --json`. Each JSON line with message_type "node"
// produces one LsNode.
type LsNode struct {
	Name        string    `json:"name"`
	Type        string    `json:"type"` // "dir", "file", etc.
	Path        string    `json:"path"`
	Size        int64     `json:"size,omitempty"` // absent for directories
	Mode        int       `json:"mode"`
	Permissions string    `json:"permissions"`
	Mtime       time.Time `json:"mtime"`
}

// lsNodeEnvelope wraps LsNode with the message_type discriminator so
// we can unmarshal each JSON line in a single pass — check the type and
// extract node fields at once, avoiding a double decode per line.
type lsNodeEnvelope struct {
	MessageType string `json:"message_type"`
	LsNode
}

// ParseLsNodes parses the JSON-lines output of `restic ls --json`,
// skipping the snapshot header line (message_type "snapshot") and
// returning all node entries in their original order.
//
// This is a convenience wrapper around parseLsReader for callers that
// already have the complete output in memory (e.g. tests).
func ParseLsNodes(data []byte) ([]LsNode, error) {
	return parseLsReader(bytes.NewReader(data))
}

// parseLsReader scans JSON lines from r, skipping non-node lines
// (snapshot headers, blank lines), and returns all node entries in
// order. This is the core parser used by both ParseLsNodes and RunLs.
func parseLsReader(r io.Reader) ([]LsNode, error) {
	var nodes []LsNode
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := scanner.Bytes()
		if len(bytes.TrimSpace(line)) == 0 {
			continue
		}

		var envelope lsNodeEnvelope
		if err := json.Unmarshal(line, &envelope); err != nil {
			return nil, fmt.Errorf("parsing ls JSON line: %w", err)
		}
		if envelope.MessageType != "node" {
			continue
		}

		nodes = append(nodes, envelope.LsNode)
	}
	if err := scanner.Err(); err != nil {
		return nil, fmt.Errorf("scanning ls output: %w", err)
	}

	return nodes, nil
}

// RunLs runs `restic ls --json <snapshotID>` using connection details
// from the given resolved config and returns the parsed nodes.
//
// Stdout is stream-parsed rather than buffered entirely into memory,
// so large snapshots don't require holding the full JSON output at
// once. Stderr is captured for error reporting.
//
// The config's environ map is copied before resolving _COMMAND entries
// so the caller's config is not mutated.
func RunLs(cfg *config.ResolvedConfig, snapshotID string) ([]LsNode, error) {
	argv, err := buildLsCmd(cfg, snapshotID)
	if err != nil {
		return nil, err
	}

	env := copyEnviron(cfg.Environ)
	if err := resolveEnvironCommands(env, cfg.Workdir); err != nil {
		return nil, fmt.Errorf("resolving environ for ls: %w", err)
	}

	cmd := exec.Command(argv[0], argv[1:]...) //nolint:gosec
	cmd.Env = buildEnv(env)

	if cfg.Workdir != "" {
		cmd.Dir = cfg.Workdir
	}

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, fmt.Errorf("creating stdout pipe for restic ls: %w", err)
	}

	var stderr bytes.Buffer
	cmd.Stderr = &stderr

	if err := cmd.Start(); err != nil {
		return nil, fmt.Errorf("starting restic ls: %w", err)
	}

	nodes, parseErr := parseLsReader(stdout)

	// Always wait for the process to finish, even if parsing failed.
	waitErr := cmd.Wait()

	if parseErr != nil {
		return nil, fmt.Errorf("parsing restic ls output: %w", parseErr)
	}
	if waitErr != nil {
		return nil, fmt.Errorf("running restic ls: %w\n%s",
			waitErr, bytes.TrimSpace(stderr.Bytes()))
	}

	return nodes, nil
}

// snapshotSelectors are flags that affect which snapshot the special ID
// "latest" resolves to. They are accepted by both `restic ls` and
// `restic restore`, so we forward them from the resolved restore config
// to ensure the picker browses the same snapshot the restore will use.
//
// Sourced from `restic ls --help`.
var snapshotSelectors = map[string]bool{
	"--host": true,
	"-H":     true,
	"--path": true,
	"--tag":  true,
}

// buildLsCmd constructs the argument vector for running
// `restic ls --json <snapshotID>`, extracting global flags and
// snapshot-selection flags from the given resolved config. The snapshot
// ID is always the last argument. Returns an error if no repository
// source is available.
func buildLsCmd(cfg *config.ResolvedConfig, snapshotID string) ([]string, error) {
	if !hasRepoSource(cfg) {
		return nil, ErrNoRepo
	}

	argv := []string{executable(), "ls", "--json"}

	for _, f := range cfg.Flags {
		if !globalFlags[f.Name] && !snapshotSelectors[f.Name] {
			continue
		}
		argv = append(argv, f.Name)
		if f.Value != "" {
			argv = append(argv, f.Value)
		}
	}

	argv = append(argv, snapshotID)

	return argv, nil
}
