package handlers

import (
	"net/http"
	"strings"
	"time"

	"warpbox.dev/backend/libs/services"
	"warpbox.dev/backend/libs/web"
)

const userSessionCookieName = "warpbox_session"

type authPageData struct {
	Mode       string
	Token      string
	Email      string
	IsReset    bool
	Error      string
	ReturnPath string
}

func (a *App) Register(w http.ResponseWriter, r *http.Request) {
	available, err := a.authService.BootstrapAvailable()
	if err != nil {
		http.Error(w, "unable to check registration", http.StatusInternalServerError)
		return
	}
	if !available {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}
	a.renderAuth(w, r, http.StatusOK, authPageData{Mode: "register"})
}

func (a *App) RegisterPost(w http.ResponseWriter, r *http.Request) {
	if !a.rateLimiter.Allow("register:"+uploadClientIP(r), 10, time.Minute, time.Now().UTC()) {
		a.logger.Warn("registration rate limited", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4291)...)
		a.renderAuth(w, r, http.StatusTooManyRequests, authPageData{Mode: "register", Error: "Too many registration attempts."})
		return
	}
	if err := r.ParseForm(); err != nil {
		a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "register", Error: "Unable to read form."})
		return
	}
	user, err := a.authService.CreateBootstrapUser(r.FormValue("username"), r.FormValue("email"), r.FormValue("password"))
	if err != nil {
		a.logger.Warn("bootstrap registration failed", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4400, "email", r.FormValue("email"), "error", err.Error())...)
		a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "register", Error: err.Error()})
		return
	}
	a.logger.Info("first admin created", withRequestLogAttrs(r, "source", "auth", "severity", "user_activity", "code", 2401, "user_id", user.ID)...)
	a.loginAndRedirect(w, r, user.Email, r.FormValue("password"), "/app")
}

func (a *App) Login(w http.ResponseWriter, r *http.Request) {
	if _, ok := a.currentUser(r); ok {
		http.Redirect(w, r, "/app", http.StatusSeeOther)
		return
	}
	a.logger.Info("login page viewed", withRequestLogAttrs(r, "source", "page", "severity", "user_activity", "code", 2503, "actor", "anonymous")...)
	a.renderAuth(w, r, http.StatusOK, authPageData{Mode: "login", ReturnPath: r.URL.Query().Get("next")})
}

func (a *App) LoginPost(w http.ResponseWriter, r *http.Request) {
	if !a.rateLimiter.Allow("login:"+uploadClientIP(r), 10, time.Minute, time.Now().UTC()) {
		a.logger.Warn("login rate limited", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4292, "email", r.FormValue("email"))...)
		a.renderAuth(w, r, http.StatusTooManyRequests, authPageData{Mode: "login", Error: "Too many login attempts."})
		return
	}
	if err := r.ParseForm(); err != nil {
		a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "login", Error: "Unable to read form."})
		return
	}
	next := r.FormValue("next")
	if next == "" {
		next = "/app"
	}
	user, token, err := a.authService.Login(r.FormValue("email"), r.FormValue("password"))
	if err != nil {
		a.logger.Warn("login failed", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4401, "email", r.FormValue("email"))...)
		a.recordLoginAbuse(r, services.AbuseKindUserLogin, "user login failed")
		a.renderAuth(w, r, http.StatusUnauthorized, authPageData{Mode: "login", Error: "Invalid email or password.", ReturnPath: next})
		return
	}
	a.setUserSessionCookie(w, r, token)
	a.logger.Info("user login", withRequestLogAttrs(r, "source", "auth", "severity", "user_activity", "code", 2402, "user_id", user.ID)...)
	http.Redirect(w, r, safeReturnPath(next), http.StatusSeeOther)
}

