package auth

import (
	"errors"
	"fmt"
	"html"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/wormhole/wormhole/internal/cache"
	"github.com/wormhole/wormhole/internal/store"
)

const (
	visitorLoginPath = "/_wormhole/login"
	visitorCookie    = "wh_visitor"
)

type VisitorClaims struct {
	UserID       uint   `json:"user_id"`
	ResourceType string `json:"resource_type"`
	ResourceID   uint   `json:"resource_id"`
	jwt.RegisteredClaims
}

type VisitorConfig struct {
	Enabled    bool
	BindMode   string
	UserIDs    []uint
	TTLSeconds int
}

func visitorConfigFromHost(h *store.Host) VisitorConfig {
	ttl := h.VisitorTTLSeconds
	if ttl <= 0 {
		ttl = 7200
	}
	return VisitorConfig{
		Enabled:    h.VisitorAuthEnabled,
		BindMode:   h.VisitorBindMode,
		TTLSeconds: ttl,
	}
}

func visitorConfigFromTunnel(t *store.Tunnel) VisitorConfig {
	ttl := t.VisitorTTLSeconds
	if ttl <= 0 {
		ttl = 7200
	}
	return VisitorConfig{
		Enabled:    t.VisitorAuthEnabled,
		BindMode:   t.VisitorBindMode,
		TTLSeconds: ttl,
	}
}

func (ra *ResourceAuth) CheckVisitorHTTP(w http.ResponseWriter, r *http.Request, resourceType string, resourceID uint, cfg VisitorConfig, userIDs []uint) bool {
	if !cfg.Enabled {
		return true
	}
	cfg.UserIDs = userIDs
	if bindMode := strings.TrimSpace(cfg.BindMode); bindMode == "" {
		cfg.BindMode = "all"
	} else {
		cfg.BindMode = bindMode
	}

	if r.URL.Path == visitorLoginPath {
		ra.handleVisitorLogin(w, r, resourceType, resourceID, cfg)
		return false
	}

	if claims, ok := ra.parseVisitorCookie(r, resourceType, resourceID); ok {
		if ra.visitorAllowed(claims.UserID, cfg) {
			ra.logVisitorAccess(claims.UserID, resourceType, resourceID, "access", r)
			return true
		}
	}

	redirect := r.URL.RequestURI()
	http.Redirect(w, r, visitorLoginPath+"?redirect="+url.QueryEscape(redirect), http.StatusFound)
	return false
}

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

func (ra *ResourceAuth) handleVisitorLogin(w http.ResponseWriter, r *http.Request, resourceType string, resourceID uint, cfg VisitorConfig) {
	ip := DirectRemoteIP(r)
	scopedIP := visitorLimiterIP(resourceType, resourceID, ip)

	switch r.Method {
	case http.MethodPost:
		_ = r.ParseForm()
		username := strings.TrimSpace(r.FormValue("username"))
		if retry, blocked := ra.limiter.IsBlocked(scopedIP, username); blocked {
			ra.renderVisitorLoginBlocked(w, r, cfg, retry)
			return
		}
		password := r.FormValue("password")
		user, err := ra.authenticateVisitor(username, password, cfg)
		if err != nil {
			ra.limiter.RecordFailure(scopedIP, username)
			time.Sleep(300 * time.Millisecond)
			ra.renderVisitorLogin(w, r, cfg, "用户名或密码错误")
			return
		}
		ra.limiter.Reset(scopedIP, username)
		token, err := ra.signVisitorToken(user.ID, resourceType, resourceID, cfg.TTLSeconds)
		if err != nil {
			http.Error(w, "internal error", http.StatusInternalServerError)
			return
		}
		secure := r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https")
		http.SetCookie(w, &http.Cookie{
			Name:     visitorCookie,
			Value:    token,
			Path:     "/",
			MaxAge:   cfg.TTLSeconds,
			HttpOnly: true,
			SameSite: http.SameSiteLaxMode,
			Secure:   secure,
		})
		redirect := SafeRedirectPath(r.FormValue("redirect"))
		ra.logVisitorAccess(user.ID, resourceType, resourceID, "login", r)
		http.Redirect(w, r, redirect, http.StatusFound)
	case http.MethodGet:
		if retry, blocked := ra.limiter.IsBlocked(scopedIP, ""); blocked {
			ra.renderVisitorLoginBlocked(w, r, cfg, retry)
			return
		}
		ra.renderVisitorLogin(w, r, cfg, "")
	default:
		http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
	}
}

