package api

import (
	"encoding/json"
	"errors"
	"net/http"
	"strings"
	"time"

	"github.com/jackc/pgx/v5"

	"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
	"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
	"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
)

// sessionCookieMaxAge is the cookie lifetime. Sessions don't auto-expire
// server-side yet (future work); the cookie still caps browser-side lifetime
// so an abandoned laptop doesn't stay logged in forever.
const sessionCookieMaxAge = 30 * 24 * time.Hour

func (h *handlers) handleLogout(w http.ResponseWriter, r *http.Request) {
	// The session token can be on the cookie OR bearer header — RequireUser
	// accepted either. Re-resolve it here so we can delete the row.
	token := sessionTokenFromHTTP(r)
	if token != "" {
		if err := dbq.New(h.pool).DeleteSessionByTokenHash(r.Context(), auth.HashSessionToken(token)); err != nil {
			h.logger.Warn("api: delete session failed", "err", err)
			// Continue — logout is best-effort; the client still gets the
			// cookie cleared.
		}
	}
	http.SetCookie(w, &http.Cookie{
		Name:     auth.SessionCookieName,
		Value:    "",
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
		MaxAge:   -1,
	})
	w.WriteHeader(http.StatusNoContent)
}

// sessionTokenFromHTTP duplicates the internal helper in internal/auth
// because that one is unexported. Cheap to repeat here; keeping the auth
// package's internal helper package-private is worth more than DRY. Must
// match that helper's trimming behavior exactly — otherwise a bearer with
// trailing whitespace authenticates via RequireUser (which trims) but logout
// hashes the padded value and silently no-ops, leaving the session alive.
func sessionTokenFromHTTP(r *http.Request) string {
	if c, err := r.Cookie(auth.SessionCookieName); err == nil && c.Value != "" {
		return c.Value
	}
	h := r.Header.Get("Authorization")
	const prefix = "bearer "
	if len(h) > len(prefix) && (h[:7] == "Bearer " || h[:7] == "bearer ") {
		return strings.TrimSpace(h[len(prefix):])
	}
	return ""
}

func (h *handlers) handleLogin(w http.ResponseWriter, r *http.Request) {
	var req LoginRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		writeErr(w, apierror.BadRequest("bad_request", "invalid JSON body"))
		return
	}
	if req.Username == "" || req.Password == "" {
		writeErr(w, apierror.BadRequest("bad_request", "username and password required"))
		return
	}

	q := dbq.New(h.pool)
	user, err := q.GetUserByUsername(r.Context(), req.Username)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			writeErr(w, apierror.Unauthorized("invalid_credentials", "invalid username or password"))
			return
		}
		h.logger.Error("api: user lookup failed", "err", err)
		writeErr(w, apierror.InternalMsg("lookup failed", err))
		return
	}
	if !auth.VerifyPassword(user.PasswordHash, req.Password) {
		writeErr(w, apierror.Unauthorized("invalid_credentials", "invalid username or password"))
		return
	}

	token, err := auth.MintSessionToken()
	if err != nil {
		h.logger.Error("api: mint session token failed", "err", err)
		writeErr(w, apierror.InternalMsg("mint failed", err))
		return
	}
	if _, err := q.InsertSession(r.Context(), dbq.InsertSessionParams{
		UserID:    user.ID,
		TokenHash: auth.HashSessionToken(token),
		UserAgent: r.UserAgent(),
		// Origin address, frozen at issue time. Compared against last_ip in
		// the active-sessions surface: a session that was born somewhere the
		// user recognises but is being used from somewhere they don't is the
		// case this whole surface exists to surface.
		Ip: auth.ClientIP(r, h.netSettings.Hops()),
	}); err != nil {
		h.logger.Error("api: insert session failed", "err", err)
		writeErr(w, apierror.InternalMsg("insert failed", err))
		return
	}

	http.SetCookie(w, &http.Cookie{
		Name:     auth.SessionCookieName,
		Value:    token,
		Path:     "/",
		HttpOnly: true,
		Secure:   r.TLS != nil, // dev over http stays functional; prod over https gets Secure
		SameSite: http.SameSiteStrictMode,
		MaxAge:   int(sessionCookieMaxAge.Seconds()),
	})

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(LoginResponse{
		Token: token,
		User: UserView{
			ID:       user.ID,
			Username: user.Username,
			IsAdmin:  user.IsAdmin,
		},
	})
}
