package main

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"slices"
	"sync"
)

type ApiHandler struct {
	db         *Database
	mutex      sync.RWMutex
	authTokens []string
}

const authTokenCookieName = "auth-token"
const isAuthorizedContextKey = "is-authorized"
const contentTypeHeaderKey = "Content-Type"
const JsonMimeType = "application/json"

func MakeApiHandler(db *Database) ApiHandler {
	return ApiHandler{
		db:         db,
		mutex:      sync.RWMutex{},
		authTokens: make([]string, 0),
	}
}

func (h *ApiHandler) ServeLoginPost(writer http.ResponseWriter, request *http.Request) {
	if !HasContentType(request, JsonMimeType) {
		WriteError(writer, http.StatusBadRequest, "expected json body", nil)
		return
	}

	bodyReader := request.Body
	body, err := io.ReadAll(bodyReader)
	_ = bodyReader.Close()
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "failed to read body", err)
		return
	}

	type LoginBody struct {
		Password string `json:"password"`
	}

	loginBody := LoginBody{}
	err = json.Unmarshal(body, &loginBody)
	if err != nil {
		WriteError(writer, http.StatusBadRequest, "failed to read body", err)
		return
	}

	success, err := h.db.ValidateRootPassword(loginBody.Password)
	if err != nil {
		WriteError(writer, http.StatusInternalServerError, "failed to read database", err)
		return
	}

	if !success {
		log.Printf("failed login from '%s'", request.RemoteAddr)
		WriteError(writer, http.StatusUnauthorized, "invalid password", nil)
		return
	}

	rawAuthToken := make([]byte, 128)
	_, _ = rand.Read(rawAuthToken)
	authToken := hex.EncodeToString(rawAuthToken)

	h.mutex.Lock()
	h.authTokens = append(h.authTokens, authToken)
	h.mutex.Unlock()

	cookie := http.Cookie{}
	cookie.Name = authTokenCookieName
	cookie.Value = authToken
	cookie.Secure = true
	cookie.HttpOnly = true
	http.SetCookie(writer, &cookie)

	WriteResponse(writer, http.StatusOK, map[string]interface{}{})

	log.Printf("successful login from '%s'", request.RemoteAddr)
}

func (h *ApiHandler) ServeLogoutPost(writer http.ResponseWriter, request *http.Request) {
	cookie, _ := request.Cookie(authTokenCookieName)

	h.mutex.Lock()
	index := slices.Index(h.authTokens, cookie.Value)
	if index >= 0 {
		slices.Delete(h.authTokens, index, index+1)
	}
	h.mutex.Unlock()

	newCookie := http.Cookie{}
	newCookie.Name = authTokenCookieName
	newCookie.Value = ""
	newCookie.Secure = true
	newCookie.HttpOnly = true
	http.SetCookie(writer, &newCookie)

	WriteResponse(writer, http.StatusOK, map[string]interface{}{})

	log.Printf("successful logout from '%s'", request.RemoteAddr)
}

func (h *ApiHandler) ProcessAuth(next http.Handler, required bool) http.Handler {
	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		isAuthorized := false
		cookie, _ := request.Cookie(authTokenCookieName)
		if cookie != nil {
			h.mutex.RLock()
			isAuthorized = slices.Contains(h.authTokens, cookie.Value)
			h.mutex.RUnlock()
		}

		if !isAuthorized && required {
			WriteError(writer, http.StatusUnauthorized, "authentication required", nil)
			return
		}

		next.ServeHTTP(writer, request.WithContext(context.WithValue(request.Context(), isAuthorizedContextKey, isAuthorized)))
	})
}

func IsAuthorized(request *http.Request) bool {
	value := request.Context().Value(isAuthorizedContextKey)
	return value != nil && value.(bool)
}

func WriteResponse(writer http.ResponseWriter, code int, body any) {
	writer.Header().Set(contentTypeHeaderKey, "application/json")
	writer.WriteHeader(code)
	_ = json.NewEncoder(writer).Encode(body)
}

func WriteError(writer http.ResponseWriter, code int, message string, err error) {
	if err != nil {
		log.Println(err)
	}

	WriteResponse(writer, code, map[string]interface{}{
		"message": message,
	})
}

func HasContentType(request *http.Request, mimeType string) bool {
	contentType := request.Header.Get(contentTypeHeaderKey)
	return contentType == mimeType
}