func (ra *ResourceAuth) authenticateVisitor(username, password string, cfg VisitorConfig) (*store.User, error) {
	if username == "" || password == "" {
		return nil, errors.New("invalid credentials")
	}
	user, err := ra.store.GetUserByUsername(username)
	if err != nil {
		return nil, errors.New("invalid credentials")
	}
	if user.Role != "visitor" || !user.Status {
		return nil, errors.New("invalid credentials")
	}
	if !store.CheckPassword(user.PasswordHash, password) {
		return nil, errors.New("invalid credentials")
	}
	if !ra.visitorAllowed(user.ID, cfg) {
		return nil, errors.New("not allowed")
	}
	return user, nil
}

func (ra *ResourceAuth) visitorAllowed(userID uint, cfg VisitorConfig) bool {
	if cfg.BindMode == "all" {
		return true
	}
	for _, id := range cfg.UserIDs {
		if id == userID {
			return true
		}
	}
	return false
}

func (ra *ResourceAuth) parseVisitorCookie(r *http.Request, resourceType string, resourceID uint) (*VisitorClaims, bool) {
	cookie, err := r.Cookie(visitorCookie)
	if err != nil || cookie.Value == "" {
		return nil, false
	}
	claims, err := ra.parseVisitorToken(cookie.Value)
	if err != nil {
		return nil, false
	}
	if claims.ResourceType != resourceType || claims.ResourceID != resourceID {
		return nil, false
	}
	return claims, true
}

func (ra *ResourceAuth) signVisitorToken(userID uint, resourceType string, resourceID uint, ttlSeconds int) (string, error) {
	if ttlSeconds <= 0 {
		ttlSeconds = 7200
	}
	claims := VisitorClaims{
		UserID:       userID,
		ResourceType: resourceType,
		ResourceID:   resourceID,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(ttlSeconds) * time.Second)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(ra.jwtSecret))
}

func (ra *ResourceAuth) parseVisitorToken(tokenStr string) (*VisitorClaims, error) {
	token, err := jwt.ParseWithClaims(tokenStr, &VisitorClaims{}, func(t *jwt.Token) (interface{}, error) {
		return []byte(ra.jwtSecret), nil
	})
	if err != nil {
		return nil, err
	}
	claims, ok := token.Claims.(*VisitorClaims)
	if !ok || !token.Valid {
		return nil, errors.New("invalid token")
	}
	return claims, nil
}