func (a *App) Logout(w http.ResponseWriter, r *http.Request) {
	if !a.validateCSRF(w, r) {
		return
	}
	if user, ok := a.currentUser(r); ok {
		a.logger.Info("user logout", withRequestLogAttrs(r, "source", "auth", "severity", "user_activity", "code", 2405, "user_id", user.ID)...)
	}
	if cookie, err := r.Cookie(userSessionCookieName); err == nil {
		_ = a.authService.Logout(cookie.Value)
	}
	a.clearUserSessionCookie(w)
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

func (a *App) Invite(w http.ResponseWriter, r *http.Request) {
	invite, err := a.authService.InviteByToken(r.PathValue("token"))
	if err != nil || invite.UsedAt != nil || time.Now().UTC().After(invite.ExpiresAt) {
		a.renderAuth(w, r, http.StatusNotFound, authPageData{Mode: "invite", Error: "This invite is invalid or expired."})
		return
	}
	a.logger.Info("invite page viewed", withRequestLogAttrs(r, "source", "page", "severity", "user_activity", "code", 2504, "invite_email", invite.Email, "reset", invite.UserID != "")...)
	a.renderAuth(w, r, http.StatusOK, authPageData{Mode: "invite", Token: r.PathValue("token"), Email: invite.Email, IsReset: invite.UserID != ""})
}

func (a *App) InvitePost(w http.ResponseWriter, r *http.Request) {
	token := r.PathValue("token")
	invite, err := a.authService.InviteByToken(token)
	if err != nil {
		a.logger.Warn("invite accept invalid", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4404)...)
		a.renderAuth(w, r, http.StatusNotFound, authPageData{Mode: "invite", Error: "This invite is invalid or expired."})
		return
	}
	if err := r.ParseForm(); err != nil {
		a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "invite", Token: token, Email: invite.Email, IsReset: invite.UserID != "", Error: "Unable to read form."})
		return
	}
	user, err := a.authService.AcceptInvite(token, r.FormValue("username"), r.FormValue("password"))
	if err != nil {
		a.logger.Warn("invite accept failed", withRequestLogAttrs(r, "source", "auth", "severity", "warn", "code", 4405, "invite_email", invite.Email, "error", err.Error())...)
		a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "invite", Token: token, Email: invite.Email, IsReset: invite.UserID != "", Error: err.Error()})
		return
	}
	a.logger.Info("invite accepted", withRequestLogAttrs(r, "source", "auth", "severity", "user_activity", "code", 2403, "user_id", user.ID, "invite_email", invite.Email)...)
	a.loginAndRedirect(w, r, user.Email, r.FormValue("password"), "/app")
}

type apiTokenView struct {
	ID         string
	Name       string
	CreatedAt  string
	LastUsedAt string
}

type accountData struct {
	ID       string
	Email    string
	Role     string
	Tokens   []apiTokenView
	NewToken string
	Error    string
}

func (a *App) AccountSettings(w http.ResponseWriter, r *http.Request) {
	user, ok := a.requireUser(w, r)
	if !ok {
		return
	}
	a.logger.Info("account settings viewed", withRequestLogAttrs(r, "source", "page", "severity", "user_activity", "code", 2505, "user_id", user.ID)...)
	a.renderAccount(w, r, http.StatusOK, user, accountData{})
}

// CreateUserToken mints a new personal access token and renders the account
// page with the one-time plaintext shown. The secret is never recoverable after
// this response.
func (a *App) CreateUserToken(w http.ResponseWriter, r *http.Request) {
	user, ok := a.requireUser(w, r)
	if !ok || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		a.renderAccount(w, r, http.StatusBadRequest, user, accountData{Error: "Unable to read form."})
		return
	}
	result, err := a.authService.CreateAPIToken(user.ID, r.FormValue("name"))
	if err != nil {
		a.logger.Warn("api token create failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4420, "user_id", user.ID, "error", err.Error())...)
		a.renderAccount(w, r, http.StatusBadRequest, user, accountData{Error: "Could not create token."})
		return
	}
	a.logger.Info("api token created", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2420, "user_id", user.ID, "token_id", result.Token.ID)...)
	a.renderAccount(w, r, http.StatusOK, user, accountData{NewToken: result.Plaintext})
}

func (a *App) DeleteUserToken(w http.ResponseWriter, r *http.Request) {
	user, ok := a.requireUser(w, r)
	if !ok || !a.validateCSRF(w, r) {
		return
	}
	if err := a.authService.DeleteAPIToken(user.ID, r.PathValue("tokenID")); err != nil {
		a.logger.Warn("api token delete failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4421, "user_id", user.ID, "error", err.Error())...)
	} else {
		a.logger.Info("api token deleted", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2421, "user_id", user.ID, "token_id", r.PathValue("tokenID"))...)
	}
	http.Redirect(w, r, "/account/settings", http.StatusSeeOther)
}

