package main

import (
	"context"
	"gifuu/routes"
	"gifuu/tools"
	"net"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"
	"time"
)

func main() {
	time.Local = time.UTC

	// Startup Services
	var stopCtx, stop = context.WithCancel(context.Background())
	var stopWg sync.WaitGroup
	var syncWg sync.WaitGroup

	tools.LoggerInit.Log(tools.INFO, "Starting Services")
	for _, fn := range []func(stop context.Context, await *sync.WaitGroup){
		tools.SetupDatabase,
		tools.SetupModel,
	} {
		syncWg.Add(1)
		go func() {
			defer syncWg.Done()
			fn(stopCtx, &stopWg)
		}()
	}
	syncWg.Wait()
	go StartupHTTP(stopCtx, &stopWg)

	// Await Shutdown Signal
	cancel := make(chan os.Signal, 1)
	signal.Notify(cancel, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
	<-cancel
	stop()

	// Begin Shutdown Process
	tools.LoggerInit.Log(tools.WARN, "Shutting Down!")
	timeout, finish := context.WithTimeout(context.Background(), tools.TIMEOUT_CONTEXT)
	defer finish()
	go func() {
		<-timeout.Done()
		if timeout.Err() == context.DeadlineExceeded {
			tools.LoggerInit.Log(tools.FATAL, "Shutdown Deadline Exceeded")
			return
		}
	}()
	stopWg.Wait()
	os.Exit(0)
}

func SetupMux() http.HandlerFunc {

	var (
		mux               = http.NewServeMux()
		limitPublic       = tools.NewRatelimiter("PUBLIC", 900, 5*time.Minute)
		limitDelete       = tools.NewRatelimiter("DELETE", 100, 5*time.Minute)
		limitStart        = tools.NewRatelimiter("START", 100, 5*time.Minute)
		limitCreate       = tools.NewRatelimiter("CREATE", 50, 5*time.Minute)
		limitModerators   = tools.NewRatelimiter("MOD", 300, 5*time.Minute)
		gatekeepModerator = tools.NewGatekeeper(true)
		powExpensive      = tools.NewChallenger(20)
		powNormal         = tools.NewChallenger(18)
	)

	// General
	mux.Handle("/uploads", tools.MethodHandler{
		http.MethodPost: tools.Chain(routes.POST_Uploads, limitCreate, powExpensive),
	})
	mux.Handle("/tags/popular", tools.MethodHandler{
		http.MethodGet: tools.Chain(routes.GET_Tags_Popular, limitPublic),
	})
	mux.Handle("/tags/autocomplete", tools.MethodHandler{
		http.MethodGet: tools.Chain(routes.GET_Tags_Autocomplete, limitPublic),
	})
	mux.Handle("/art/latest", tools.MethodHandler{
		http.MethodGet: tools.Chain(routes.GET_Art_Latest, limitPublic),
	})
	mux.Handle("/art/search", tools.MethodHandler{
		http.MethodGet: tools.Chain(routes.GET_Art_Search, limitPublic),
	})
	mux.Handle("/art/{id}", tools.MethodHandler{
		http.MethodGet:    tools.Chain(routes.GET_Art_ID, limitPublic),
		http.MethodDelete: tools.Chain(routes.DELETE_Art_ID, limitDelete),
	})
	mux.Handle("/art/{id}/report", tools.MethodHandler{
		http.MethodPost: tools.Chain(routes.POST_Art_ID_Reports, limitCreate, powNormal),
	})
	mux.Handle("/metadata/{id}", tools.MethodHandler{
		http.MethodGet: tools.Chain(routes.GET_Metadata_ID, limitPublic),
	})

	// Moderation
	mux.Handle("/moderation/art/{id}", tools.MethodHandler{
		http.MethodDelete: tools.Chain(routes.DELETE_Moderation_Art_ID, limitModerators, gatekeepModerator),
	})
	// mux.Handle("/moderation/art/{id}/rating", tools.MethodHandler{
	// 	// TODO: Sets the items moderation to 100% hiding it from the public
	// 	http.MethodPatch: tools.Chain(routes.PATCH_Moderation_Art_ID_Rating, limitModerators, gatekeepModerator),
	// })
	// mux.Handle("/moderation/art/{id}/bypass", tools.MethodHandler{
	// 	// TODO: Disables Reporting for the item by setting the 'flag_bypass' flag
	// 	http.MethodPatch: tools.Chain(routes.PATCH_Moderation_Art_ID_Bypass, limitModerators, gatekeepModerator),
	// })
	// mux.Handle("/moderation/art/{id}/reports", tools.MethodHandler{
	// 	// TODO: Get Latest Reports for an Item
	// 	http.MethodGet: tools.Chain(routes.GET_Moderation_Art_ID_Reports, limitModerators, gatekeepModerator),
	// 	// TODO: Delete all Reports for an Item
	// 	http.MethodDelete: tools.Chain(routes.DELETE_Moderation_Art_ID_Reports, limitModerators, gatekeepModerator),
	// })
	// // TODO: Get the Latest Reports across Site
	// mux.Handle("/moderation/latest", tools.MethodHandler{
	// 	http.MethodGet: tools.Chain(routes.GET_Moderation_Latest, limitModerators, gatekeepModerator),
	// })

	// Other
	mux.Handle("/challenge", tools.MethodHandler{
		http.MethodGet: tools.Chain(routes.GET_Challenge, limitStart),
	})
	mux.Handle("/limits", tools.MethodHandler{
		http.MethodGet: tools.Chain(routes.GET_Limits, limitPublic),
	})
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		tools.SendClientError(w, r, tools.ERROR_UNKNOWN_ENDPOINT)
	})

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		// Inject CORS
		w.Header().Set("Access-Control-Allow-Origin", "*")
		if r.Method == http.MethodOptions {
			w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
			w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Challenge-Nonce, X-Challenge-Counter")
			w.WriteHeader(http.StatusNoContent)
			return
		}

		mux.ServeHTTP(w, r)
	})
}

