package api

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/coreos/go-oidc/v3/oidc"
	"golang.org/x/crypto/bcrypt"

	"github.com/Temikus/denkeeper/internal/config"
)

// rateBucket tracks login attempts for a single IP.
type rateBucket struct {
	count   int
	resetAt time.Time
}

// loginRateLimiter provides IP-based rate limiting for password login.
type loginRateLimiter struct {
	mu       sync.Mutex
	buckets  map[string]*rateBucket
	limit    int
	window   time.Duration
	stopOnce sync.Once
	stopCh   chan struct{}
}

func newLoginRateLimiter(limit int, window time.Duration) *loginRateLimiter {
	rl := &loginRateLimiter{
		buckets: make(map[string]*rateBucket),
		limit:   limit,
		window:  window,
		stopCh:  make(chan struct{}),
	}
	go rl.cleanup()
	return rl
}

// allow returns true if the IP has not exceeded the rate limit.
// A limit of 0 disables rate limiting (always allows).
func (rl *loginRateLimiter) allow(ip string) bool {
	if rl.limit <= 0 {
		return true
	}

	rl.mu.Lock()
	defer rl.mu.Unlock()

	b, ok := rl.buckets[ip]
	if !ok || time.Now().After(b.resetAt) {
		rl.buckets[ip] = &rateBucket{count: 1, resetAt: time.Now().Add(rl.window)}
		return true
	}
	b.count++
	return b.count <= rl.limit
}

// retryAfter returns seconds until the rate limit window resets for this IP.
func (rl *loginRateLimiter) retryAfter(ip string) int {
	rl.mu.Lock()
	defer rl.mu.Unlock()

	b, ok := rl.buckets[ip]
	if !ok {
		return 0
	}
	secs := int(time.Until(b.resetAt).Seconds())
	if secs < 1 {
		return 1
	}
	return secs
}

func (rl *loginRateLimiter) cleanup() {
	ticker := time.NewTicker(30 * time.Minute)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			rl.mu.Lock()
			now := time.Now()
			for ip, b := range rl.buckets {
				if now.After(b.resetAt) {
					delete(rl.buckets, ip)
				}
			}
			rl.mu.Unlock()
		case <-rl.stopCh:
			return
		}
	}
}

func (rl *loginRateLimiter) stop() {
	rl.stopOnce.Do(func() { close(rl.stopCh) })
}

// handleAuthConfig godoc
// @Summary Get auth configuration
// @Description Return which authentication methods (password, OIDC) are enabled and the preferred login method.
// @Tags auth
// @Produce json
// @Success 200 {object} map[string]any
// @Router /auth/config [get]
func (s *Server) handleAuthConfig(w http.ResponseWriter, _ *http.Request) {
	pref := "auto"
	if s.deps.Config != nil && s.deps.Config.API.Auth.PreferredLoginMethod != "" {
		pref = s.deps.Config.API.Auth.PreferredLoginMethod
	}
	resp := map[string]any{
		"password_enabled":       s.passwordHash != "",
		"oidc_enabled":           s.oidcProvider != nil,
		"preferred_login_method": pref,
	}
	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp) //nolint:errcheck
}

