//go:generate env GO111MODULE=on go run github.com/kevinburke/go-bindata/go-bindata -o static_gen.go -ignore 'map|go' -tags assets -pkg static data/...

package static

import (
	"fmt"
	"io"
	"io/fs"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	assetfs "github.com/elazarl/go-bindata-assetfs"
	platform "github.com/influxdata/influxdb/v2"
)

const (
	// defaultFile is the default UI asset file that will be served if no other
	// static asset matches. This is particularly useful for serving content
	// related to a SPA with client-side routing.
	defaultFile = "index.html"

	// embedBaseDir is the prefix for files in the bundle with the binary.
	embedBaseDir = "data"

	// uiBaseDir is the directory in embedBaseDir where the built UI assets
	// reside.
	uiBaseDir = "build"

	// swaggerFile is the name of the swagger JSON.
	swaggerFile = "swagger.json"

	// fallbackPathSlug is the path to re-write on the request if the requested
	// path does not match a file and the default file is served. For telemetry
	// and metrics reporting purposes.
	fallbackPathSlug = "/:fallback_path"
)

// NewAssetHandler returns an http.Handler to serve files from the provided
// path. If no --assets-path flag is used when starting influxd, the path will
// be empty and files are served from the embedded filesystem.
func NewAssetHandler(assetsPath string) http.Handler {
	var fileOpener http.FileSystem

	if assetsPath == "" {
		fileOpener = &assetfs.AssetFS{
			Asset:     Asset,
			AssetDir:  AssetDir,
			AssetInfo: AssetInfo,
			Prefix:    filepath.Join(embedBaseDir, uiBaseDir),
		}
	} else {
		fileOpener = http.FS(os.DirFS(assetsPath))
	}

	return mwSetCacheControl(assetHandler(fileOpener))
}

// NewSwaggerHandler returns an http.Handler to serve the swaggerFile from the
// embedBaseDir. If the swaggerFile is not found, returns a 404.
func NewSwaggerHandler() http.Handler {
	fileOpener := &assetfs.AssetFS{
		Asset:     Asset,
		AssetDir:  AssetDir,
		AssetInfo: AssetInfo,
		Prefix:    embedBaseDir,
	}

	return mwSetCacheControl(swaggerHandler(fileOpener))
}

// mwSetCacheControl sets a default cache control header.
func mwSetCacheControl(next http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Add("Cache-Control", "public, max-age=3600")
		next.ServeHTTP(w, r)
	}
	return http.HandlerFunc(fn)
}

// swaggerHandler returns a handler that serves the swaggerFile or returns a 404
// if the swaggerFile is not present.
func swaggerHandler(fileOpener http.FileSystem) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		f, err := fileOpener.Open(swaggerFile)
		if err != nil {
			http.Error(w, err.Error(), http.StatusNotFound)
			return
		}
		defer f.Close()

		staticFileHandler(f).ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

// assetHandler returns a handler that either serves the file at that path, or
// the default file if a file cannot be found at that path. If the default file
// is served, the request path is re-written to the root path to simplify
// metrics reporting.
func assetHandler(fileOpener http.FileSystem) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		name := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
		// If the root directory is being requested, respond with the default file.
		if name == "" {
			name = defaultFile
			r.URL.Path = "/" + defaultFile
		}

		// Try to open the file requested by name, falling back to the default file.
		// If even the default file can't be found, the binary must not have been
		// built with assets, so respond with not found.
		f, fallback, err := openAsset(fileOpener, name)
		if err != nil {
			http.Error(w, err.Error(), http.StatusNotFound)
			return
		}
		defer f.Close()

		// If the default file will be served because the requested path didn't
		// match any existing files, re-write the request path a placeholder value.
		// This is to ensure that metrics do not get collected for an arbitrarily
		// large range of incorrect paths.
		if fallback {
			r.URL.Path = fallbackPathSlug
		}

		staticFileHandler(f).ServeHTTP(w, r)
	}

	return http.HandlerFunc(fn)
}

// staticFileHandler sets the ETag header prior to calling http.ServeContent
// with the contents of the file.
func staticFileHandler(f fs.File) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		content, ok := f.(io.ReadSeeker)
		if !ok {
			err := fmt.Errorf("could not open file for reading")
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		i, err := f.Stat()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		modTime, err := modTimeFromInfo(i, buildTime)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.Header().Set("ETag", etag(i.Size(), modTime))

		// ServeContent will automatically set the content-type header for files
		// from the extension of "name", and will also set the Last-Modified header
		// from the provided time.
		http.ServeContent(w, r, i.Name(), modTime, content)
	}

	return http.HandlerFunc(fn)
}

// openAsset attempts to open the asset by name in the given directory, falling
// back to the default file if the named asset can't be found. Returns an error
// if even the default asset can't be opened.
func openAsset(fileOpener http.FileSystem, name string) (fs.File, bool, error) {
	var fallback bool

	f, err := fileOpener.Open(name)
	if err != nil {
		if os.IsNotExist(err) {
			fallback = true
			f, err = fileOpener.Open(defaultFile)
		}
		if err != nil {
			return nil, fallback, err
		}
	}

	return f, fallback, nil
}

// modTimeFromInfo gets the modification time from an fs.FileInfo. If this
// modification time is time.Time{}, it falls back to the time returned by
// timeFunc. The modification time will only be time.Time{} if using assets
// embedded with go:embed.
func modTimeFromInfo(i fs.FileInfo, timeFunc func() (time.Time, error)) (time.Time, error) {
	modTime := i.ModTime()
	if modTime.IsZero() {
		return timeFunc()
	}

	return modTime, nil
}

// etag calculates an etag string from the provided file size and modification
// time.
func etag(s int64, mt time.Time) string {
	hour, minute, second := mt.Clock()
	return fmt.Sprintf(`"%d%d%d%d%d"`, s, mt.Day(), hour, minute, second)
}

func buildTime() (time.Time, error) {
	return time.Parse(time.RFC3339, platform.GetBuildInfo().Date)
}
