package api

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"log/slog"
	"net/http"

	"github.com/go-chi/chi/v5"

	"github.com/alexei/docker-watcher/internal/auth"
	"github.com/alexei/docker-watcher/internal/crypto"
	"github.com/alexei/docker-watcher/internal/store"
)

// rateLimitedLogin wraps the login handler with per-IP rate limiting.
func (s *Server) rateLimitedLogin(rl *rateLimiter) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ip := r.RemoteAddr
		if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
			ip = fwd
		}
		if !rl.allow(ip) {
			respondError(w, http.StatusTooManyRequests, "too many login attempts, try again later")
			return
		}
		s.login(w, r)
	}
}

// authMode handles GET /api/auth/mode — public endpoint returning the auth mode.
func (s *Server) authMode(w http.ResponseWriter, r *http.Request) {
	as, err := s.store.GetAuthSettings()
	if err != nil {
		respondJSON(w, http.StatusOK, map[string]string{"auth_mode": "local"})
		return
	}
	respondJSON(w, http.StatusOK, map[string]string{"auth_mode": as.AuthMode})
}

// login handles POST /api/auth/login.
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
	var req auth.LoginRequest
	if !decodeJSON(w, r, &req) {
		return
	}

	if req.Username == "" || req.Password == "" {
		respondError(w, http.StatusBadRequest, "username and password are required")
		return
	}

	user, err := s.store.GetUserByUsername(req.Username)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondError(w, http.StatusUnauthorized, "invalid credentials")
			return
		}
		slog.Error("failed to get user", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	if err := auth.CheckPassword(user.PasswordHash, req.Password); err != nil {
		respondError(w, http.StatusUnauthorized, "invalid credentials")
		return
	}

	token, err := s.localAuth.GenerateToken(auth.Claims{
		UserID:   user.ID,
		Username: user.Username,
		Role:     user.Role,
	})
	if err != nil {
		slog.Error("failed to generate token", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	respondJSON(w, http.StatusOK, token)
}

// currentUser handles GET /api/auth/me — returns the authenticated user.
func (s *Server) currentUser(w http.ResponseWriter, r *http.Request) {
	claims, ok := auth.ClaimsFromContext(r.Context())
	if !ok {
		respondError(w, http.StatusUnauthorized, "not authenticated")
		return
	}

	user, err := s.store.GetUserByID(claims.UserID)
	if err != nil {
		slog.Error("failed to get user", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	respondJSON(w, http.StatusOK, user)
}

// oidcLogin handles GET /api/auth/oidc/login — redirects to OIDC provider.
func (s *Server) oidcLogin(w http.ResponseWriter, r *http.Request) {
	if s.oidcProvider == nil {
		respondError(w, http.StatusBadRequest, "OIDC is not configured")
		return
	}

	// Generate random state.
	stateBytes := make([]byte, 16)
	if _, err := rand.Read(stateBytes); err != nil {
		respondError(w, http.StatusInternalServerError, "failed to generate state")
		return
	}
	state := hex.EncodeToString(stateBytes)

	// Store state in a short-lived cookie for validation on callback.
	http.SetCookie(w, &http.Cookie{
		Name:     "oidc_state",
		Value:    state,
		Path:     "/api/auth/oidc",
		MaxAge:   300, // 5 minutes
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteLaxMode,
	})

	http.Redirect(w, r, s.oidcProvider.AuthCodeURL(state), http.StatusFound)
}

// oidcCallback handles GET /api/auth/oidc/callback — exchanges code for tokens.
func (s *Server) oidcCallback(w http.ResponseWriter, r *http.Request) {
	if s.oidcProvider == nil {
		respondError(w, http.StatusBadRequest, "OIDC is not configured")
		return
	}

	// Validate state parameter.
	stateCookie, err := r.Cookie("oidc_state")
	if err != nil || stateCookie.Value == "" {
		respondError(w, http.StatusBadRequest, "missing OIDC state")
		return
	}

	if r.URL.Query().Get("state") != stateCookie.Value {
		respondError(w, http.StatusBadRequest, "invalid OIDC state")
		return
	}

	// Clear the state cookie.
	http.SetCookie(w, &http.Cookie{
		Name:   "oidc_state",
		Value:  "",
		Path:   "/api/auth/oidc",
		MaxAge: -1,
	})

	code := r.URL.Query().Get("code")
	if code == "" {
		respondError(w, http.StatusBadRequest, "missing authorization code")
		return
	}

	userInfo, err := s.oidcProvider.Exchange(r.Context(), code)
	if err != nil {
		slog.Error("OIDC exchange failed", "error", err)
		respondError(w, http.StatusInternalServerError, "OIDC authentication failed")
		return
	}

	// Find or create local user linked to the OIDC identity.
	username := userInfo.Username
	if username == "" {
		username = userInfo.Email
	}
	if username == "" {
		username = userInfo.Subject
	}

	user, err := s.store.GetUserByUsername(username)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			// Auto-create user from OIDC.
			user, err = s.store.CreateUser(store.User{
				Username: username,
				Email:    userInfo.Email,
				Role:     "viewer", // OIDC users default to viewer; admin promotes via settings
			})
			if err != nil {
				slog.Error("failed to create user", "error", err)
				respondError(w, http.StatusInternalServerError, "internal server error")
				return
			}
		} else {
			slog.Error("failed to get user", "error", err)
			respondError(w, http.StatusInternalServerError, "internal server error")
			return
		}
	}

	token, err := s.localAuth.GenerateToken(auth.Claims{
		UserID:   user.ID,
		Username: user.Username,
		Role:     user.Role,
	})
	if err != nil {
		slog.Error("failed to generate token", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	// Pass token via short-lived httpOnly cookie. The frontend reads it via
	// a dedicated /api/auth/oidc/token endpoint and then the cookie is cleared.
	http.SetCookie(w, &http.Cookie{
		Name:     "auth_token",
		Value:    token.Token,
		Path:     "/api/auth/oidc",
		MaxAge:   60,
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteLaxMode,
	})
	http.Redirect(w, r, "/?oidc=success", http.StatusFound)
}

// oidcExchangeToken handles POST /api/auth/oidc/token — exchanges the httpOnly cookie for a JSON token.
func (s *Server) oidcExchangeToken(w http.ResponseWriter, r *http.Request) {
	cookie, err := r.Cookie("auth_token")
	if err != nil || cookie.Value == "" {
		respondError(w, http.StatusUnauthorized, "no OIDC token available")
		return
	}

	// Clear the cookie immediately.
	http.SetCookie(w, &http.Cookie{
		Name:     "auth_token",
		Value:    "",
		Path:     "/api/auth/oidc",
		MaxAge:   -1,
		HttpOnly: true,
		Secure:   true,
		SameSite: http.SameSiteLaxMode,
	})

	respondJSON(w, http.StatusOK, map[string]string{"token": cookie.Value})
}

// getAuthSettings handles GET /api/auth/settings.
func (s *Server) getAuthSettings(w http.ResponseWriter, r *http.Request) {
	as, err := s.store.GetAuthSettings()
	if err != nil {
		slog.Error("failed to get auth settings", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}
	// Mask the client secret for the response.
	if as.OIDCClientSecret != "" {
		as.OIDCClientSecret = "********"
	}
	respondJSON(w, http.StatusOK, as)
}

// updateAuthSettings handles PUT /api/auth/settings.
func (s *Server) updateAuthSettings(w http.ResponseWriter, r *http.Request) {
	var req store.AuthSettings
	if !decodeJSON(w, r, &req) {
		return
	}

	if req.AuthMode != "local" && req.AuthMode != "oidc" {
		respondError(w, http.StatusBadRequest, "auth_mode must be 'local' or 'oidc'")
		return
	}

	// If client secret is masked, preserve the existing encrypted value.
	if req.OIDCClientSecret == "********" || req.OIDCClientSecret == "" {
		existing, err := s.store.GetAuthSettings()
		if err == nil {
			req.OIDCClientSecret = existing.OIDCClientSecret
		}
	} else {
		// Encrypt the new client secret before storage.
		encrypted, err := crypto.Encrypt(s.encKey, req.OIDCClientSecret)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "failed to encrypt OIDC client secret")
			return
		}
		// Keep plaintext for OIDC init below, store encrypted.
		plaintextSecret := req.OIDCClientSecret
		req.OIDCClientSecret = encrypted
		defer func() { req.OIDCClientSecret = plaintextSecret }()
	}

	if err := s.store.UpdateAuthSettings(req); err != nil {
		slog.Error("failed to update auth settings", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	// Re-initialize OIDC provider if mode is oidc and config is present.
	if req.AuthMode == "oidc" && req.OIDCIssuerURL != "" && req.OIDCClientID != "" {
		s.initOIDCProvider(r.Context(), req)
	}

	respondJSON(w, http.StatusOK, req)
}

// listUsers handles GET /api/auth/users.
func (s *Server) listUsers(w http.ResponseWriter, r *http.Request) {
	users, err := s.store.GetAllUsers()
	if err != nil {
		slog.Error("failed to list users", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}
	respondJSON(w, http.StatusOK, users)
}

// createUser handles POST /api/auth/users.
func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
	var req struct {
		Username string `json:"username"`
		Password string `json:"password"`
		Email    string `json:"email"`
		Role     string `json:"role"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}

	if req.Username == "" || req.Password == "" {
		respondError(w, http.StatusBadRequest, "username and password are required")
		return
	}
	if err := validatePassword(req.Password); err != nil {
		respondError(w, http.StatusBadRequest, err.Error())
		return
	}

	if req.Role == "" {
		req.Role = "viewer"
	}
	if req.Role != "admin" && req.Role != "viewer" {
		respondError(w, http.StatusBadRequest, "role must be 'admin' or 'viewer'")
		return
	}

	hash, err := auth.HashPassword(req.Password)
	if err != nil {
		slog.Error("failed to hash password", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	user, err := s.store.CreateUser(store.User{
		Username:     req.Username,
		PasswordHash: hash,
		Email:        req.Email,
		Role:         req.Role,
	})
	if err != nil {
		slog.Error("failed to create user", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	respondJSON(w, http.StatusCreated, user)
}

// deleteUser handles DELETE /api/auth/users/{uid}.
func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "uid")

	// Prevent deleting your own account.
	claims, ok := auth.ClaimsFromContext(r.Context())
	if ok && claims.UserID == id {
		respondError(w, http.StatusBadRequest, "cannot delete your own account")
		return
	}

	// Prevent deleting the last admin.
	user, err := s.store.GetUserByID(id)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "user")
			return
		}
		slog.Error("failed to get user", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	if user.Role == "admin" {
		users, err := s.store.GetAllUsers()
		if err == nil {
			adminCount := 0
			for _, u := range users {
				if u.Role == "admin" {
					adminCount++
				}
			}
			if adminCount <= 1 {
				respondError(w, http.StatusBadRequest, "cannot delete the last admin user")
				return
			}
		}
	}

	if err := s.store.DeleteUser(id); err != nil {
		slog.Error("failed to delete user", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"deleted": id})
}

// validatePassword checks that a password meets minimum complexity requirements.
func validatePassword(password string) error {
	if len(password) < 8 {
		return fmt.Errorf("password must be at least 8 characters long")
	}
	return nil
}

// logout handles POST /api/auth/logout — revokes the current token.
func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
	tokenStr := auth.ExtractToken(r)
	if tokenStr != "" {
		s.localAuth.RevokeToken(tokenStr)
	}
	respondJSON(w, http.StatusOK, map[string]string{"status": "logged out"})
}

// changePassword handles PUT /api/auth/users/{uid}/password.
func (s *Server) changePassword(w http.ResponseWriter, r *http.Request) {
	uid := chi.URLParam(r, "uid")

	var req struct {
		Password string `json:"password"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}

	if req.Password == "" {
		respondError(w, http.StatusBadRequest, "password is required")
		return
	}
	if err := validatePassword(req.Password); err != nil {
		respondError(w, http.StatusBadRequest, err.Error())
		return
	}

	hash, err := auth.HashPassword(req.Password)
	if err != nil {
		slog.Error("failed to hash password", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	if err := s.store.UpdateUserPassword(uid, hash); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "user")
			return
		}
		slog.Error("failed to update password", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	respondJSON(w, http.StatusOK, map[string]string{"status": "password updated"})
}

// updateUser handles PUT /api/auth/users/{uid}.
func (s *Server) updateUser(w http.ResponseWriter, r *http.Request) {
	uid := chi.URLParam(r, "uid")

	var req struct {
		Email string `json:"email"`
		Role  string `json:"role"`
	}
	if !decodeJSON(w, r, &req) {
		return
	}

	if req.Role != "" && req.Role != "admin" && req.Role != "viewer" {
		respondError(w, http.StatusBadRequest, "role must be 'admin' or 'viewer'")
		return
	}

	existing, err := s.store.GetUserByID(uid)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			respondNotFound(w, "user")
			return
		}
		slog.Error("failed to get user", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	// If demoting from admin, check we're not removing the last admin.
	if existing.Role == "admin" && req.Role == "viewer" {
		users, err := s.store.GetAllUsers()
		if err == nil {
			adminCount := 0
			for _, u := range users {
				if u.Role == "admin" {
					adminCount++
				}
			}
			if adminCount <= 1 {
				respondError(w, http.StatusBadRequest, "cannot demote the last admin user")
				return
			}
		}
	}

	if req.Email != "" {
		existing.Email = req.Email
	}
	if req.Role != "" {
		existing.Role = req.Role
	}

	if err := s.store.UpdateUser(existing); err != nil {
		slog.Error("failed to update user", "error", err)
		respondError(w, http.StatusInternalServerError, "internal server error")
		return
	}

	respondJSON(w, http.StatusOK, existing)
}
