package main

import (
	"encoding/base64"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"log"
	"net/http"
	"net/url"
	"os"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"

	"git.tordarus.net/Tordarus/dockerhealth"
)

var (
	directory = flag.String("d", ".", "directory")
	intf      = flag.String("i", "", "interface")
	port      = flag.Uint("p", 80, "port")
	noauth    = flag.Bool("a", false, "dont use basic auth")
)

var (
	fileServer http.Handler
)

func main() {
	flag.Parse()

	fileServer = http.FileServer(http.Dir(*directory))
	http.HandleFunc("/", handler)

	dockerhealth.Healthy = true
	err := http.ListenAndServe(fmt.Sprintf("%s:%d", *intf, *port), nil)
	if err != nil {
		panic(err)
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	log.Println(r.Method, getPath(r.URL.Path, r))
	switch r.Method {
	case "GET":
		get(w, r)
	case "PUT":
		put(w, r) // TODO accept Range header for PUT requests
	case "HEAD":
		head(w, r)
	case "DELETE":
		delete(w, r)
	case "COPY":
		copy(w, r)
	case "LINK":
		link(w, r)
	}
}

func get(w http.ResponseWriter, r *http.Request) {
	path := getPath(r.URL.Path, r)

	fi, err := os.Stat(path)
	if err != nil {
		handleError(w, r, err)
		return
	}

	w.Header().Add("Content-Type", getMimetype(path))
	w.Header().Add("File-Permissions", fi.Mode().Perm().String())
	w.Header().Add("Modified-Time", fi.ModTime().String())

	username, groupname, err := getOwnerNames(getOwnerIDs(fi))
	if err == nil {
		w.Header().Add("File-Owner", username)
		w.Header().Add("Group-Owner", groupname)
	}

	if fi.IsDir() {
		files, err := os.ReadDir(path)
		if err != nil {
			handleError(w, r, err)
			return
		}

		for _, file := range files {
			fmt.Fprintln(w, url.QueryEscape(file.Name()))
		}
		return
	}

	fileServer.ServeHTTP(w, r)
}

func put(w http.ResponseWriter, r *http.Request) {
	path := getPath(r.URL.Path, r)

	if r.Header.Get("Content-Type") == "inode/directory" {
		err := os.MkdirAll(path, os.ModePerm)
		if err != nil {
			handleError(w, r, err)
		}
		return
	}

	err := os.MkdirAll(filepath.Dir(path), os.ModePerm)
	if err != nil {
		handleError(w, r, err)
		return
	}

	file, err := os.Create(path)
	if err != nil {
		handleError(w, r, err)
		return
	}
	defer file.Close()

	io.Copy(file, r.Body)
}

func head(w http.ResponseWriter, r *http.Request) {
	path := getPath(r.URL.Path, r)

	fi, err := os.Stat(path)
	if err != nil {
		handleError(w, r, err)
		return
	}

	w.Header().Add("Content-Type", getMimetype(path))
	w.Header().Add("File-Permissions", fi.Mode().Perm().String())
	w.Header().Add("Modified-Time", fi.ModTime().String())

	username, groupname, err := getOwnerNames(getOwnerIDs(fi))
	if err == nil {
		w.Header().Add("File-Owner", username)
		w.Header().Add("Group-Owner", groupname)
	}

	fileServer.ServeHTTP(w, r)
}

func delete(w http.ResponseWriter, r *http.Request) {
	path := getPath(r.URL.Path, r)
	err := os.RemoveAll(path)
	if err != nil {
		handleError(w, r, err)
		return
	}
}

func copy(w http.ResponseWriter, r *http.Request) {
	destinations, ok := r.Header["Destination"]

	if !ok {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintln(w, "missing Destination header")
	}

	current_path := getPath(r.URL.Path, r)

	destination_paths := make([]string, 0, len(destinations))
	for _, destination := range destinations {
		destination_paths = append(destination_paths, getPath(destination, r))
	}

	current_file, err := os.Open(current_path)
	if err != nil {
		handleError(w, r, err)
		return
	}
	defer current_file.Close()

	destination_files := make([]io.Writer, 0, len(destination_paths))
	for _, destination_path := range destination_paths {
		destination_file, err := os.Create(destination_path)
		if err != nil {
			handleError(w, r, err)
			return
		}
		defer destination_file.Close()

		destination_files = append(destination_files, destination_file)
	}

	io.Copy(io.MultiWriter(destination_files...), current_file)
}

func link(w http.ResponseWriter, r *http.Request) {
	_, ok := r.Header["Link"]

	if !ok {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintln(w, "missing Destination header")
	}

	current_path := getPath(r.URL.Path, r)
	destination_path := getPath(r.Header.Get("Link"), r)

	err := os.Symlink(current_path, destination_path)
	if err != nil {
		handleError(w, r, err)
		return
	}
}

func handleError(w http.ResponseWriter, r *http.Request, err error) {
	if errors.Is(err, os.ErrNotExist) {
		w.WriteHeader(http.StatusNotFound)
	} else if errors.Is(err, errInvalidRange) {
		w.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
	} else {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintln(w, err)
	}
}

func getOwnerIDs(fi fs.FileInfo) (uid, gid string) {
	if stat, ok := fi.Sys().(*syscall.Stat_t); ok {
		return strconv.Itoa(int(stat.Uid)), strconv.Itoa(int(stat.Gid))
	}

	return "", ""
}

func getOwnerNames(uid, gid string) (username, groupname string, err error) {
	usr, err := user.LookupId(uid)
	if err != nil {
		return "", "", err
	}

	grp, err := user.LookupGroupId(gid)
	if err != nil {
		return "", "", err
	}

	return usr.Username, grp.Name, nil
}

func getPath(dirty_path string, r *http.Request) string {
	if *noauth {
		return filepath.Join(*directory, filepath.Clean(dirty_path))
	}

	username, _, _ := basicAuth(r.Header.Get("Authorization"))
	return filepath.Join(*directory, filepath.Clean(username), filepath.Clean(dirty_path))
}

func basicAuth(authHeader string) (username, password string, err error) {
	data, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authHeader, "Basic "))
	if err != nil {
		return "", "", err
	}

	splits := strings.SplitN(string(data), ":", 2)
	if len(splits) < 2 {
		return "", "", errors.New("invalid auth header")
	}
	return splits[0], splits[1], nil
}