// handlePasswordLogin godoc
// @Summary Log in with password
// @Description Authenticate with a bcrypt-hashed password and create a session cookie.
// @Tags auth
// @Accept json
// @Produce json
// @Param body body object true "Login credentials" SchemaExample({"password": "string"})
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 429 {object} map[string]string
// @Router /auth/login [post]
func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
	if s.passwordHash == "" {
		http.Error(w, `{"error":"password login not configured"}`, http.StatusNotFound)
		return
	}

	// Rate limiting.
	ip := clientIP(r)
	if !s.loginLimiter.allow(ip) {
		retryAfter := s.loginLimiter.retryAfter(ip)
		w.Header().Set("Retry-After", fmt.Sprintf("%d", retryAfter))
		s.logger.Warn("login rate limited", "ip", ip)
		http.Error(w, `{"error":"too many login attempts"}`, http.StatusTooManyRequests)
		return
	}

	// CSRF: verify Origin header.
	if origin := r.Header.Get("Origin"); origin != "" {
		if !s.isValidOrigin(origin) {
			s.logger.Warn("login CSRF check failed", "ip", ip, "origin", origin)
			http.Error(w, `{"error":"origin not allowed"}`, http.StatusForbidden)
			return
		}
	}

	var input struct {
		Password string `json:"password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		http.Error(w, `{"error":"invalid request body"}`, http.StatusBadRequest)
		return
	}

	if err := bcrypt.CompareHashAndPassword([]byte(s.passwordHash), []byte(input.Password)); err != nil {
		s.logger.Warn("failed login attempt", "ip", ip)
		http.Error(w, `{"error":"invalid password"}`, http.StatusUnauthorized)
		return
	}

	if s.sessions == nil {
		http.Error(w, `{"error":"session manager not configured"}`, http.StatusInternalServerError)
		return
	}

	sess := Session{
		Email:  "admin",
		Scopes: adminScopes(),
	}
	if err := s.sessions.CreateWithRequest(w, r, sess); err != nil {
		s.logger.Error("creating session", "error", err)
		http.Error(w, `{"error":"failed to create session"}`, http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{"authenticated": true, "email": sess.Email}) //nolint:errcheck
}

// handleLogout godoc
// @Summary Log out
// @Description Clear the session cookie and end the current session.
// @Tags auth
// @Produce json
// @Success 200 {object} map[string]any
// @Router /auth/logout [post]
func (s *Server) handleLogout(w http.ResponseWriter, _ *http.Request) {
	if s.sessions != nil {
		s.sessions.Clear(w)
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write([]byte(`{"ok":true}`)) //nolint:errcheck
}

// handleSessionCheck godoc
// @Summary Check session validity
// @Description Verify the current session cookie and return authentication status.
// @Tags auth
// @Produce json
// @Success 200 {object} map[string]any
// @Router /auth/session [get]
func (s *Server) handleSessionCheck(w http.ResponseWriter, r *http.Request) {
	if s.sessions == nil {
		json.NewEncoder(w).Encode(map[string]any{"authenticated": false}) //nolint:errcheck
		return
	}

	sess, err := s.sessions.Read(r)
	if err != nil {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]any{"authenticated": false}) //nolint:errcheck
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{"authenticated": true, "email": sess.Email}) //nolint:errcheck
}

// adminScopes returns the full set of scopes for a dashboard password/OIDC login.
func adminScopes() []string {
	return []string{
		"admin", "chat", "sessions:read", "sessions:write", "costs:read",
		"skills:read", "skills:write", "schedules:read", "schedules:write",
		"approvals:read", "approvals:write", "tools:read", "tools:write",
		"browser:read", "browser:write", "kv:read", "kv:write",
		"agents:read", "agents:write",
		"channels:read", "channels:write", "audit:read",
	}
}

// clientIP extracts the client IP from the request.
func clientIP(r *http.Request) string {
	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
		if i := strings.IndexByte(xff, ','); i > 0 {
			return strings.TrimSpace(xff[:i])
		}
		return strings.TrimSpace(xff)
	}
	if xri := r.Header.Get("X-Real-Ip"); xri != "" {
		return xri
	}
	// Strip port from RemoteAddr.
	ip := r.RemoteAddr
	if i := strings.LastIndex(ip, ":"); i > 0 {
		ip = ip[:i]
	}
	return ip
}

// handleListSessions godoc
// @Summary List active sessions
// @Description Return all active sessions for the authenticated user, including the current session ID. API-key callers (no session cookie) receive an empty list.
// @Tags auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]any
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/sessions [get]
func (s *Server) handleListSessions(w http.ResponseWriter, r *http.Request) {
	if s.sessions == nil || s.sessions.Store == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "server-tracked sessions not enabled"})
		return
	}

	sess, err := s.sessions.Read(r)
	if err != nil {
		// Authenticated via API key (no session cookie). API keys aren't
		// browser sessions, so there's nothing to list — return empty rather
		// than 401, which the web client treats as "log out".
		writeJSON(w, http.StatusOK, map[string]any{
			"sessions":           []any{},
			"current_session_id": nil,
		})
		return
	}

	records, err := s.sessions.Store.ListByEmail(r.Context(), sess.Email)
	if err != nil {
		s.logger.Error("listing sessions", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to list sessions"})
		return
	}

	currentID := s.sessions.ReadID(r)
	writeJSON(w, http.StatusOK, map[string]any{
		"sessions":           records,
		"current_session_id": currentID,
	})
}

// handleRevokeSession godoc
// @Summary Revoke a session
// @Description Revoke a single active session by its ID.
// @Tags auth
// @Security BearerAuth
// @Param id path string true "Session ID"
// @Success 204
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/sessions/{id} [delete]
func (s *Server) handleRevokeSession(w http.ResponseWriter, r *http.Request) {
	if s.sessions == nil || s.sessions.Store == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "server-tracked sessions not enabled"})
		return
	}

	id := r.PathValue("id")
	if id == "" {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "session ID required"})
		return
	}

	if err := s.sessions.Store.Delete(r.Context(), id); err != nil {
		s.logger.Error("revoking session", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to revoke session"})
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// handleRevokeAllSessions godoc
// @Summary Revoke all sessions
// @Description Revoke all active sessions for the authenticated user. API-key callers (no session cookie) receive {"revoked": 0}.
// @Tags auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]any
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/sessions [delete]
func (s *Server) handleRevokeAllSessions(w http.ResponseWriter, r *http.Request) {
	if s.sessions == nil || s.sessions.Store == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "server-tracked sessions not enabled"})
		return
	}

	sess, err := s.sessions.Read(r)
	if err != nil {
		// API-key callers have no associated browser session, so there is
		// nothing to revoke.  Return 200 with revoked:0 (not an error) so
		// the web UI can treat every auth method uniformly.
		writeJSON(w, http.StatusOK, map[string]any{"revoked": 0})
		return
	}

	count, err := s.sessions.Store.DeleteAllByEmail(r.Context(), sess.Email)
	if err != nil {
		s.logger.Error("revoking all sessions", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to revoke sessions"})
		return
	}

	writeJSON(w, http.StatusOK, map[string]any{"revoked": count})
}

// handleAuthStatus godoc
// @Summary Get auth status
// @Description Return a detailed summary of auth configuration including password, OIDC, session, and API key state.
// @Tags auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]any
// @Router /auth/status [get]
func (s *Server) handleAuthStatus(w http.ResponseWriter, r *http.Request) {
	resp := map[string]any{
		"password_enabled":   s.passwordHash != "",
		"oidc_enabled":       s.oidcProvider != nil,
		"sessions_trackable": s.sessions != nil && s.sessions.Store != nil,
	}

	if s.sessions != nil && s.sessions.Store != nil {
		count, err := s.sessions.Store.Count(r.Context())
		if err == nil {
			resp["active_session_count"] = count
		}
	}

	// OIDC details.
	if s.deps.Config != nil {
		resp["oidc_issuer"] = s.deps.Config.API.Auth.OIDC.Issuer
		resp["oidc_allowed_emails"] = s.deps.Config.API.Auth.OIDC.AllowedEmails

		pref := s.deps.Config.API.Auth.PreferredLoginMethod
		if pref == "" {
			pref = "auto"
		}
		resp["preferred_login_method"] = pref
	}

	// API key count.
	if s.deps.KeyStore != nil {
		keys, err := s.deps.KeyStore.List(r.Context())
		if err == nil {
			resp["api_keys_count"] = len(keys)
		}
	}

	writeJSON(w, http.StatusOK, resp)
}

// handlePasswordChange godoc
// @Summary Change dashboard password
// @Description Verify the current password and replace it with a new one (minimum 8 characters).
// @Tags auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body object true "Password change payload" SchemaExample({"current_password": "string", "new_password": "string"})
// @Success 200 {object} map[string]bool
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 429 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/password [post]
func (s *Server) handlePasswordChange(w http.ResponseWriter, r *http.Request) {
	if s.passwordHash == "" {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "password login not configured"})
		return
	}

	// Rate limiting (reuse login limiter).
	ip := clientIP(r)
	if !s.loginLimiter.allow(ip) {
		retryAfter := s.loginLimiter.retryAfter(ip)
		w.Header().Set("Retry-After", fmt.Sprintf("%d", retryAfter))
		writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": "too many attempts"})
		return
	}

	var input struct {
		CurrentPassword string `json:"current_password"`
		NewPassword     string `json:"new_password"`
	}
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
		return
	}

	if err := bcrypt.CompareHashAndPassword([]byte(s.passwordHash), []byte(input.CurrentPassword)); err != nil {
		writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid current password"})
		return
	}

	if len(input.NewPassword) < 8 {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "new password must be at least 8 characters"})
		return
	}

	hash, err := bcrypt.GenerateFromPassword([]byte(input.NewPassword), s.bcryptCost)
	if err != nil {
		s.logger.Error("hashing new password", "error", err)
		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to hash password"})
		return
	}

	if s.deps.ConfigPath != "" {
		if err := config.UpdateAuthConfig(s.deps.ConfigPath, map[string]any{"password_hash": string(hash)}); err != nil {
			s.logger.Error("persisting password change", "error", err)
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save configuration"})
			return
		}
	}

	s.passwordHash = string(hash)
	writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}

// handleOIDCTest godoc
// @Summary Test OIDC provider
// @Description Perform a fresh OIDC discovery against the configured issuer and return endpoint details.
// @Tags auth
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]any
// @Failure 404 {object} map[string]string
// @Failure 502 {object} map[string]any
// @Router /auth/oidc/test [get]
func (s *Server) handleOIDCTest(w http.ResponseWriter, r *http.Request) {
	if s.oidcProvider == nil || s.deps.Config == nil {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "OIDC not configured"})
		return
	}

	issuer := s.deps.Config.API.Auth.OIDC.Issuer
	if issuer == "" {
		writeJSON(w, http.StatusNotFound, map[string]string{"error": "OIDC issuer not configured"})
		return
	}

	ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
	defer cancel()

	provider, err := oidc.NewProvider(ctx, issuer)
	if err != nil {
		writeJSON(w, http.StatusBadGateway, map[string]any{
			"ok":    false,
			"error": fmt.Sprintf("discovery failed: %v", err),
		})
		return
	}

	endpoint := provider.Endpoint()

	// Extract userinfo_endpoint from discovery document.
	var disc struct {
		UserInfo string `json:"userinfo_endpoint"`
	}
	_ = provider.Claims(&disc)

	writeJSON(w, http.StatusOK, map[string]any{
		"ok":     true,
		"issuer": issuer,
		"endpoints": map[string]string{
			"authorization": endpoint.AuthURL,
			"token":         endpoint.TokenURL,
			"userinfo":      disc.UserInfo,
		},
	})
}

// handleAuthPreferences godoc
// @Summary Set auth preferences
// @Description Update the preferred login method (auto, password, or apikey) and persist to config.
// @Tags auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param body body object true "Preferences payload" SchemaExample({"preferred_login_method": "auto"})
// @Success 200 {object} map[string]bool
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/preferences [post]
func (s *Server) handleAuthPreferences(w http.ResponseWriter, r *http.Request) {
	var input struct {
		PreferredLoginMethod string `json:"preferred_login_method"`
	}
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
		return
	}

	switch input.PreferredLoginMethod {
	case "auto", "password", "apikey":
		// valid
	default:
		writeJSON(w, http.StatusBadRequest, map[string]string{
			"error": "preferred_login_method must be one of: auto, password, apikey",
		})
		return
	}

	if s.deps.ConfigPath != "" {
		if err := config.UpdateAuthConfig(s.deps.ConfigPath, map[string]any{
			"preferred_login_method": input.PreferredLoginMethod,
		}); err != nil {
			s.logger.Error("persisting auth preferences", "error", err)
			writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to save configuration"})
			return
		}
	}

	if s.deps.Config != nil {
		s.deps.Config.API.Auth.PreferredLoginMethod = input.PreferredLoginMethod
	}

	writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}

// isValidOrigin checks if the origin is allowed by CORS config or matches the server host.
func (s *Server) isValidOrigin(origin string) bool {
	for _, o := range s.cfg.CORSOrigins {
		if o == "*" || o == origin {
			return true
		}
	}
	// If no CORS origins are configured, allow same-origin (origin matches listen addr).
	return len(s.cfg.CORSOrigins) == 0
}
