package web

import (
	"errors"
	"fmt"
	"net"
	"net/http"
	"sync"
	"time"

	"golang.org/x/time/rate"
	"party.at/party/internal/data"
	"party.at/party/internal/validator"
	"party.at/party/cmd/party/common"
)

const cookieName = "session"

func (web *Web) AuthenticateCookie(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cookie, err := r.Cookie(cookieName)
		if err != nil {
			r = common.SetUser(r, data.AnonymousUser)
			next.ServeHTTP(w, r)
			return
		}

		v := validator.New()
		if data.ValidateTokenPlaintext(v, cookie.Value); !v.Valid() {
			clearCookie(w)
			r = common.SetUser(r, data.AnonymousUser)
			next.ServeHTTP(w, r)
			return
		}

		userIdentity, err := web.App.Models.UserIdentities.GetForToken(data.ScopeAuthentication, cookie.Value)
		if err != nil {
			if !errors.Is(err, data.ErrRecordNotFound) {
				web.App.LogError(r, err)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
				return
			}
			clearCookie(w)
			r = common.SetUser(r, data.AnonymousUser)
			next.ServeHTTP(w, r)
			return
		}

		user, err := web.App.Models.Users.Get(userIdentity.UserID)
		if err != nil {
			if !errors.Is(err, data.ErrRecordNotFound) {
				web.App.LogError(r, err)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
				return
			}
			clearCookie(w)
			r = common.SetUser(r, data.AnonymousUser)
			next.ServeHTTP(w, r)
			return
		}

		r = common.SetUser(r, user)
		permissions, _ := web.App.Models.Permissions.GetAllForUser(user.ID)
		r = common.SetPermissions(r, permissions)
		next.ServeHTTP(w, r)
	})
}

func (web *Web) RequireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if common.GetUser(r).IsAnonymous() {
			http.Redirect(w, r, "/", http.StatusSeeOther)
			return
		}
		next.ServeHTTP(w, r)
	}
}

func (web *Web) RequireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
	return web.RequireAuthenticatedUser(func(w http.ResponseWriter, r *http.Request) {
		user := common.GetUser(r)

		if !user.Activated {
			web.render(w, r, http.StatusOK, "forbidden", nil)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func (web *Web) RequirePermission(code string, next http.HandlerFunc) http.HandlerFunc {
	return web.RequireActivatedUser(func(w http.ResponseWriter, r *http.Request) {
		if !common.GetPermissions(r).Include(code) {
			web.errorResponse(w, r, data.ErrNotPermitted)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func (web *Web) WebRecoverPanic(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				w.Header().Set("Connection", "close")
				web.App.LogError(r, fmt.Errorf("%s", err))
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func (web *Web) WebRateLimit(next http.Handler) http.Handler {
	type client struct {
		limiter  *rate.Limiter
		lastSeen time.Time
	}

	var mu sync.Mutex
	clients := make(map[string]*client)

	go func() {
		for {
			time.Sleep(time.Minute)
			mu.Lock()
			for ip, c := range clients {
				if time.Since(c.lastSeen) > 3*time.Minute {
					delete(clients, ip)
				}
			}
			mu.Unlock()
		}
	}()

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if web.App.LimiterEnabled {
			ip, _, err := net.SplitHostPort(r.RemoteAddr)
			if err != nil {
				web.App.LogError(r, err)
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
				return
			}
			mu.Lock()
			if _, found := clients[ip]; !found {
				clients[ip] = &client{limiter: rate.NewLimiter(rate.Limit(web.App.LimiterRPS), web.App.LimiterBurst)}
			}
			clients[ip].lastSeen = time.Now()
			if !clients[ip].limiter.Allow() {
				mu.Unlock()
				http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
				return
			}
			mu.Unlock()
		}
		next.ServeHTTP(w, r)
	})
}
