// Copyright (C) 2022 Marius Schellenberger

package srv

import (
	"errors"
	"io"
	stdfs "io/fs"
	"math"
	"net/http"
	"path"
	"strings"

	"cachefs/pkg/fs"

	"github.com/go-logr/logr"
)

const (
	csp      = "Content-Security-Policy"
	indexCSP = "default-src 'none';style-src 'unsafe-inline';frame-ancestors 'none'"
	videoCSP = indexCSP + ";script-src 'sha256-ZLSc/s5/US9uVMMZOJ/yWiS1tj9nEoi+5Qoohy3QetU=';media-src 'self'"

	gib = 1024 * 1024 * 1024
)

type data struct {
	QuotaCur float64
	QuotaMax float64
	Paths    dirContents
}

type FileServer struct {
	log logr.Logger
	fs  *fs.FS
	h   http.Handler
}

func NewFileServer(fs *fs.FS, log logr.Logger) http.Handler {
	return &FileServer{log, fs, http.FileServer(fs)}
}

func skipLog(path string) bool {
	return path == "/?o=preloads" ||
		strings.HasSuffix(path, "favicon.ico")
}

func (fs *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s := &statusInterceptor{w: w}
	w = s
	if !skipLog(r.URL.Path) {
		defer func() {
			if s.Status() == http.StatusPartialContent {
				return
			}
			fs.log.Info("access",
				"client", r.RemoteAddr,
				"method", r.Method,
				"status", s.Status(),
				"uri", r.URL.Path,
			)
		}()
	}
	upath := r.URL.Path
	if !strings.HasPrefix(upath, "/") {
		upath = "/" + upath
		r.URL.Path = upath
	}
	p := path.Clean(upath)
	option := r.FormValue("o")
	d, err := fs.fs.Stat(p)
	if err != nil {
		msg, code := toHTTPError(err)
		http.Error(w, msg, code)
		return
	}
	h := w.Header()
	if option == "v" {
		h.Set(csp, videoCSP)
		err = video.Execute(w, data{Paths: dirContents{Base: r.URL.Path}})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}

	if !d.IsDir() {
		if option == "p" || option == "i" || option == "d" || option == "s" {
			switch option {
			case "p":
				fs.fs.Preload(p, 0)
			case "i":
				fs.fs.Preload(p, 1)
			case "d":
				fs.fs.Preload(p, -1)
			case "s":
				fs.fs.RemovePreload(p)
			}
			rdir := "/"
			if r.FormValue("r") == "preloads" {
				http.Redirect(w, r, rdir+"?o=preloads", http.StatusFound)
				return
			}
			if i := strings.LastIndex(p, "/"); i > 0 {
				rdir = p[:i]
			}
			http.Redirect(w, r, rdir+"#"+anchor(p), http.StatusFound)
			return
		}
		if option == "n" {
			fs.fs.NoCache.ServeHTTP(w, r)
			return
		}
		fs.h.ServeHTTP(w, r)
		return
	}
	if option == "preloads" {
		h.Set(csp, indexCSP)
		cur, max := fs.fs.QuotaUsage()
		err = preloads.Execute(w, data{
			QuotaCur: math.Round(float64(cur)/gib*100) / 100,
			QuotaMax: float64(max) / gib,
			Paths:    getPreloads(p, fs.fs),
		})
		if err != nil {
			fs.log.Error(err, "error rendering preloads")
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}

	i := &responseInterceptor{w: w}
	r.Header.Del("If-Modified-Since")
	r.Header.Del("Cache-Control")
	fs.h.ServeHTTP(i, r)

	h.Del("Last-Modified")

	paths, err := i.GetPaths(p, fs.fs, false)
	if err == io.EOF {
		w.WriteHeader(i.Status())
		return
	}
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	h.Set(csp, indexCSP)

	w.WriteHeader(i.Status())
	err = index.Execute(w, data{Paths: paths})
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func toHTTPError(err error) (string, int) {
	if errors.Is(err, stdfs.ErrNotExist) {
		return "404 page not found", http.StatusNotFound
	}
	if errors.Is(err, stdfs.ErrPermission) {
		return "403 Forbidden", http.StatusForbidden
	}
	return "500 Internal Server Error", http.StatusInternalServerError
}
