package handlers

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"path/filepath"
	"runtime/debug"
	"strconv"

	"gogs.carducci-dante.gov.it/karmen/core/config"
	"gogs.carducci-dante.gov.it/karmen/core/orm"
	"gogs.carducci-dante.gov.it/karmen/core/renderer"

	jwtmiddleware "github.com/auth0/go-jwt-middleware"
	jwt "github.com/dgrijalva/jwt-go"
	"github.com/gorilla/mux"
	"github.com/gorilla/sessions"
)

type User struct {
	Name  string
	Admin bool
}

type PathPattern struct {
	PathPattern     string
	RedirectPattern string
	Methods         []string
}

var (
	signingKey = []byte(config.Config.Keys.JWTSigningKey)
	store      = sessions.NewCookieStore([]byte(config.Config.Keys.CookieStoreKey))

	jwtCookie = jwtmiddleware.New(jwtmiddleware.Options{
		ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
			return signingKey, nil
		},
		SigningMethod: jwt.SigningMethodHS256,
		Extractor:     fromCookie,
		ErrorHandler:  onError,
	})

	jwtHeader = jwtmiddleware.New(jwtmiddleware.Options{
		ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
			return signingKey, nil
		},
		SigningMethod: jwt.SigningMethodHS256,
	})

	models = []string{
		"teachers",
		"classes",
		"subjects",
		"departments",
		"activities",
		"students",
		"offices",
		"administratives",
		"documents",
		"jobs",
	}
)

func (pp PathPattern) RedirectPath(model string, id ...uint) string {
	if len(id) > 0 {
		return fmt.Sprintf(pp.RedirectPattern, model, id[0], model)
	}
	return fmt.Sprintf(pp.RedirectPattern, model, model)
}

func (pp PathPattern) Path(model string) string {
	return fmt.Sprintf(pp.PathPattern, model)
}

// Generate CRUD handlers
func generateHandler(r *mux.Router, model string) {
	var (
		patterns []PathPattern = []PathPattern{
			PathPattern{"/%s", "", []string{"GET"}},
			PathPattern{"/%s/{id}", "", []string{"GET"}},
			PathPattern{"/%s/add/", "/%s/%d?format=html&tpl_layout=base&tpl_content=%s_show", []string{"GET", "POST"}},
			PathPattern{"/%s/{id}/update", "/%s/%d?format=html&tpl_layout=base&tpl_content=%s_show", []string{"GET", "POST"}},
			PathPattern{"/%s/{id}/delete", "/%s?format=html&tpl_layout=base&tpl_content=%s", []string{"DELETE"}},
		}

		apiPatterns []PathPattern = []PathPattern{
			PathPattern{"/api/%s", "", []string{"GET"}},
			PathPattern{"/api/%s/{id}", "", []string{"GET"}},
			PathPattern{"/api/%s/add/", "", []string{"GET", "POST"}},
			PathPattern{"/api/%s/{id}/update", "", []string{"GET", "POST"}},
			PathPattern{"/api/%s/{id}/delete", "", []string{"DELETE"}},
		}

		executePatterns []PathPattern = []PathPattern{
			PathPattern{"/%s/{id}/execute", "", []string{"GET"}},
		}

		filePatterns []PathPattern = []PathPattern{
			PathPattern{"/%s/{id}/files/{filename}", "", []string{"GET"}},
		}
	)
	for _, pattern := range patterns {
		r.Handle(pattern.Path(model), jwtCookie.Handler(recoverHandler(modelHandler(model, pattern)))).Methods(pattern.Methods...)
	}

	for _, pattern := range apiPatterns {
		r.Handle(pattern.Path(model), jwtHeader.Handler(recoverHandler(modelHandler(model, pattern)))).Methods(pattern.Methods...)
	}

	if model == "documents" {
		for _, pattern := range executePatterns {
			r.Handle(pattern.Path(model), jwtCookie.Handler(recoverHandler(modelHandler(model, pattern)))).Methods(pattern.Methods...)
		}
	}

	if model == "jobs" {
		for _, pattern := range filePatterns {
			r.Handle(pattern.Path(model), jwtCookie.Handler(recoverHandler(modelHandler(model, pattern)))).Methods(pattern.Methods...)
		}
	}

}

