// Command bj-artifactory — простой сервер раздачи релизов и обновлений.
//
// Раскладка хранилища (--root), один подкаталог на канал:
//
//	<root>/stable/manifest.json     — подписанный SignedManifest
//	<root>/stable/bj-server         — артефакты, перечисленные в манифесте
//	<root>/stable/crypto-service.jar
//	<root>/beta/manifest.json
//	...
//
// HTTP API (потребляет bj-server auto-update и install.sh):
//
//	GET /v1/<channel>/manifest.json      — манифест канала
//	GET /v1/<channel>/files/<name>       — артефакт по имени
//	GET /healthz                         — проверка живости
//
// Подпись манифеста делает bj-release; здесь только статическая раздача.
// Перед прод-выкаткой ставится за TLS-reverse-proxy (nginx, см.
// deploy/artifactory/nginx.conf).
package main

import (
	"flag"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

func main() {
	addr := flag.String("addr", ":8090", "адрес прослушивания")
	root := flag.String("root", "./releases", "корень хранилища релизов")
	flag.Parse()

	abs, err := filepath.Abs(*root)
	if err != nil {
		log.Fatalf("bj-artifactory: root: %v", err)
	}
	if _, err := os.Stat(abs); err != nil {
		log.Fatalf("bj-artifactory: каталог релизов %s недоступен: %v", abs, err)
	}
	srv := &server{root: abs}

	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })
	mux.HandleFunc("/v1/", srv.handleV1)

	log.Printf("bj-artifactory: раздаю %s на %s", abs, *addr)
	httpSrv := &http.Server{Addr: *addr, Handler: logging(mux), ReadHeaderTimeout: 10 * time.Second}
	log.Fatal(httpSrv.ListenAndServe())
}

type server struct{ root string }

// handleV1 разбирает /v1/<channel>/manifest.json и /v1/<channel>/files/<name>.
func (s *server) handleV1(w http.ResponseWriter, r *http.Request) {
	rest := strings.TrimPrefix(r.URL.Path, "/v1/")
	parts := strings.SplitN(rest, "/", 3)
	if len(parts) < 2 {
		http.NotFound(w, r)
		return
	}
	channel := parts[0]
	if !safeName(channel) {
		http.Error(w, "bad channel", http.StatusBadRequest)
		return
	}
	switch {
	case len(parts) == 2 && parts[1] == "manifest.json":
		s.serveFile(w, r, filepath.Join(s.root, channel, "manifest.json"), "application/json")
	case len(parts) == 3 && parts[1] == "files":
		name := parts[2]
		if !safeName(name) {
			http.Error(w, "bad name", http.StatusBadRequest)
			return
		}
		s.serveFile(w, r, filepath.Join(s.root, channel, name), "application/octet-stream")
	default:
		http.NotFound(w, r)
	}
}

func (s *server) serveFile(w http.ResponseWriter, r *http.Request, path, ctype string) {
	f, err := os.Open(path)
	if err != nil {
		http.NotFound(w, r)
		return
	}
	defer f.Close()
	fi, err := f.Stat()
	if err != nil || fi.IsDir() {
		http.NotFound(w, r)
		return
	}
	w.Header().Set("Content-Type", ctype)
	http.ServeContent(w, r, filepath.Base(path), fi.ModTime(), f)
}

// safeName запрещает обход каталогов (.., /, пустые).
func safeName(s string) bool {
	if s == "" || s == "." || s == ".." {
		return false
	}
	return !strings.ContainsAny(s, "/\\") && !strings.Contains(s, "..")
}

func logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		next.ServeHTTP(w, r)
		log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path)
	})
}