func (ra *ResourceAuth) renderVisitorLogin(w http.ResponseWriter, r *http.Request, cfg VisitorConfig, errMsg string) {
	redirect := SafeRedirectPath(r.URL.Query().Get("redirect"))
	errHTML := ""
	if errMsg != "" {
		errHTML = fmt.Sprintf(`<p class="error">%s</p>`, html.EscapeString(errMsg))
	}
	page := fmt.Sprintf(`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>登录验证</title>
<style>
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f1419;color:#e7e9ea}
.card{width:100%%;max-width:380px;padding:32px 28px;background:#16181c;border:1px solid #2f3336;border-radius:16px;box-shadow:0 8px 32px rgba(0,0,0,.35)}
h1{margin:0 0 8px;font-size:22px;font-weight:600}
.sub{margin:0 0 24px;color:#71767b;font-size:14px}
label{display:block;margin-bottom:6px;font-size:13px;color:#71767b}
input{width:100%%;padding:10px 12px;margin-bottom:14px;border:1px solid #2f3336;border-radius:8px;background:#000;color:#e7e9ea;font-size:15px}
input:focus{outline:none;border-color:#1d9bf0}
button{width:100%%;padding:11px;border:none;border-radius:999px;background:#1d9bf0;color:#fff;font-size:15px;font-weight:600;cursor:pointer}
button:hover{background:#1a8cd8}
.error{color:#f4212e;font-size:13px;margin:0 0 12px}
</style>
</head>
<body>
<div class="card">
<h1>登录验证</h1>
<p class="sub">此资源已开启访客登录，请使用分配的账号访问。</p>
%s
<form method="POST" action="%s">
<input type="hidden" name="redirect" value="%s"/>
<label>用户名</label>
<input name="username" autocomplete="username" required autofocus/>
<label>密码</label>
<input name="password" type="password" autocomplete="current-password" required/>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>`, errHTML, visitorLoginPath, html.EscapeString(redirect))
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte(page))
}

func (ra *ResourceAuth) renderVisitorLoginBlocked(w http.ResponseWriter, r *http.Request, _ VisitorConfig, retryAfter time.Duration) {
	redirect := SafeRedirectPath(r.URL.Query().Get("redirect"))
	minutes := int(retryAfter.Minutes())
	if minutes < 1 {
		minutes = 1
	}
	msg := fmt.Sprintf("登录尝试次数过多，请 %d 分钟后再试", minutes)
	if retryAfter < time.Minute {
		secs := int(retryAfter.Seconds())
		if secs < 1 {
			secs = 1
		}
		msg = fmt.Sprintf("登录尝试次数过多，请 %d 秒后再试", secs)
	}
	retrySecs := int(retryAfter.Seconds()) + 1
	if retrySecs < 1 {
		retrySecs = 1
	}
	w.Header().Set("Retry-After", strconv.Itoa(retrySecs))
	loginURL := visitorLoginPath + "?redirect=" + url.QueryEscape(redirect)
	page := fmt.Sprintf(`<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>登录验证</title>
<style>
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0f1419;color:#e7e9ea}
.card{width:100%%;max-width:380px;padding:32px 28px;background:#16181c;border:1px solid #2f3336;border-radius:16px;box-shadow:0 8px 32px rgba(0,0,0,.35)}
h1{margin:0 0 8px;font-size:22px;font-weight:600}
.sub{margin:0 0 16px;color:#71767b;font-size:14px}
.error{color:#f4212e;font-size:14px;margin:0 0 16px;line-height:1.5}
a{color:#1d9bf0;text-decoration:none}
</style>
</head>
<body>
<div class="card">
<h1>登录验证</h1>
<p class="sub">此资源已开启访客登录，请使用分配的账号访问。</p>
<p class="error">%s</p>
<p class="sub"><a href="%s">稍后再试</a></p>
</div>
</body>
</html>`, html.EscapeString(msg), html.EscapeString(loginURL))
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.WriteHeader(http.StatusTooManyRequests)
	_, _ = w.Write([]byte(page))
}

func GetVisitorConfig(c *cache.Manager, resourceType string, resourceID uint) (VisitorConfig, []uint, bool) {
	switch resourceType {
	case "host":
		h, ok := c.GetHost(resourceID)
		if !ok {
			return VisitorConfig{}, nil, false
		}
		cfg := visitorConfigFromHost(h)
		ids, _ := c.GetResourceVisitorIDs(resourceType, resourceID)
		return cfg, ids, true
	case "tunnel":
		t, ok := c.GetTunnel(resourceID)
		if !ok {
			return VisitorConfig{}, nil, false
		}
		cfg := visitorConfigFromTunnel(t)
		ids, _ := c.GetResourceVisitorIDs(resourceType, resourceID)
		return cfg, ids, true
	default:
		return VisitorConfig{}, nil, false
	}
}
