package handlers

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"time"

	"golang.org/x/crypto/bcrypt"

	"git.soup.land/soup/lookbook/internal/data/auth"
	"git.soup.land/soup/lookbook/internal/data/session"
)

const sessionCookieName = "lookbook_session"
const sessionDuration = 30 * 24 * time.Hour

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

type authStatus struct {
	Authenticated bool `json:"authenticated"`
	HasPassword   bool `json:"has_password"`
}

func HandlePostAuthLogin(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
	var req authRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return nil
	}
	if req.Password == "" {
		http.Error(w, "Password required", http.StatusBadRequest)
		return nil
	}

	existing, err := auth.QGet(r.Context(), rc.DB)
	if err != nil {
		return fmt.Errorf("get auth: %w", err)
	}

	if existing == nil {
		hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
		if err != nil {
			return fmt.Errorf("hash password: %w", err)
		}
		if _, err := auth.QCreate(r.Context(), rc.DB, hash); err != nil {
			return fmt.Errorf("create password: %w", err)
		}
	} else {
		if err := bcrypt.CompareHashAndPassword(existing.PasswordHash, []byte(req.Password)); err != nil {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return nil
		}
	}

	token, err := newToken(32)
	if err != nil {
		return fmt.Errorf("token: %w", err)
	}

	expiresAt := time.Now().Add(sessionDuration)
	if _, err := session.QCreate(r.Context(), rc.DB, token, expiresAt); err != nil {
		return fmt.Errorf("create session: %w", err)
	}

	setSessionCookie(w, r, token, expiresAt)
	w.WriteHeader(http.StatusNoContent)
	return nil
}

func HandlePostAuthLogout(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
	cookie, err := r.Cookie(sessionCookieName)
	if err == nil {
		_ = session.QDelete(r.Context(), rc.DB, cookie.Value)
	}
	clearSessionCookie(w, r)
	w.WriteHeader(http.StatusNoContent)
	return nil
}

func HandleGetAuthStatus(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
	status := authStatus{}

	existing, err := auth.QGet(r.Context(), rc.DB)
	if err != nil {
		return fmt.Errorf("get auth: %w", err)
	}
	status.HasPassword = existing != nil

	authed, err := isAuthenticated(r, rc)
	if err != nil {
		return fmt.Errorf("auth status: %w", err)
	}
	status.Authenticated = authed

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

func isAuthenticated(r *http.Request, rc *RequestContext) (bool, error) {
	cookie, err := r.Cookie(sessionCookieName)
	if err != nil {
		return false, nil
	}

	row, err := session.QFindByToken(r.Context(), rc.DB, cookie.Value)
	if err != nil {
		return false, err
	}
	if row == nil {
		return false, nil
	}
	if time.Now().After(row.ExpiresAt) {
		_ = session.QDelete(r.Context(), rc.DB, cookie.Value)
		return false, nil
	}
	return true, nil
}

func requireAuth(w http.ResponseWriter, r *http.Request, rc *RequestContext) bool {
	authed, err := isAuthenticated(r, rc)
	if err != nil {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return false
	}
	if !authed {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return false
	}
	return true
}

func setSessionCookie(w http.ResponseWriter, r *http.Request, token string, expiresAt time.Time) {
	http.SetCookie(w, &http.Cookie{
		Name:     sessionCookieName,
		Value:    token,
		Path:     "/",
		HttpOnly: true,
		Secure:   r.TLS != nil,
		SameSite: http.SameSiteLaxMode,
		Expires:  expiresAt,
	})
}

func clearSessionCookie(w http.ResponseWriter, r *http.Request) {
	http.SetCookie(w, &http.Cookie{
		Name:     sessionCookieName,
		Value:    "",
		Path:     "/",
		HttpOnly: true,
		Secure:   r.TLS != nil,
		SameSite: http.SameSiteLaxMode,
		Expires:  time.Unix(0, 0),
	})
}

func newToken(length int) (string, error) {
	buf := make([]byte, length)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(buf), nil
}
