package web

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"golang.org/x/crypto/bcrypt"
)

type session struct {
	expiresAt time.Time
	csrfToken string
}

// loginAttempt tracks failed login attempts for rate limiting.
type loginAttempt struct {
	count    int
	lastFail time.Time
}

const (
	sessionCookieName   = "felhom_session"
	sessionMaxAge       = 7 * 24 * time.Hour
	loginMaxAttempts    = 5
	loginWindowDuration = 1 * time.Minute
)

// effectivePasswordHash returns the active password hash using the priority:
// 1. settings.json → password_hash (customer changed it)
// 2. controller.yaml → web.password_hash (operator provisioned)
// 3. Empty string → no auth required
func (s *Server) effectivePasswordHash() string {
	if s.settings != nil {
		if h := s.settings.GetPasswordHash(); h != "" {
			return h
		}
	}
	return s.cfg.Web.PasswordHash
}

// authEnabled returns true if a password is configured from any source.
func (s *Server) authEnabled() bool {
	return s.effectivePasswordHash() != ""
}

// RequireAuth returns middleware that checks for valid session or shows login.
func (s *Server) RequireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Skip auth if no password is configured
		if !s.authEnabled() {
			if s.isDebug() {
				s.logger.Printf("[DEBUG] [web] auth: no password configured, passing through %s %s", r.Method, r.URL.Path)
			}
			next.ServeHTTP(w, r)
			return
		}

		if r.URL.Path == "/api/health" {
			next.ServeHTTP(w, r)
			return
		}

		if r.URL.Path == "/login" && r.Method == http.MethodPost {
			s.handleLogin(w, r)
			return
		}
		if r.URL.Path == "/login" {
			s.renderLogin(w, "", r.URL.Query().Get("flash"))
			return
		}
		if r.URL.Path == "/logout" {
			s.handleLogout(w, r)
			return
		}

		cookie, err := r.Cookie(sessionCookieName)
		if err != nil || !s.isValidSession(cookie.Value) {
			if s.isDebug() {
				reason := "no cookie"
				if err == nil {
					reason = "invalid/expired session"
				}
				s.logger.Printf("[DEBUG] [web] auth: rejected %s %s from %s (%s)", r.Method, r.URL.Path, r.RemoteAddr, reason)
			}
			if strings.HasPrefix(r.URL.Path, "/api/") {
				s.logger.Printf("[WARN] [api] Unauthorized request to %s from %s", r.URL.Path, r.RemoteAddr)
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusUnauthorized)
				fmt.Fprint(w, `{"ok":false,"error":"authentication required"}`)
				return
			}
			// Redirect to login with ?next= so we can return to the original page
			loginURL := "/login"
			if r.URL.Path != "/" && r.URL.Path != "/dashboard" {
				loginURL = "/login?next=" + url.QueryEscape(r.URL.RequestURI())
			}
			http.Redirect(w, r, loginURL, http.StatusFound)
			return
		}

		if s.isDebug() {
			s.logger.Printf("[DEBUG] [web] auth: valid session for %s %s", r.Method, r.URL.Path)
		}
		next.ServeHTTP(w, r)
	})
}

func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
	_ = r.ParseForm()
	password := r.FormValue("password")
	nextURL := r.FormValue("next")

	if s.isDebug() {
		s.logger.Printf("[DEBUG] [web] login attempt from %s (X-Forwarded-For: %s)", r.RemoteAddr, r.Header.Get("X-Forwarded-For"))
	}

	if password == "" {
		s.renderLogin(w, "Kérjük adja meg a jelszót", "")
		return
	}

	// Rate limit: check failed attempts from this IP
	ip := r.RemoteAddr
	if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
		ip = strings.Split(fwd, ",")[0]
	}
	ip = strings.TrimSpace(ip)

	s.loginAttemptMu.Lock()
	attempt := s.loginAttempts[ip]
	if attempt != nil && time.Since(attempt.lastFail) > loginWindowDuration {
		// Window expired — reset
		attempt = nil
		delete(s.loginAttempts, ip)
	}
	if attempt != nil && attempt.count >= loginMaxAttempts {
		s.loginAttemptMu.Unlock()
		s.logger.Printf("[WARN] [web] Login rate limited for %s (%d attempts)", ip, attempt.count)
		s.renderLogin(w, "Túl sok sikertelen próbálkozás, próbálja újra 1 perc múlva", "")
		return
	}
	s.loginAttemptMu.Unlock()

	effectiveHash := s.effectivePasswordHash()
	if err := bcrypt.CompareHashAndPassword([]byte(effectiveHash), []byte(password)); err != nil {
		s.logger.Printf("[WARN] [web] Failed login from %s", r.RemoteAddr)
		s.loginAttemptMu.Lock()
		if s.loginAttempts[ip] == nil {
			s.loginAttempts[ip] = &loginAttempt{}
		}
		s.loginAttempts[ip].count++
		s.loginAttempts[ip].lastFail = time.Now()
		s.loginAttemptMu.Unlock()
		s.renderLogin(w, "Hibás jelszó", "")
		return
	}

	// Successful login — clear rate limit for this IP
	s.loginAttemptMu.Lock()
	delete(s.loginAttempts, ip)
	s.loginAttemptMu.Unlock()

	if s.isDebug() {
		s.logger.Printf("[DEBUG] [web] login successful from %s, creating session", ip)
	}

	token := s.createSession()
	isSecure := r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
	http.SetCookie(w, &http.Cookie{
		Name:     sessionCookieName,
		Value:    token,
		Path:     "/",
		MaxAge:   int(sessionMaxAge.Seconds()),
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		Secure:   isSecure,
	})

	s.logger.Printf("[INFO] [web] Login from %s", r.RemoteAddr)

	// Redirect to ?next= target if provided, otherwise to dashboard
	redirectTo := "/"
	if nextURL != "" && strings.HasPrefix(nextURL, "/") {
		redirectTo = nextURL
	}
	http.Redirect(w, r, redirectTo, http.StatusFound)
}

