package http

import (
	"fmt"
	stdhttp "net/http"
	"os"
	"path/filepath"
	"strings"

	"gitea.dcglab.co.uk/steve/restic-manager/internal/api"
)

// agent_assets.go serves the agent binary (one per OS/arch) and the
// install scripts. Lookup is dual-path:
//
//  1. <DataDir>/agent-binaries/<name>  (or <DataDir>/install/<name>) —
//     operator-managed override; lets the operator hot-patch a
//     pre-release agent without rebuilding the server image.
//  2. <BundledAssetsDir>/agent-binaries/<name> — read-only, baked
//     into the server image at build time (P5-03). This is what
//     makes a fresh container Just Work without first-run staging.
//
// Both endpoints are intentionally unauthenticated: the install
// payload is unprivileged on its own — it's the one-time enrollment
// token that grants access. Anyone can pull the binary; only
// someone with a valid token can use it productively.
//
// P1-31: signed-binary verification is deferred. The image is the
// unit of trust; pull-by-digest is the verification primitive.
// Future work bumps standalone-binary delivery to minisign/cosign.

// installAssetsRoutes adds /agent/binary and /install/* to r.
func (s *Server) handleAgentBinary(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	osTag := r.URL.Query().Get("os")
	archTag := r.URL.Query().Get("arch")
	if osTag == "" || archTag == "" {
		writeJSONError(w, stdhttp.StatusBadRequest, "missing_os_or_arch",
			"query params os= and arch= are required")
		return
	}
	if !validOS(osTag) || !validArch(archTag) {
		writeJSONError(w, stdhttp.StatusBadRequest, "unsupported_target",
			fmt.Sprintf("os=%q arch=%q not in {linux,windows} × {amd64,arm64}", osTag, archTag))
		return
	}

	ext := ""
	if osTag == "windows" {
		ext = ".exe"
	}
	name := fmt.Sprintf("restic-manager-agent-%s-%s%s", osTag, archTag, ext)
	path, ok := s.resolveBundledAsset("agent-binaries", name)
	if !ok {
		writeJSONError(w, stdhttp.StatusNotFound, "binary_not_published",
			fmt.Sprintf("agent binary for %s/%s not published on this server", osTag, archTag))
		return
	}
	w.Header().Set("Content-Type", "application/octet-stream")
	w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
	stdhttp.ServeFile(w, r, path)
}

func (s *Server) handleInstallAsset(w stdhttp.ResponseWriter, r *stdhttp.Request) {
	// chi's TrimPrefix-like behaviour: r.URL.Path is "/install/<file>".
	rel := strings.TrimPrefix(r.URL.Path, "/install/")
	// Reject any path traversal — must be a flat filename.
	if rel == "" || strings.ContainsAny(rel, "/\\") {
		writeJSONError(w, stdhttp.StatusBadRequest, "bad_path", "")
		return
	}
	path, ok := s.resolveBundledAsset("install", rel)
	if !ok {
		writeJSONError(w, stdhttp.StatusNotFound, "not_found", "")
		return
	}
	stdhttp.ServeFile(w, r, path)
}

// resolveBundledAsset looks up an asset by (subdir, name). DataDir
// wins so an operator can override the image-baked copy by dropping
// a file into <DataDir>/<subdir>/<name>. If neither path resolves,
// returns ("", false).
func (s *Server) resolveBundledAsset(subdir, name string) (string, bool) {
	candidates := []string{
		filepath.Join(s.deps.Cfg.DataDir, subdir, name),
	}
	if s.deps.Cfg.BundledAssetsDir != "" {
		candidates = append(candidates,
			filepath.Join(s.deps.Cfg.BundledAssetsDir, subdir, name))
	}
	for _, p := range candidates {
		if _, err := os.Stat(p); err == nil {
			return p, true
		}
	}
	return "", false
}

func validOS(s string) bool {
	switch api.HostOS(s) {
	case api.OSLinux, api.OSWindows:
		return true
	}
	return false
}

func validArch(s string) bool {
	switch api.HostArch(s) {
	case api.ArchAmd64, api.ArchArm64:
		return true
	}
	return false
}
