package main

import (
	"database/sql"
	"math/rand/v2"
	"net/http"
	"os"
	"time"

	"github.com/go-chi/chi/v5"
	"golang.org/x/crypto/bcrypt"
)

func createUser(username, password string) error {
	now := time.Now().Unix()

	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}

	// uid ist nie 0: 0 ist Sentinel für "gelöscht"/"anonym" (siehe handleDeleteEntry,
	// uidFromContext). Bei der seltenen UNIQUE-Kollision auf uid neu würfeln.
	var lastErr error
	for i := 0; i < 5; i++ {
		uid := int64(rand.IntN(99999999)) + 1
		_, lastErr = db.Exec(
			`INSERT INTO user (uid, created_at, username, password) VALUES (?, ?, ?, ?)`,
			uid, now, username, hash,
		)
		if lastErr == nil {
			return nil
		}
		// username ist ebenfalls UNIQUE und wird vom Aufrufer vorab geprüft;
		// eine Kollision hier kann also nur die uid sein -> erneut versuchen.
	}
	return lastErr
}

// idFromUsername liefert die interne id oder 0, wenn der Nutzer nicht existiert.
func idFromUsername(username string) int64 {
	var id int64
	if err := db.QueryRow(`SELECT id FROM user WHERE username = ?`, username).Scan(&id); err != nil {
		return 0
	}
	return id
}

// serveUserPage liefert die statische Profilseite; das Frontend lädt die Daten
// dann über /u/{username}/info und /u/{username}/feed/{page}.
func serveUserPage(w http.ResponseWriter, r *http.Request) {
	http.ServeFile(w, r, "web/user.html")
}

func handleUserPage(w http.ResponseWriter, r *http.Request) {
	username := chi.URLParam(r, "username")

	var uid, createdAt int64
	var avatar string
	err := db.QueryRow(`SELECT uid, created_at, avatar FROM user WHERE username = ?`, username).Scan(&uid, &createdAt, &avatar)
	if err != nil {
		writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"uid":        uid,
		"username":   username,
		"created_at": createdAt,
		"avatar":     avatar,
	})
}

func handleUserInfo(w http.ResponseWriter, r *http.Request) {
	uid := uidFromContext(r.Context())

	var createdAt, lastLogin sql.NullInt64
	var username, avatar string
	err := db.QueryRow(
		`SELECT created_at, last_login, username, avatar FROM user WHERE uid = ?`, uid,
	).Scan(&createdAt, &lastLogin, &username, &avatar)
	if err != nil {
		writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
		return
	}
	writeJSON(w, http.StatusOK, map[string]any{
		"uid":        uid,
		"username":   username,
		"created_at": createdAt.Int64,
		"last_login": lastLogin.Int64,
		"avatar":     avatar,
	})
}

// handleUserRename benennt den eingeloggten Account um. Nichts außer der user-Zeile
// referenziert den username (alles hängt an der uid), daher genügt ein UPDATE.
func handleUserRename(w http.ResponseWriter, r *http.Request) {
	uid := uidFromContext(r.Context())

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

	var current string
	if err := db.QueryRow(`SELECT username FROM user WHERE uid = ?`, uid).Scan(&current); err != nil {
		writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
		return
	}
	if name == current {
		writeError(w, http.StatusBadRequest, "Das ist bereits dein Name.")
		return
	}
	if idFromUsername(name) != 0 {
		writeError(w, http.StatusConflict, "Der Nutzername wird bereits verwendet.")
		return
	}

	if _, err := db.Exec(`UPDATE user SET username = ? WHERE uid = ?`, name, uid); err != nil {
		writeError(w, http.StatusInternalServerError, "Umbenennen fehlgeschlagen")
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"username": name})
}

// handleSetAvatar speichert ein hochgeladenes Profilbild des eingeloggten
// Nutzers (über storeImage skaliert) und ersetzt ein eventuell vorhandenes.
func handleSetAvatar(w http.ResponseWriter, r *http.Request) {
	uid := uidFromContext(r.Context())

	// Wie bei handleCreateEntry: Request-Größe hart deckeln, bevor Multipart
	// in Temp-Dateien streamen darf.
	r.Body = http.MaxBytesReader(w, r.Body, maxUploadBytes)
	if err := r.ParseMultipartForm(16 << 20); err != nil {
		writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
		return
	}
	if r.MultipartForm == nil || len(r.MultipartForm.File["avatar"]) == 0 {
		writeError(w, http.StatusBadRequest, "Kein Bild hochgeladen.")
		return
	}

	stored, err := storeImage(r.MultipartForm.File["avatar"][0])
	if err != nil {
		writeError(w, http.StatusBadRequest, "Bild konnte nicht verarbeitet werden.")
		return
	}

	var old string
	db.QueryRow(`SELECT avatar FROM user WHERE uid = ?`, uid).Scan(&old)

	if _, err := db.Exec(`UPDATE user SET avatar = ? WHERE uid = ?`, stored, uid); err != nil {
		os.Remove(stored)
		writeError(w, http.StatusInternalServerError, "Speichern fehlgeschlagen")
		return
	}
	if old != "" && old != stored {
		os.Remove(old)
	}
	writeJSON(w, http.StatusOK, map[string]string{"avatar": stored})
}

// handleUserDelete löscht den eingeloggten Account dauerhaft, nach Bestätigung
// durch das Passwort (Formularfeld pass1). Sessions und Votes werden in einer
// Transaktion mitgelöscht; die eigenen Beiträge werden soft-gelöscht (als
// [deleted]-Platzhalter erhalten), damit fremde Antworten nicht verwaisen.
// Zugehörige Mediendateien werden best effort entfernt.
func handleUserDelete(w http.ResponseWriter, r *http.Request) {
	uid := uidFromContext(r.Context())

	var hash []byte
	if err := db.QueryRow(`SELECT password FROM user WHERE uid = ?`, uid).Scan(&hash); err != nil {
		writeError(w, http.StatusNotFound, "Nutzer nicht gefunden")
		return
	}

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

	// Mediendateien der Beiträge (und das Profilbild) einsammeln, bevor die
	// Pfade geleert bzw. die user-Zeile gelöscht wird.
	var media []string
	if rows, err := db.Query(`SELECT filepath FROM entry WHERE uid = ?`, uid); err == nil {
		for rows.Next() {
			var fp string
			if rows.Scan(&fp) == nil && fp != "" {
				media = append(media, fp)
			}
		}
		rows.Close()
	}
	var avatar string
	if db.QueryRow(`SELECT avatar FROM user WHERE uid = ?`, uid).Scan(&avatar) == nil && avatar != "" {
		media = append(media, avatar)
	}

	tx, err := db.Begin()
	if err != nil {
		writeError(w, http.StatusInternalServerError, "Löschen fehlgeschlagen")
		return
	}
	for _, q := range []string{
		`DELETE FROM session WHERE uid = ?`,
		`DELETE FROM vote WHERE uid = ?`,
		`UPDATE entry SET deleted = 1, content = '', filepath = '', uid = 0 WHERE uid = ?`,
		`DELETE FROM user WHERE uid = ?`,
	} {
		if _, err := tx.Exec(q, uid); err != nil {
			tx.Rollback()
			writeError(w, http.StatusInternalServerError, "Löschen fehlgeschlagen")
			return
		}
	}
	if err := tx.Commit(); err != nil {
		writeError(w, http.StatusInternalServerError, "Löschen fehlgeschlagen")
		return
	}

	// Erst nach erfolgreichem Commit die Dateien entfernen.
	for _, fp := range media {
		os.Remove(fp)
	}

	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": "deleted"})
}
