package main

import (
	crand "crypto/rand"
	"database/sql"
	"encoding/hex"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"time"

	"golang.org/x/crypto/bcrypt"
)

type Session struct {
	UID         int64
	CreatedAt   int64
	Expires     int64
	Description string
	Value       string
}

var (
	reNonWord = regexp.MustCompile(`[^._\w\s-]`)
	reSpace   = regexp.MustCompile(`\s+`)
)

// getSession liest den session-Cookie und liefert die zugehörige, noch gültige Session.
func getSession(r *http.Request) (*Session, bool) {
	c, err := r.Cookie("session")
	if err != nil || c.Value == "" {
		return nil, false
	}

	s := Session{Value: c.Value}
	err = db.QueryRow(
		`SELECT uid, created_at, expires, description FROM session WHERE value = ?`,
		c.Value,
	).Scan(&s.UID, &s.CreatedAt, &s.Expires, &s.Description)
	if err != nil {
		return nil, false
	}

	if s.Expires < time.Now().Unix() {
		db.Exec(`DELETE FROM session WHERE value = ?`, c.Value)
		return nil, false
	}

	return &s, true
}

func handleLogin(w http.ResponseWriter, r *http.Request) {
	if _, ok := getSession(r); ok {
		writeJSON(w, http.StatusOK, map[string]string{"status": "already_logged_in"})
		return
	}

	username := sanitizeUsername(r.FormValue("user"))
	if len(username) < 3 {
		writeError(w, http.StatusBadRequest, "Der Nutzername ist zu kurz.")
		return
	}

	var uid int64
	var hash []byte
	err := db.QueryRow(`SELECT uid, password FROM user WHERE username = ?`, username).Scan(&uid, &hash)
	if err == sql.ErrNoRows {
		writeError(w, http.StatusUnauthorized, "Nutzername oder Passwort ist falsch.")
		return
	} else if err != nil {
		writeError(w, http.StatusInternalServerError, "Datenbankfehler")
		return
	}

	if bcrypt.CompareHashAndPassword(hash, []byte(r.FormValue("pass"))) != nil {
		writeError(w, http.StatusUnauthorized, "Nutzername oder Passwort ist falsch.")
		return
	}

	now := time.Now().Unix()
	// timeout ist clientgesteuert -> serverseitig deckeln, damit niemand eine
	// quasi-unbegrenzte Session anlegen kann. Default 1 Tag, Maximum 30 Tage.
	timeoutSec, _ := strconv.ParseInt(r.FormValue("timeout"), 10, 64)
	const maxTimeout = 30 * 86400
	if timeoutSec <= 0 {
		timeoutSec = 86400
	}
	if timeoutSec > maxTimeout {
		timeoutSec = maxTimeout
	}
	expires := now + timeoutSec

	token, err := newToken()
	if err != nil {
		writeError(w, http.StatusInternalServerError, "Token konnte nicht erzeugt werden")
		return
	}

	db.Exec(`DELETE FROM session WHERE expires < ?`, now)
	if _, err := db.Exec(
		`INSERT INTO session (uid, created_at, expires, description, value) VALUES (?, ?, ?, ?, ?)`,
		uid, now, expires, "", token,
	); err != nil {
		writeError(w, http.StatusInternalServerError, "Session konnte nicht erstellt werden")
		return
	}
	db.Exec(`UPDATE user SET last_login = ? WHERE uid = ?`, now, uid)

	http.SetCookie(w, &http.Cookie{
		Name:     "session",
		Value:    token,
		Expires:  time.Unix(expires, 0),
		Path:     "/",
		HttpOnly: true,
		Secure:   isHTTPS(r),
		SameSite: http.SameSiteLaxMode,
	})
	writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

func handleLogout(w http.ResponseWriter, r *http.Request) {
	if s, ok := getSession(r); ok {
		db.Exec(`DELETE FROM session WHERE value = ?`, s.Value)
	}
	http.SetCookie(w, &http.Cookie{
		Name:     "session",
		Value:    "",
		Path:     "/",
		Expires:  time.Unix(0, 0),
		MaxAge:   -1,
		HttpOnly: true,
		Secure:   isHTTPS(r),
		SameSite: http.SameSiteLaxMode,
	})
	writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

func handleNewUser(w http.ResponseWriter, r *http.Request) {
	var errs []string

	username := sanitizeUsername(r.FormValue("user"))
	if len(username) < 3 {
		errs = append(errs, "Der Nutzername ist zu kurz.")
	}
	if idFromUsername(username) != 0 {
		errs = append(errs, "Der Nutzername wird bereits verwendet.")
	}

	pass1 := r.FormValue("pass1")
	pass2 := r.FormValue("pass2")
	if pass1 != pass2 {
		errs = append(errs, "Passwort und Passwortbestätigung sind ungleich.")
	}
	if len(pass1) < 10 {
		errs = append(errs, "Das Passwort ist kürzer als 10 Zeichen.")
	}

	if len(errs) > 0 {
		writeJSON(w, http.StatusBadRequest, map[string]any{"errors": errs})
		return
	}

	if err := createUser(username, pass1); err != nil {
		writeError(w, http.StatusInternalServerError, "Account konnte nicht erstellt werden")
		return
	}
	writeJSON(w, http.StatusCreated, map[string]string{"username": username})
}

func handleSessionInfo(w http.ResponseWriter, r *http.Request) {
	s, ok := getSession(r)
	if !ok {
		writeError(w, http.StatusUnauthorized, "Invalid session")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"uid":         s.UID,
		"created_at":  s.CreatedAt,
		"expires":     s.Expires,
		"description": s.Description,
	})
}

func handleHeaderbar(w http.ResponseWriter, r *http.Request) {
	_, ok := getSession(r)
	writeJSON(w, http.StatusOK, map[string]bool{"loggedin": ok})
}

func sanitizeUsername(u string) string {
	u = strings.TrimSpace(u)
	if len(u) > 32 {
		u = u[:32]
	}
	u = reNonWord.ReplaceAllString(u, "")
	u = reSpace.ReplaceAllString(u, "_")
	return u
}

// isHTTPS erkennt, ob die Anfrage über TLS kam — direkt oder über einen
// TLS-terminierenden Reverse-Proxy (X-Forwarded-Proto). Steuert das Secure-Flag,
// ohne lokale Tests über http zu brechen.
func isHTTPS(r *http.Request) bool {
	return r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
}

func newToken() (string, error) {
	b := make([]byte, 32)
	if _, err := crand.Read(b); err != nil {
		return "", err
	}
	return hex.EncodeToString(b), nil
}