func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Redirect(w, r, "/", http.StatusFound)
		return
	}
	if s.isDebug() {
		s.logger.Printf("[DEBUG] [web] logout from %s", r.RemoteAddr)
	}
	if cookie, err := r.Cookie(sessionCookieName); err == nil {
		s.sessionsMu.Lock()
		delete(s.sessions, cookie.Value)
		s.sessionsMu.Unlock()
	}
	s.logger.Printf("[INFO] [web] User logged out from %s", r.RemoteAddr)
	http.SetCookie(w, &http.Cookie{Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1})
	http.Redirect(w, r, "/login", http.StatusFound)
}

func (s *Server) createSession() string {
	b := make([]byte, 32)
	_, _ = rand.Read(b)
	token := hex.EncodeToString(b)

	csrfB := make([]byte, 32)
	_, _ = rand.Read(csrfB)
	csrfToken := hex.EncodeToString(csrfB)

	s.sessionsMu.Lock()
	s.sessions[token] = &session{
		expiresAt: time.Now().Add(sessionMaxAge),
		csrfToken: csrfToken,
	}
	sessionCount := len(s.sessions)
	s.sessionsMu.Unlock()

	if s.isDebug() {
		s.logger.Printf("[DEBUG] [web] session created, expires=%s, active_sessions=%d", time.Now().Add(sessionMaxAge).Format(time.RFC3339), sessionCount)
	}

	return token
}

// csrfTokenForSession returns the CSRF token for the given session cookie value.
// Returns "" if the session is invalid or expired.
func (s *Server) csrfTokenForSession(sessionToken string) string {
	s.sessionsMu.RLock()
	defer s.sessionsMu.RUnlock()
	sess, ok := s.sessions[sessionToken]
	if !ok || time.Now().After(sess.expiresAt) {
		return ""
	}
	return sess.csrfToken
}

func (s *Server) isValidSession(token string) bool {
	s.sessionsMu.RLock()
	defer s.sessionsMu.RUnlock()
	sess, ok := s.sessions[token]
	return ok && time.Now().Before(sess.expiresAt)
}

// invalidateAllSessions clears all sessions, forcing re-login.
// Used after password change.
func (s *Server) invalidateAllSessions() {
	s.sessionsMu.Lock()
	count := len(s.sessions)
	s.sessions = make(map[string]*session)
	s.sessionsMu.Unlock()
	s.logger.Printf("[INFO] [web] All sessions invalidated (cleared %d)", count)
}

func (s *Server) cleanupSessions() {
	ticker := time.NewTicker(15 * time.Minute)
	defer ticker.Stop()
	for {
		select {
		case <-s.done:
			return
		case <-ticker.C:
			s.sessionsMu.Lock()
			now := time.Now()
			expired := 0
			for t, sess := range s.sessions {
				if now.After(sess.expiresAt) {
					delete(s.sessions, t)
					expired++
				}
			}
			remaining := len(s.sessions)
			s.sessionsMu.Unlock()
			if expired > 0 {
				s.logger.Printf("[INFO] [web] Cleaned up %d expired sessions, %d remaining", expired, remaining)
			}
			if s.isDebug() && expired > 0 {
				s.logger.Printf("[DEBUG] [web] session cleanup: expired=%d remaining=%d", expired, remaining)
			}
		}
	}
}

// Close signals the server to stop background goroutines. Safe to call multiple times.
func (s *Server) Close() {
	s.closeOnce.Do(func() {
		close(s.done)
	})
}

func (s *Server) renderLogin(w http.ResponseWriter, errorMsg, flashMsg string) {
	data := map[string]interface{}{
		"Title":        "Bejelentkezés",
		"CustomerName": s.cfg.Customer.Name,
		"Error":        errorMsg,
		"Flash":        flashMsg,
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := s.tmpl.ExecuteTemplate(w, "login", data); err != nil {
		s.logger.Printf("[ERROR] [web] Template error (login): %v", err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
	}
}
