package util

import (
	"bytes"
	"errors"
	"fmt"
	"html/template"
	"io/ioutil"
	"net"
	"net/http"
	pathUtil "path/filepath"
	"strings"

	"github.com/7sDream/rikka/common/logger"
)

// GetTaskIDByRequest gets last part of url path as a taskID and return it.
func GetTaskIDByRequest(r *http.Request) string {
	splitPath := strings.Split(r.URL.Path, "/")
	filename := splitPath[len(splitPath)-1]
	return filename
}

// ErrHandle is a simple error handle function.
// See ErrHandleWithCode, this func use `http.StatusInternalServerError` as code.
func ErrHandle(w http.ResponseWriter, err error) bool {
	return ErrHandleWithCode(w, err, http.StatusInternalServerError)
}

// ErrHandleWithCode is a simple error handle function.
// If err is an error, write code to header and write error message to response and return true.
// Else (err is nil), don't do anything and return false.
func ErrHandleWithCode(w http.ResponseWriter, err error, code int) bool {
	if err != nil {
		http.Error(w, err.Error(), code)
		return true
	}
	return false
}

// GetClientIP get client ip address from a http request.
// Try to get ip from x-forwarded-for header first,
// If key not exist in header, try to get ip:host from r.RemoteAddr
// split it to ip:port and return ip, If error happened, return 0.0.0.0
func GetClientIP(r *http.Request) string {
	defer func() {
		if r := recover(); r != nil {
			l.Error("Unexpected panic happened when get client ip:", r)
		}
	}()

	forwardIP := r.Header.Get("X-FORWARDED-FOR")
	if forwardIP != "" {
		return forwardIP
	}

	socket := r.RemoteAddr
	host, _, err := net.SplitHostPort(socket)
	if err != nil {
		l.Warn("Error happened when get IP address :", err)
		return "0.0.0.0"
	}
	return host
}

// CheckMethod check if request method is as excepted.
// If not, write the status "MethodNotAllow" to header, "Method Not Allowed." to response and return false.
// Else don't do anything and return true.
func CheckMethod(w http.ResponseWriter, r *http.Request, excepted string) bool {
	if r.Method != excepted {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return false
	}
	return true
}

// RenderTemplate is a shortcut function to render template to response.
func RenderTemplate(templatePath string, w http.ResponseWriter, data interface{}) error {
	t, err := template.ParseFiles(templatePath)
	if ErrHandle(w, err) {
		l.Error("Error happened when parse template file", templatePath, ":", err)
		return err
	}

	// a buff that ues in execute, if error happened,
	// error message will not be write to truly response
	buff := bytes.NewBuffer([]byte{})
	err = t.Execute(buff, data)

	// error happened, write a generic error message to response
	if err != nil {
		l.Error("Error happened when execute template", t, "with data", fmt.Sprintf("%+v", data), ":", err)
		ErrHandle(w, errors.New("error when render template"))
		return err
	}

	_, err = w.Write(buff.Bytes())

	return err
}

func RenderTemplateString(templateString string, w http.ResponseWriter, data interface{}) error {
	t := template.New("_")
	t, err := t.Parse(templateString)
	if ErrHandle(w, err) {
		l.Error("Error happened when parse template string", templateString, ":", err)
		return err
	}
	buff := bytes.NewBuffer([]byte{})
	err = t.Execute(buff, data)
	if err != nil {
		l.Error("Error happened when execute template", t, "with data", fmt.Sprintf("%+v", data), ":", err)
		ErrHandle(w, errors.New("error when render template"))
		return err
	}
	_, err = w.Write(buff.Bytes())
	return err
}

// RenderJson is a shortcut function to write JSON data to response, and set the header Content-Type.
func RenderJson(w http.ResponseWriter, data []byte, code int) (err error) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	_, err = w.Write(data)
	return err
}

