package restic

import (
	"bytes"
	"errors"
	"fmt"
	"maps"
	"os/exec"
	"sort"

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

// ErrNoRepo is returned by ListSnapshots when no repository is configured
// via flags, config environ, or process environment.
var ErrNoRepo = errors.New("no repository configured (need --repo, --repository-file, or RESTIC_REPOSITORY)")

// globalFlags is the set of restic global flags that should be forwarded
// when running `restic snapshots` to list available snapshots. These are
// the connection, auth, cache, and TLS flags shared across all restic
// commands — not command-specific flags like --target or --exclude.
//
// Sourced from `restic snapshots --help` "Global Flags" section.
var globalFlags = map[string]bool{
	"--cacert":                true,
	"--cache-dir":             true,
	"--cleanup-cache":         true,
	"--compression":           true,
	"--http-user-agent":       true,
	"--insecure-no-password":  true,
	"--insecure-tls":          true,
	"--key-hint":              true,
	"--limit-download":        true,
	"--limit-upload":          true,
	"--no-cache":              true,
	"--no-extra-verify":       true,
	"--no-lock":               true,
	"-o":                      true,
	"--pack-size":             true,
	"--password-command":      true,
	"--password-file":         true,
	"-p":                      true,
	"--repo":                  true,
	"-r":                      true,
	"--repository-file":       true,
	"--retry-lock":            true,
	"--stuck-request-timeout": true,
	"--tls-client-cert":       true,
}

// hasRepoSource reports whether a repository location is available from
// any source: CLI flags in the resolved config, the config's environ
// section (including _COMMAND variants), or the process environment.
func hasRepoSource(cfg *config.ResolvedConfig) bool {
	for _, f := range cfg.Flags {
		switch f.Name {
		case "--repo", "-r", "--repository-file":
			return true
		}
	}
	return cfg.HasEnvSource("RESTIC_REPOSITORY") ||
		cfg.HasEnvSource("RESTIC_REPOSITORY_FILE")
}

// buildSnapshotCmd constructs the argument vector for running
// `restic snapshots --json`, extracting only global flags from the
// given resolved config. Returns an error if no repository source
// is available.
func buildSnapshotCmd(cfg *config.ResolvedConfig) ([]string, error) {
	if !hasRepoSource(cfg) {
		return nil, ErrNoRepo
	}

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

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

	return argv, nil
}

// copyEnviron returns a shallow copy of the environ map so that
// resolveEnvironCommands can mutate it without affecting the caller's
// config.
func copyEnviron(environ map[string]string) map[string]string {
	if environ == nil {
		return nil
	}
	copied := make(map[string]string, len(environ))
	maps.Copy(copied, environ)
	return copied
}

// ListSnapshots runs `restic snapshots --json` using the connection
// details from the given resolved config and returns the parsed
// snapshots sorted newest-first.
//
// The config's environ map is copied before resolving _COMMAND entries
// so the caller's config is not mutated.
func ListSnapshots(cfg *config.ResolvedConfig) ([]Snapshot, error) {
	argv, err := buildSnapshotCmd(cfg)
	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 snapshot listing: %w", err)
	}

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

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

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	if err := cmd.Run(); err != nil {
		return nil, fmt.Errorf("running restic snapshots: %w\n%s",
			err, bytes.TrimSpace(stderr.Bytes()))
	}

	snapshots, err := ParseSnapshots(stdout.Bytes())
	if err != nil {
		return nil, err
	}

	// Sort newest-first for display. Don't assume restic's JSON order.
	sort.Slice(snapshots, func(i, j int) bool {
		return snapshots[i].Time.After(snapshots[j].Time)
	})

	return snapshots, nil
}
