package auth

import (
	"net/http"
	"time"

	"codeberg.org/nextgo/dbots/internal/db"
	"codeberg.org/nextgo/dbots/internal/discord"
	"codeberg.org/nextgo/dbots/internal/errorutil"
	"codeberg.org/nextgo/dbots/internal/middleware"
	"codeberg.org/nextgo/dbots/internal/token"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/render"
)

const cookieName = "session"

type Router struct {
	auth      *Service
	router    chi.Router
	pasetoKey string // hex-encoded AUTH_PASETO_KEY
	queries   *db.Queries
}

func NewRouter(q *db.Queries, client *discord.Client, pasetoKey string) *Router {
	return &Router{
		auth:      NewService(q, client),
		router:    chi.NewRouter(),
		pasetoKey: pasetoKey,
		queries:   q,
	}
}

func (r *Router) Routes() http.Handler {
	r.router.Get("/login", r.login)
	r.router.Get("/callback", r.callback)
	r.router.Post("/logout", r.logout)
	r.router.With(middleware.AuthGuardMiddleware).Get("/me", r.me)
	return r.router
}

func (r *Router) me(w http.ResponseWriter, req *http.Request) {
	user := middleware.GetUser(req.Context())
	if user == nil {
		render.Render(w, req, errorutil.ErrUnauthorized)
		return
	}
	render.JSON(w, req, user)
}

func (r *Router) login(w http.ResponseWriter, req *http.Request) {
	state, err := GenerateState()
	if err != nil {
		render.Render(w, req, errorutil.ErrInternal(err))
		return
	}

	http.SetCookie(w, &http.Cookie{
		Name:     "oauth_state",
		Value:    state,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		MaxAge:   300,
		Path:     "/",
	})

	http.Redirect(w, req, r.auth.client.AuthURL(state), http.StatusFound)
}

func (r *Router) callback(w http.ResponseWriter, req *http.Request) {
	stateCookie, err := req.Cookie("oauth_state")
	if err != nil || stateCookie.Value != req.URL.Query().Get("state") {
		render.Render(w, req, errorutil.ErrUnauthorized)
		return
	}
	http.SetCookie(w, &http.Cookie{Name: "oauth_state", MaxAge: -1, Path: "/"})

	code := req.URL.Query().Get("code")
	if code == "" {
		render.Render(w, req, errorutil.ErrInvalidRequest(nil))
		return
	}

	user, _, err := r.auth.Callback(req.Context(), code)
	if err != nil {
		render.Render(w, req, errorutil.ErrInternal(err))
		return
	}

	// Generate a session ID (jti) and persist it to the DB for server-side revocation.
	jti, err := GenerateState() // reuses the same crypto/rand helper
	if err != nil {
		render.Render(w, req, errorutil.ErrInternal(err))
		return
	}

	_, err = r.queries.CreateSession(req.Context(), db.CreateSessionParams{
		ID:        jti,
		UserID:    user.ID,
		ExpiresAt: time.Now().Add(token.TokenDuration),
	})
	if err != nil {
		render.Render(w, req, errorutil.ErrInternal(err))
		return
	}

	key, err := token.KeyFromHex(r.pasetoKey)
	if err != nil {
		render.Render(w, req, errorutil.ErrInternal(err))
		return
	}

	raw, err := token.IssueToken(key, user.ID, jti)
	if err != nil {
		render.Render(w, req, errorutil.ErrInternal(err))
		return
	}

	http.SetCookie(w, &http.Cookie{
		Name:     cookieName,
		Value:    raw,
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
		MaxAge:   int(token.TokenDuration.Seconds()),
		Path:     "/",
		// Secure: true, // enable in production (HTTPS)
	})

	http.Redirect(w, req, "/", http.StatusFound)
}

// POST /auth/logout — revoke the session server-side and clear the cookie.
func (r *Router) logout(w http.ResponseWriter, req *http.Request) {
	c, err := req.Cookie(cookieName)
	if err != nil {
		// Already logged out.
		render.NoContent(w, req)
		return
	}

	key, err := token.KeyFromHex(r.pasetoKey)
	if err == nil {
		if claims, err := token.VerifyToken(key, c.Value); err == nil {
			// Best-effort: ignore DB errors, the cookie will be cleared anyway.
			_ = r.queries.RevokeSession(req.Context(), claims.JTI)
		}
	}

	http.SetCookie(w, &http.Cookie{
		Name:   cookieName,
		MaxAge: -1,
		Path:   "/",
	})
	render.NoContent(w, req)
}