func (a *App) renderAccount(w http.ResponseWriter, r *http.Request, status int, user services.User, data accountData) {
	tokens, err := a.authService.ListAPITokens(user.ID)
	if err != nil {
		http.Error(w, "unable to load tokens", http.StatusInternalServerError)
		return
	}
	views := make([]apiTokenView, 0, len(tokens))
	for _, token := range tokens {
		lastUsed := "Never"
		if token.LastUsedAt != nil {
			lastUsed = token.LastUsedAt.Format("Jan 2, 2006 15:04")
		}
		views = append(views, apiTokenView{
			ID:         token.ID,
			Name:       token.Name,
			CreatedAt:  token.CreatedAt.Format("Jan 2, 2006"),
			LastUsedAt: lastUsed,
		})
	}
	data.ID = user.ID
	data.Email = user.Email
	data.Role = user.Role
	data.Tokens = views

	a.renderPage(w, r, status, "account.html", web.PageData{
		Title:       "Account settings",
		Description: "Manage your Warpbox account.",
		CurrentUser: a.authService.PublicUser(user),
		Data:        data,
	})
}

func (a *App) ChangePassword(w http.ResponseWriter, r *http.Request) {
	user, ok := a.requireUser(w, r)
	if !ok || !a.validateCSRF(w, r) {
		return
	}
	if err := r.ParseForm(); err != nil {
		http.Redirect(w, r, "/account/settings", http.StatusSeeOther)
		return
	}
	if !services.VerifyPasswordHash(user.PasswordHash, r.FormValue("current_password")) {
		a.logger.Warn("password change failed current password", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4422, "user_id", user.ID)...)
		http.Redirect(w, r, "/account/settings", http.StatusSeeOther)
		return
	}
	if err := a.authService.SetPassword(user.ID, r.FormValue("new_password")); err != nil {
		a.logger.Warn("password change failed", withRequestLogAttrs(r, "source", "user_activity", "severity", "warn", "code", 4423, "user_id", user.ID, "error", err.Error())...)
		http.Redirect(w, r, "/account/settings", http.StatusSeeOther)
		return
	}
	a.logger.Info("password changed", withRequestLogAttrs(r, "source", "user_activity", "severity", "user_activity", "code", 2422, "user_id", user.ID)...)
	http.Redirect(w, r, "/account/settings", http.StatusSeeOther)
}

func (a *App) renderAuth(w http.ResponseWriter, r *http.Request, status int, data authPageData) {
	a.renderPage(w, r, status, "auth.html", web.PageData{
		Title:       "Account",
		Description: "Sign in to Warpbox.",
		Data:        data,
	})
}

func (a *App) loginAndRedirect(w http.ResponseWriter, r *http.Request, email, password, path string) {
	_, token, err := a.authService.Login(email, password)
	if err != nil {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}
	a.setUserSessionCookie(w, r, token)
	http.Redirect(w, r, path, http.StatusSeeOther)
}

func (a *App) currentUser(r *http.Request) (services.User, bool) {
	user, ok, _ := a.currentUserWithAuthError(r)
	return user, ok
}

func (a *App) currentUserWithAuthError(r *http.Request) (services.User, bool, error) {
	// Personal access tokens via Authorization: Bearer act as their owning user.
	// A bearer header is never set by browsers cross-site, so this path is not
	// subject to CSRF and intentionally bypasses the session cookie.
	if header := r.Header.Get("Authorization"); header != "" {
		if raw, ok := strings.CutPrefix(header, "Bearer "); ok {
			user, err := a.authService.UserForAPIToken(raw)
			if err != nil {
				return services.User{}, false, err
			}
			return user, true, nil
		}
	}
	cookie, err := r.Cookie(userSessionCookieName)
	if err != nil {
		return services.User{}, false, nil
	}
	user, _, err := a.authService.UserForSession(cookie.Value)
	if err != nil {
		return services.User{}, false, nil
	}
	return user, true, nil
}

func (a *App) requireUser(w http.ResponseWriter, r *http.Request) (services.User, bool) {
	user, ok := a.currentUser(r)
	if ok {
		return user, true
	}
	http.Redirect(w, r, "/login?next="+r.URL.Path, http.StatusSeeOther)
	return services.User{}, false
}

func (a *App) setUserSessionCookie(w http.ResponseWriter, r *http.Request, token string) {
	http.SetCookie(w, &http.Cookie{
		Name:     userSessionCookieName,
		Value:    token,
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		Secure:   r.TLS != nil,
		Expires:  time.Now().Add(30 * 24 * time.Hour),
	})
}

func (a *App) clearUserSessionCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name:     userSessionCookieName,
		Value:    "",
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		MaxAge:   -1,
	})
}

func safeReturnPath(path string) string {
	if path == "" || path[0] != '/' || len(path) > 1 && path[1] == '/' {
		return "/app"
	}
	return path
}