func Handlers() *mux.Router {
	r := mux.NewRouter()

	// Authentication

	r.Handle("/login", loginHandler())
	r.Handle("/logout", logoutHandler())

	// Dashboard

	r.Handle("/", jwtCookie.Handler(recoverHandler(homeHandler())))

	// Generate model handlers

	for _, model := range models {
		generateHandler(r, model)
	}

	// Token handling
	r.Handle("/get_token", tokenHandler())

	// Static file server
	r.PathPrefix("/").Handler(http.FileServer(http.Dir("./dist/")))

	return r
}

func onError(w http.ResponseWriter, r *http.Request, err string) {
	http.Redirect(w, r, "/login?tpl_layout=login&tpl_content=login", http.StatusTemporaryRedirect)
}

func respondWithStaticFile(w http.ResponseWriter, filename string) error {
	f, err := ioutil.ReadFile(filepath.Join("public/html", filename))
	if err != nil {
		return err
	}
	w.Write(f)
	return nil
}

func fromCookie(r *http.Request) (string, error) {
	session, err := store.Get(r, "login-session")
	if err != nil {
		return "", nil
	}

	if session.Values["token"] == nil {
		return "", nil
	}

	token := session.Values["token"].([]uint8)

	return string(token), nil
}

func recoverHandler(next http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				panicMsg := fmt.Sprintf("PANIC: %v\n\n== STACKTRACE ==\n%s", err, debug.Stack())
				log.Print(panicMsg)
				http.Error(w, panicMsg, http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	}
	return http.HandlerFunc(fn)
}

func get(w http.ResponseWriter, r *http.Request, model string, pattern PathPattern) {
	format := r.URL.Query().Get("format")
	getFn, err := orm.GetFunc(pattern.Path(model))

	if err != nil {
		respondWithError(w, r, err)
	} else {
		data, err := getFn(mux.Vars(r))
		if err != nil {
			renderer.Render[format](w, r, err)
		} else {
			renderer.Render[format](w, r, data, r.URL.Query())
		}
	}

}

func respondWithError(w http.ResponseWriter, r *http.Request, err error) {
	respFormat := renderer.GetContentFormat(r)
	w.WriteHeader(http.StatusInternalServerError)
	renderer.Render[respFormat](w, r, err)
}

func post(w http.ResponseWriter, r *http.Request, model string, pattern PathPattern) {
	var (
		data orm.IDer
		err  error
	)

	respFormat := renderer.GetContentFormat(r)
	postFn, err := orm.PostFunc(pattern.Path(model))

	if err != nil {
		respondWithError(w, r, err)
	} else {
		data, err = postFn(mux.Vars(r), r)
		if err != nil {
			respondWithError(w, r, err)
		} else if pattern.RedirectPattern != "" {
			if id := mux.Vars(r)["id"]; id != "" {
				modelId, _ := strconv.Atoi(id)
				http.Redirect(w, r, pattern.RedirectPath(model, uint(modelId)), http.StatusSeeOther)
			} else {
				http.Redirect(w, r, pattern.RedirectPath(model, data.GetID()), http.StatusSeeOther)
			}
		} else {
			renderer.Render[respFormat](w, r, data.GetID())
		}

	}

}

func delete(w http.ResponseWriter, r *http.Request, model string, pattern PathPattern) {
	postFn, ok := orm.Post[pattern.Path(model)]
	if !ok {
		renderer.Render[r.URL.Query().Get("format")](w, r, fmt.Errorf("Can't find ORM function for path %s!", pattern.PathPattern))
	}
	_, err := postFn(mux.Vars(r), r)
	if err != nil {
		renderer.Render["html"](w, r, err)
	} else {
		var data struct {
			RedirectUrl string `json:"redirect_url"`
		}
		data.RedirectUrl = pattern.RedirectPath(model)

		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(data)
	}
}

func modelHandler(model string, pattern PathPattern) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {

		case "GET":
			get(w, r, model, pattern)

		case "POST":
			post(w, r, model, pattern)

		case "DELETE":
			delete(w, r, model, pattern)
		}
	}

	return http.HandlerFunc(fn)
}

func homeHandler() http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/teachers?format=html&tpl_layout=teachers&tpl_content=teachers", http.StatusSeeOther)
	}
	return http.HandlerFunc(fn)
}
