package main

import (
	"fmt"
	"io/fs"
	"net/http"
	"os"
	"path"
	"sort"
	"strings"
)

// server holds one opened os.Root per configured tree.
// os.Root guarantees all path resolution stays inside the tree
// (it rejects ".." escapes and symlinks pointing outside).
type server struct {
	trees map[string]*os.Root
}

func newServer(roots map[string]string) (*server, error) {
	s := &server{trees: make(map[string]*os.Root)}
	for name, dir := range roots {
		root, err := os.OpenRoot(dir)
		if err != nil {
			return nil, fmt.Errorf("opening root %q: %w", name, err)
		}
		s.trees[name] = root
	}
	return s, nil
}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/" {
		s.serveIndex(w, r)
		return
	}

	name, rest, _ := strings.Cut(strings.TrimPrefix(r.URL.Path, "/"), "/")
	root, ok := s.trees[name]
	if !ok {
		http.NotFound(w, r)
		return
	}
	rel := path.Clean("/" + rest)[1:] // normalize; "" becomes "."
	if rel == "" {
		rel = "."
	}

	info, err := root.Stat(rel)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	switch {
	case info.IsDir():
		if !strings.HasSuffix(r.URL.Path, "/") {
			http.Redirect(w, r, r.URL.Path+"/", http.StatusMovedPermanently)
			return
		}
		s.serveDir(w, r, root, name, rel)
	case r.URL.Query().Get("fmt") == "raw":
		serveRaw(w, r, root, rel)
	default:
		serveRendered(w, r, root, name, rel, info.Size())
	}
}

// serveRaw sends the file bytes.
// Markdown gets text/plain so browsers display it instead of downloading;
// everything else gets stdlib content-type detection via ServeContent.
func serveRaw(w http.ResponseWriter, r *http.Request, root *os.Root, rel string) {
	f, err := root.Open(rel)
	if err != nil {
		http.NotFound(w, r)
		return
	}
	defer f.Close()
	info, err := f.Stat()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	if strings.HasSuffix(rel, ".md") {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	}
	http.ServeContent(w, r, info.Name(), info.ModTime(), f)
}

// serveIndex lists the configured roots.
func (s *server) serveIndex(w http.ResponseWriter, r *http.Request) {
	names := make([]string, 0, len(s.trees))
	for name := range s.trees {
		names = append(names, name)
	}
	sort.Strings(names)
	entries := make([]dirEntry, len(names))
	for i, name := range names {
		entries[i] = dirEntry{Name: name + "/", Href: "/" + name + "/"}
	}
	renderPage(w, pageData{Title: "repoview", Crumbs: crumbs("", ""), Listing: entries})
}

// serveDir renders a directory listing.
// The .git directory is omitted: it's not useful to browse and clutters the view.
func (s *server) serveDir(w http.ResponseWriter, r *http.Request, root *os.Root, name, rel string) {
	ents, err := fs.ReadDir(root.FS(), rel)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	var listing []dirEntry
	if rel != "." {
		listing = append(listing, dirEntry{Name: "../", Href: "../"})
	}
	for _, e := range ents {
		if e.Name() == ".git" {
			continue
		}
		n := e.Name()
		if e.IsDir() {
			n += "/"
		}
		listing = append(listing, dirEntry{Name: n, Href: n})
	}
	renderPage(w, pageData{
		Title:   name + "/" + strings.TrimSuffix(rel, "."),
		Crumbs:  crumbs(name, rel),
		Listing: listing,
	})
}
