package api

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

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

func (api *Api) RecoverPanic(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")
				api.ServerErrorResponse(w, r, fmt.Errorf("%s", err))
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func (api *Api) RequireAuthenticatedUser(next http.HandlerFunc) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if common.GetUser(r).IsAnonymous() {
			api.errorResponse(w, r, data.ErrAuthRequired)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func (api *Api) RequireActivatedUser(next http.HandlerFunc) http.HandlerFunc {
	fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !common.GetUser(r).Activated {
			api.errorResponse(w, r, data.ErrInactiveAccount)
			return
		}
		next.ServeHTTP(w, r)
	})
	return api.RequireAuthenticatedUser(fn)
}

func (api *Api) RequirePermission(code string, next http.HandlerFunc) http.HandlerFunc {
	fn := func(w http.ResponseWriter, r *http.Request) {
		permissions, err := api.App.Models.Permissions.GetAllForUser(common.GetUser(r).ID)
		if err != nil {
			api.ServerErrorResponse(w, r, err)
			return
		}
		if !permissions.Include(code) {
			api.errorResponse(w, r, data.ErrNotPermitted)
			return
		}
		next.ServeHTTP(w, r)
	}
	return api.RequireActivatedUser(fn)
}

func (api *Api) RateLimit(next http.Handler) http.Handler {
	type client struct {
		limiter  *rate.Limiter
		lastSeen time.Time
	}

	var mu sync.Mutex
	var 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 api.App.LimiterEnabled {
			ip, _, err := net.SplitHostPort(r.RemoteAddr)
			if err != nil {
				api.ServerErrorResponse(w, r, err)
				return
			}

			mu.Lock()
			if _, found := clients[ip]; !found {
				clients[ip] = &client{limiter: rate.NewLimiter(rate.Limit(api.App.LimiterRPS), api.App.LimiterBurst)}
			}
			clients[ip].lastSeen = time.Now()
			if !clients[ip].limiter.Allow() {
				mu.Unlock()
				api.errorResponse(w, r, data.ErrRateLimitExceeded)
				return
			}
			mu.Unlock()
		}
		next.ServeHTTP(w, r)
	})
}