// MustBeOr404 check if URL path is as excepted.
// If not equal, write 404 to header, "404 not fount" to response, and return false.
// Else don't do anything and return true.
func MustBeOr404(w http.ResponseWriter, r *http.Request, path string) bool {
	if r.URL.Path != path {
		http.NotFound(w, r)
		return false
	}
	return true
}

// MustExistOr404 check if a file is exist.
// If not, write 404 to header, "404 not fount" to response, and return false.
// Else don't do anything and return true.
//noinspection GoUnusedExportedFunction
func MustExistOr404(w http.ResponseWriter, r *http.Request, filepath string) bool {
	if !CheckExist(filepath) {
		http.NotFound(w, r)
		return false
	}
	return true
}

// DisableListDir accept a handle func and return a handle that not allow list dir.
func DisableListDir(log *logger.Logger, h http.HandlerFunc) http.HandlerFunc {
	if log == nil {
		l.Warn("Get a nil logger in function DisableListDirFunc")
		log = l
	}
	return func(w http.ResponseWriter, r *http.Request) {
		if strings.HasSuffix(r.URL.Path, "/") {
			log.Warn(GetClientIP(r), "try to list dir", r.URL.Path)
			http.NotFound(w, r)
		} else {
			h(w, r)
		}
	}
}

// ContextCreator accept a request and return a context, used in TemplateRenderHandler.
type ContextCreator func(r *http.Request) interface{}

// TemplateStringRenderHandler is a shortcut function that generate a http.HandlerFunc.
// The generated func use contextCreator to create context and render the templateString as template.
// If contextCreator is nil, nil will be used as context.
func TemplateStringRenderHandler(templateName string, templateString string, contextCreator ContextCreator, log *logger.Logger) http.HandlerFunc {
	if log == nil {
		l.Warn("Get a nil logger in function TemplateRenderHandler")
		log = l
	}
	return func(w http.ResponseWriter, r *http.Request) {
		ip := GetClientIP(r)
		log.Info("Receive a template render request of", templateName, "from ip", ip)

		var data interface{}
		if contextCreator != nil {
			data = contextCreator(r)
		} else {
			data = nil
		}

		err := RenderTemplateString(templateString, w, data)

		if err != nil {
			log.Warn("Error happened when render template string", templateString, "with data", fmt.Sprintf("%#v", data), "to", ip, ": ", err)
		}

		log.Info("Render template", templateName, "to", ip, "successfully")
	}
}

// TemplateRenderHandler is a shortcut function that generate a http.HandlerFunc.
// The generated func use contextCreator to create context and render the templatePath template file.
// If contextCreator is nil, nil will be used as context.
func TemplateRenderHandler(templatePath string, contextCreator ContextCreator, log *logger.Logger) http.HandlerFunc {
	templateBytes, err := ioutil.ReadFile(templatePath)
	if err != nil {
		l.Fatal("Error when read template file", templatePath, ":", err)
	}
	templateName := pathUtil.Base(templatePath)
	templateString := string(templateBytes)
	return TemplateStringRenderHandler(templateName, templateString, contextCreator, log)
}

// RequestFilter accept a http.HandlerFunc and return a new one
// which only accept path is pathMustBe and method is methodMustBe.
// Error message in new handler will be print with logger log, if log is nil, will use default logger.
// If pathMustBe or methodMustBe is empty string, no check will be performed.
func RequestFilter(pathMustBe string, methodMustBe string, log *logger.Logger, handlerFunc http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {

		if log == nil {
			l.Warn("Get a nil logger in function RequestFilter")
			log = l
		}

		ip := GetClientIP(r)

		if pathMustBe != "" {
			if !MustBeOr404(w, r, pathMustBe) {
				log.Warn(ip, "visit a non-exist page", r.URL.Path, ", excepted is /")
				return
			}
		}

		if methodMustBe != "" {
			if !CheckMethod(w, r, methodMustBe) {
				log.Warn(ip, "visit page", r.URL.Path, "with method", r.Method, ", only", methodMustBe, "is allowed")
				return
			}
		}

		handlerFunc(w, r)
	}
}
