package assets

import (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	utilruntime "k8s.io/kubernetes/pkg/util/runtime"
)

// ExtensionScriptsHandler concatenates and serves extension JavaScript files as one HTTP response.
func ExtensionScriptsHandler(files []string, developmentMode bool) (http.Handler, error) {
	return concatHandler(files, developmentMode, "text/javascript", ";\n")
}

// ExtensionStylesheetsHandler concatenates and serves extension stylesheets as one HTTP response.
func ExtensionStylesheetsHandler(files []string, developmentMode bool) (http.Handler, error) {
	return concatHandler(files, developmentMode, "text/css", "\n")
}

func concatHandler(files []string, developmentMode bool, mediaType, separator string) (http.Handler, error) {
	// Read the files for each request if development mode is enabled.
	if developmentMode {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			bytes, err := concatAll(files, separator)
			if err != nil {
				utilruntime.HandleError(fmt.Errorf("Error serving extension content: %v", err))
				http.Error(w, "Internal server error", http.StatusInternalServerError)
			}
			serve(w, r, bytes, mediaType, "")
		}), nil
	}

	// Otherwise, read the files once on server startup.
	bytes, err := concatAll(files, separator)
	if err != nil {
		return nil, err
	}
	hash := calculateMD5(bytes)

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		serve(w, r, bytes, mediaType, hash)
	}), nil
}

func concatAll(files []string, separator string) ([]byte, error) {
	var buffer bytes.Buffer
	for _, file := range files {
		f, err := os.Open(file)
		if err != nil {
			return nil, err
		}
		defer f.Close()

		_, err = buffer.ReadFrom(f)
		if err != nil {
			return nil, err
		}

		_, err = buffer.WriteString(separator)
		if err != nil {
			return nil, err
		}
	}

	return buffer.Bytes(), nil
}

func calculateMD5(bytes []byte) string {
	hasher := md5.New()
	hasher.Write(bytes)
	sum := hasher.Sum(nil)

	return hex.EncodeToString(sum)
}

func generateETag(w http.ResponseWriter, r *http.Request, hash string) string {
	vary := w.Header().Get("Vary")
	varyHeaders := []string{}
	if vary != "" {
		varyHeaders = varyHeaderRegexp.Split(vary, -1)
	}

	varyHeaderValues := ""
	for _, varyHeader := range varyHeaders {
		varyHeaderValues += r.Header.Get(varyHeader)
	}

	return fmt.Sprintf("W/\"%s_%s\"", hash, hex.EncodeToString([]byte(varyHeaderValues)))
}

func serve(w http.ResponseWriter, r *http.Request, bytes []byte, mediaType, hash string) {
	if len(bytes) == 0 {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	if len(hash) > 0 {
		etag := generateETag(w, r, hash)
		if r.Header.Get("If-None-Match") == etag {
			w.WriteHeader(http.StatusNotModified)
			return
		}

		w.Header().Set("ETag", etag)
		w.Header().Set("Cache-Control", "public, max-age=0, must-revalidate")
	} else {
		w.Header().Add("Cache-Control", "no-cache, no-store")
	}

	w.Header().Set("Content-Type", mediaType)
	_, err := w.Write(bytes)
	if err != nil {
		utilruntime.HandleError(fmt.Errorf("Error serving extension content: %v", err))
	}
}

// AssetExtensionHandler serves extension files from sourceDir. context is the URL context for this
// extension.
func AssetExtensionHandler(sourceDir, context string, html5Mode bool) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		serveExtensionFile(w, r, sourceDir, context, html5Mode)
	})
}

// Injects the HTML <base> into the file and serves it.
func serveIndex(w http.ResponseWriter, r *http.Request, path, base string) {
	content, err := ioutil.ReadFile(path)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	// Make sure the base always ends in a trailing slash.
	if !strings.HasSuffix(base, "/") {
		base += "/"
	}
	content = bytes.Replace(content, []byte(`<base href="/">`), []byte(fmt.Sprintf(`<base href="%s">`, base)), 1)

	w.Header().Add("Cache-Control", "no-cache, no-store")
	w.Header().Set("Content-Type", "text/html; charset=UTF-8")

	w.Write(content)
}

func serveFile(w http.ResponseWriter, r *http.Request, path, base string, html5Mode bool) {
	_, name := filepath.Split(path)
	if html5Mode && name == "index.html" {
		// Inject the correct base for Angular apps if the file is index.html.
		serveIndex(w, r, path, base)
	} else {
		// Otherwise just serve the file.
		http.ServeFile(w, r, path)
	}
}

// Serve the extension file under dir matching the path from the request URI.
func serveExtensionFile(w http.ResponseWriter, r *http.Request, sourceDir, context string, html5Mode bool) {
	// The path to the requested file on the filesystem.
	file := filepath.Join(sourceDir, r.URL.Path)

	if html5Mode {
		// Check if the file exists.
		fileInfo, err := os.Stat(file)
		if err != nil {
			if os.IsNotExist(err) {
				index := filepath.Join(sourceDir, "index.html")
				serveFile(w, r, index, context, html5Mode)
				return
			}

			utilruntime.HandleError(fmt.Errorf("Error serving extension file: %v", err))
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		if fileInfo.IsDir() {
			index := filepath.Join(sourceDir, "index.html")
			serveFile(w, r, index, context, html5Mode)
			return
		}
	}

	serveFile(w, r, file, context, html5Mode)
}