func StartupHTTP(stop context.Context, await *sync.WaitGroup) {
	var listener net.Listener
	var err error

	if strings.HasPrefix(tools.HTTP_ADDRESS, "unix/") {
		path := strings.TrimPrefix(tools.HTTP_ADDRESS, "unix/")
		os.Remove(path)
		listener, err = net.Listen("unix", path)
		if err == nil {
			os.Chmod(path, 0660)
		}
	} else {
		listener, err = net.Listen("tcp", tools.HTTP_ADDRESS)
	}
	if err != nil {
		tools.LoggerHTTP.Log(tools.FATAL, "Listen failed: %s", err)
		return
	}

	svr := http.Server{
		Handler:           SetupMux(),
		MaxHeaderBytes:    4096,
		IdleTimeout:       10 * time.Second,
		ReadHeaderTimeout: 10 * time.Second,
		ReadTimeout:       30 * time.Second,
		WriteTimeout:      30 * time.Second,
	}

	// Shutdown Logic
	await.Add(1)
	go func() {
		defer await.Done()
		<-stop.Done()

		shutdownCtx, cancel := context.WithTimeout(context.Background(), tools.TIMEOUT_SHUTDOWN)
		defer cancel()

		if err := svr.Shutdown(shutdownCtx); err != nil {
			tools.LoggerHTTP.Log(tools.ERROR, "Shutdown error: %s", err)
		}

		tools.LoggerHTTP.Log(tools.INFO, "Closed")
	}()

	tools.LoggerHTTP.Log(tools.INFO, "Listening @ %s", tools.HTTP_ADDRESS)
	if err := svr.Serve(listener); err != http.ErrServerClosed {
		tools.LoggerHTTP.Log(tools.FATAL, "Startup Failed: %s", err)
		return
	}
}
