package auth

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

	"github.com/wormhole/wormhole/internal/cache"
	"github.com/wormhole/wormhole/internal/config"
	"github.com/wormhole/wormhole/internal/store"
)

const accessCookieName = "wh_access"

type ResourceAuth struct {
	cache     *cache.Manager
	store     *store.Store
	jwtSecret string
	limiter   *LoginLimiter
	mu        sync.RWMutex
	grants    map[string]time.Time
}

func NewResourceAuth(c *cache.Manager, st *store.Store, authCfg *config.AuthConfig) *ResourceAuth {
	ra := &ResourceAuth{
		cache:     c,
		store:     st,
		jwtSecret: authCfg.JWTSecret,
		limiter:   NewLoginLimiter(authCfg),
		grants:    make(map[string]time.Time),
	}
	go ra.cleanupLoop()
	return ra
}

func (ra *ResourceAuth) ReloadAuth(cfg *config.AuthConfig) {
	ra.mu.Lock()
	defer ra.mu.Unlock()
	ra.jwtSecret = cfg.JWTSecret
	ra.limiter = NewLoginLimiter(cfg)
}

func grantKey(resourceType string, resourceID uint, ip string) string {
	return fmt.Sprintf("%s:%d:%s", resourceType, resourceID, ip)
}

func (ra *ResourceAuth) cleanupLoop() {
	ticker := time.NewTicker(time.Minute)
	for range ticker.C {
		now := time.Now()
		ra.mu.Lock()
		for k, exp := range ra.grants {
			if now.After(exp) {
				delete(ra.grants, k)
			}
		}
		ra.mu.Unlock()
	}
}

func (ra *ResourceAuth) GrantAccess(resourceType string, resourceID uint, ip string, ttl time.Duration) {
	ra.mu.Lock()
	defer ra.mu.Unlock()
	ra.grants[grantKey(resourceType, resourceID, ip)] = time.Now().Add(ttl)
}

func (ra *ResourceAuth) HasAccess(resourceType string, resourceID uint, ip string) bool {
	ra.mu.RLock()
	defer ra.mu.RUnlock()
	exp, ok := ra.grants[grantKey(resourceType, resourceID, ip)]
	return ok && time.Now().Before(exp)
}

func (ra *ResourceAuth) CheckHTTP(w http.ResponseWriter, r *http.Request, resourceType string, resourceID uint) bool {
	cfg, userIDs, ok := GetVisitorConfig(ra.cache, resourceType, resourceID)
	if ok && cfg.Enabled {
		return ra.CheckVisitorHTTP(w, r, resourceType, resourceID, cfg, userIDs)
	}

	ip := DirectRemoteIP(r)
	if ra.HasAccess(resourceType, resourceID, ip) {
		return true
	}
	policy, ok := ra.cache.GetAuthPolicy(resourceType, resourceID)
	if !ok || !policy.Enabled {
		return true
	}

	switch policy.Type {
	case "basic":
		_, pass, ok := r.BasicAuth()
		if ok && subtle.ConstantTimeCompare([]byte(pass), []byte(policy.Password)) == 1 {
			ra.GrantAccess(resourceType, resourceID, ip, time.Duration(policy.TTLSeconds)*time.Second)
			return true
		}
		w.Header().Set("WWW-Authenticate", `Basic realm="Wormhole"`)
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return false
	case "form":
		if r.URL.Path == "/_wormhole/auth" && r.Method == http.MethodPost {
			_ = r.ParseForm()
			pass := r.FormValue("password")
			if subtle.ConstantTimeCompare([]byte(pass), []byte(policy.Password)) == 1 {
				ra.GrantAccess(resourceType, resourceID, ip, time.Duration(policy.TTLSeconds)*time.Second)
				http.SetCookie(w, &http.Cookie{Name: accessCookieName, Value: "1", Path: "/", MaxAge: policy.TTLSeconds})
				redirect := SafeRedirectPath(r.FormValue("redirect"))
				http.Redirect(w, r, redirect, http.StatusFound)
				return false
			}
		}
		if cookie, err := r.Cookie(accessCookieName); err == nil && cookie.Value == "1" {
			ra.GrantAccess(resourceType, resourceID, ip, time.Duration(policy.TTLSeconds)*time.Second)
			return true
		}
		redirect := r.URL.RequestURI()
		html := fmt.Sprintf(`<!DOCTYPE html><html><body>
<form method="POST" action="/_wormhole/auth">
<input type="hidden" name="redirect" value="%s"/>
<input type="password" name="password" placeholder="Password"/>
<button type="submit">Login</button>
</form></body></html>`, redirect)
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.WriteHeader(http.StatusUnauthorized)
		_, _ = w.Write([]byte(html))
		return false
	case "callback":
		if policy.CallbackURL != "" {
			token := r.URL.Query().Get("token")
			if token != "" && token == policy.Password {
				ra.GrantAccess(resourceType, resourceID, ip, time.Duration(policy.TTLSeconds)*time.Second)
				return true
			}
			cb := policy.CallbackURL
			if stringsContains(cb, "?") {
				cb += "&"
			} else {
				cb += "?"
			}
			cb += "redirect=" + r.URL.RequestURI()
			http.Redirect(w, r, cb, http.StatusFound)
			return false
		}
	}
	return true
}

func (ra *ResourceAuth) CheckTCP(remoteAddr net.Addr, resourceType string, resourceID uint) bool {
	ip := remoteAddr.String()
	if host, _, err := net.SplitHostPort(ip); err == nil {
		ip = host
	}
	if ra.HasAccess(resourceType, resourceID, ip) {
		return true
	}
	policy, ok := ra.cache.GetAuthPolicy(resourceType, resourceID)
	if !ok || !policy.Enabled {
		return true
	}
	return false
}

func stringsContains(s, sub string) bool {
	return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexOf(s, sub) >= 0)
}

func indexOf(s, sub string) int {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return i
		}
	}
	return -1
}

func (ra *ResourceAuth) CreatePolicy(st *store.Store, p *store.AuthPolicy) error {
	return st.CreateAuthPolicy(p)
}
