package handler

import (
	"encoding/json"
	"net/http"
	"strings"
	"time"
	"xboard-go/internal/database"
	"xboard-go/internal/model"
	"xboard-go/internal/service"
	"xboard-go/pkg/utils"

	"github.com/gin-gonic/gin"
)

func ipv6StatusPresentation(status string, allowed bool) (string, string, string) {
	status = strings.TrimSpace(strings.ToLower(status))
	switch status {
	case "active":
		return "active", "IPv6 enabled", ""
	case "disabled":
		return "disabled", "IPv6 disabled", ""
	case "eligible":
		return "eligible", "Ready to enable", ""
	case "", "not_allowed", "not_eligible", "disallowed":
		if allowed {
			return "eligible", "Ready to enable", ""
		}
		return "not_allowed", "Not eligible", "Current plan or group is not allowed to enable IPv6"
	default:
		label := strings.ReplaceAll(strings.Title(strings.ReplaceAll(status, "_", " ")), "Ipv6", "IPv6")
		if strings.EqualFold(label, "Not Allowed") {
			label = "Not eligible"
		}
		reason := ""
		if !allowed && (status == "not_allowed" || status == "not_eligible") {
			reason = "Current plan or group is not allowed to enable IPv6"
		}
		return status, label, reason
	}
}

func PluginUserOnlineDevicesUsers(c *gin.Context) {

	page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
	perPage := parsePositiveInt(c.DefaultQuery("per_page", "20"), 20)
	keyword := strings.TrimSpace(c.Query("keyword"))

	query := database.DB.Model(&model.User{}).Preload("Plan").Order("id DESC")
	if keyword != "" {
		query = query.Where("email LIKE ? OR id = ?", "%"+keyword+"%", keyword)
	}

	var total int64
	query.Count(&total)

	var users []model.User
	if err := query.Offset((page - 1) * perPage).Limit(perPage).Find(&users).Error; err != nil {
		Fail(c, 500, "failed to fetch users")
		return
	}

	userIDs := make([]int, 0, len(users))
	for _, user := range users {
		userIDs = append(userIDs, user.ID)
	}
	devices := service.GetUsersDevices(userIDs)
	deviceMetaByUserID := make(map[int]struct {
		LastSeenAt int64
		Count      int
	}, len(userIDs))
	if len(userIDs) > 0 {
		var rows []struct {
			UserID     int
			LastSeenAt int64
			IPCount    int
		}
		_ = database.DB.Table("v2_user_online_devices").
			Select("user_id, MAX(last_seen_at) AS last_seen_at, COUNT(DISTINCT ip) AS ip_count").
			Where("user_id IN ? AND expires_at > ?", userIDs, time.Now().Unix()).
			Group("user_id").
			Scan(&rows).Error
		for _, row := range rows {
			deviceMetaByUserID[row.UserID] = struct {
				LastSeenAt int64
				Count      int
			}{LastSeenAt: row.LastSeenAt, Count: row.IPCount}
		}
	}

	list := make([]gin.H, 0, len(users))
	usersWithOnlineIP := 0
	totalOnlineIPs := 0
	for _, user := range users {
		ips := devices[user.ID]
		meta := deviceMetaByUserID[user.ID]
		onlineCount := len(ips)
		if meta.Count > onlineCount {
			onlineCount = meta.Count
		}
		if onlineCount > 0 {
			usersWithOnlineIP++
			totalOnlineIPs += onlineCount
		}

		lastOnlineText := formatTimeValue(user.LastOnlineAt)
		if meta.LastSeenAt > 0 {
			lastOnlineText = formatUnixValue(meta.LastSeenAt)
		}

		status := "offline"
		statusLabel := "Offline"
		if onlineCount > 0 {
			status = "online"
			statusLabel = "Online"
		}

		list = append(list, gin.H{
			"id":                user.ID,
			"email":             user.Email,
			"subscription_name": planName(user.Plan),
			"online_count":      onlineCount,
			"online_devices":    ips,
			"last_online_text":  lastOnlineText,
			"created_text":      formatUnixValue(user.CreatedAt),
			"status":            status,
			"status_label":      statusLabel,
		})
	}

	Success(c, gin.H{
		"list": list,
		"filters": gin.H{
			"keyword":  keyword,
			"per_page": perPage,
		},
		"summary": gin.H{
			"page_users":           len(users),
			"users_with_online_ip": usersWithOnlineIP,
			"total_online_ips":     totalOnlineIPs,
			"current_page":         page,
		},
		"pagination": gin.H{
			"current":   page,
			"last_page": calculateLastPage(total, perPage),
			"per_page":  perPage,
			"total":     total,
		},
	})
}

func AdminIPv6SubscriptionUsers(c *gin.Context) {
	page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
	perPage := parsePositiveInt(c.DefaultQuery("per_page", "20"), 20)
	keyword := strings.TrimSpace(c.Query("keyword"))

	suffix := service.GetPluginConfigString(service.PluginUserAddIPv6, "email_suffix", "-ipv6")
	shadowPattern := "%" + suffix + "@%"

	query := database.DB.Model(&model.User{}).
		Preload("Plan").
		Where("email NOT LIKE ?", shadowPattern).
		Order("id DESC")
	if keyword != "" {
		query = query.Where("email LIKE ? OR CAST(id AS CHAR) = ?", "%"+keyword+"%", keyword)
	}

	var total int64
	if err := query.Count(&total).Error; err != nil {
		Fail(c, 500, "failed to count users")
		return
	}

	var users []model.User
	if err := query.Offset((page - 1) * perPage).Limit(perPage).Find(&users).Error; err != nil {
		Fail(c, 500, "failed to fetch users")
		return
	}

	userIDs := make([]int, 0, len(users))
	for _, user := range users {
		userIDs = append(userIDs, user.ID)
	}

	subscriptionByUserID := make(map[int]model.UserIPv6Subscription, len(userIDs))
	shadowUserIDs := make([]int, 0, len(userIDs))
	if len(userIDs) > 0 {
		var rows []model.UserIPv6Subscription
		if err := database.DB.Where("user_id IN ?", userIDs).Find(&rows).Error; err == nil {
			for _, row := range rows {
				subscriptionByUserID[row.UserID] = row
				if row.ShadowUserID != nil && *row.ShadowUserID > 0 {
					shadowUserIDs = append(shadowUserIDs, *row.ShadowUserID)
				}
			}
		}
	}
	shadowByParentID := make(map[int]model.User, len(userIDs))
	shadowByID := make(map[int]model.User, len(shadowUserIDs))
	shadowByEmail := make(map[string]model.User, len(userIDs))
	if len(userIDs) > 0 {
		var shadowUsers []model.User
		query := database.DB.Preload("Plan")
		if len(shadowUserIDs) > 0 {
			query = query.Where("parent_id IN ? OR id IN ?", userIDs, shadowUserIDs)
		} else {
			query = query.Where("parent_id IN ?", userIDs)
		}
		if err := query.Find(&shadowUsers).Error; err == nil {
			for _, shadow := range shadowUsers {
				shadowByID[shadow.ID] = shadow
				shadowByEmail[strings.ToLower(shadow.Email)] = shadow
				if shadow.ParentID != nil {
					shadowByParentID[*shadow.ParentID] = shadow
				}
			}
		}
	}
	planNames := make(map[int]string)
	var plans []model.Plan
	if err := database.DB.Select("id", "name").Find(&plans).Error; err == nil {
		for _, plan := range plans {
			planNames[plan.ID] = plan.Name
		}
	}

	list := make([]gin.H, 0, len(users))
	for _, user := range users {
		subscription, hasSubscription := subscriptionByUserID[user.ID]
		shadowUser, hasShadowUser := shadowByParentID[user.ID]
		if !hasShadowUser && hasSubscription && subscription.ShadowUserID != nil {
			shadowUser, hasShadowUser = shadowByID[*subscription.ShadowUserID]
		}
		if !hasShadowUser {
			shadowUser, hasShadowUser = shadowByEmail[strings.ToLower(service.IPv6ShadowEmail(user.Email))]
		}
		if !hasSubscription && hasShadowUser {
			subscription = model.UserIPv6Subscription{
				UserID:       user.ID,
				ShadowUserID: &shadowUser.ID,
				IPv6Email:    shadowUser.Email,
				Allowed:      !user.Banned,
				Status:       "active",
				UpdatedAt:    shadowUser.UpdatedAt,
			}
			hasSubscription = true
		}
		status := "eligible"
		planID := 0
		planNameValue := ""
		if hasShadowUser {
			planID = intFromPointer(shadowUser.PlanID)
			planNameValue = firstString(planName(shadowUser.Plan), planNames[planID])
		}
		shadowUserID := 0
		shadowUpdatedAt := int64(0)
		if hasSubscription {
			status = firstString(subscription.Status, status)
			if subscription.ShadowUserID != nil {
				shadowUserID = *subscription.ShadowUserID
			}
			shadowUpdatedAt = subscription.UpdatedAt
		}
		effectiveAllowed := true
		status, statusLabel, _ := ipv6StatusPresentation(status, effectiveAllowed)

		list = append(list, gin.H{
			"id":             user.ID,
			"email":          user.Email,
			"plan_id":        planID,
			"plan_name":      planNameValue,
			"allowed":        effectiveAllowed,
			"is_active":      hasSubscription && subscription.Status == "active",
			"status":         status,
			"status_label":   statusLabel,
			"ipv6_email":     firstString(subscription.IPv6Email, service.IPv6ShadowEmail(user.Email)),
			"shadow_user_id": shadowUserID,
			"updated_at":     shadowUpdatedAt,
			"group_id":       user.GroupID,
		})
	}

	Success(c, gin.H{
		"list": list,
		"pagination": gin.H{
			"current":   page,
			"last_page": calculateLastPage(total, perPage),
			"per_page":  perPage,
			"total":     total,
		},
	})
}

func AdminIPv6SubscriptionConfigFetch(c *gin.Context) {
	cfg := service.GetPluginConfig(service.PluginUserAddIPv6)
	Success(c, gin.H{
		"ipv6_plan_id":   parsePositiveInt(service.GetPluginConfigString(service.PluginUserAddIPv6, "ipv6_plan_id", "0"), 0),
		"allowed_plans":  service.GetPluginConfigIntList(service.PluginUserAddIPv6, "allowed_plans"),
		"allowed_groups": service.GetPluginConfigIntList(service.PluginUserAddIPv6, "allowed_groups"),
		"email_suffix":   firstString(stringFromAny(cfg["email_suffix"]), "-ipv6"),
		"reference_flag": firstString(stringFromAny(cfg["reference_flag"]), "ipv6"),
	})
}

func AdminIPv6SubscriptionConfigSave(c *gin.Context) {
	var payload struct {
		IPv6PlanID int `json:"ipv6_plan_id"`
	}
	if err := c.ShouldBindJSON(&payload); err != nil {
		Fail(c, http.StatusBadRequest, "invalid request body")
		return
	}

	var plugin model.Plugin
	err := database.DB.Where("code = ?", service.PluginUserAddIPv6).First(&plugin).Error
	if err != nil {
		plugin = model.Plugin{
			Code:      service.PluginUserAddIPv6,
			Name:      "IPv6 Subscription",
			IsEnabled: true,
		}
	}

	cfg := service.GetPluginConfig(service.PluginUserAddIPv6)
	if payload.IPv6PlanID > 0 {
		cfg["ipv6_plan_id"] = payload.IPv6PlanID
	} else {
		delete(cfg, "ipv6_plan_id")
	}

	raw, marshalErr := json.Marshal(cfg)
	if marshalErr != nil {
		Fail(c, http.StatusInternalServerError, "failed to encode plugin config")
		return
	}
	configText := string(raw)
	plugin.Config = &configText

	if plugin.ID > 0 {
		if updateErr := database.DB.Model(&model.Plugin{}).Where("id = ?", plugin.ID).Updates(map[string]any{
			"config":     plugin.Config,
			"is_enabled": plugin.IsEnabled,
		}).Error; updateErr != nil {
			Fail(c, http.StatusInternalServerError, "failed to save plugin config")
			return
		}
	} else {
		if createErr := database.DB.Create(&plugin).Error; createErr != nil {
			Fail(c, http.StatusInternalServerError, "failed to create plugin config")
			return
		}
	}

	Success(c, gin.H{
		"ipv6_plan_id": payload.IPv6PlanID,
	})
}

func AdminIPv6SubscriptionEnable(c *gin.Context) {
	userID := parsePositiveInt(c.Param("userId"), 0)
	if userID == 0 {
		Fail(c, 400, "invalid user id")
		return
	}

	var payload struct {
		PlanID int `json:"plan_id"`
	}
	_ = c.ShouldBindJSON(&payload)

	var user model.User
	if err := database.DB.Preload("Plan").First(&user, userID).Error; err != nil {
		Fail(c, 404, "user not found")
		return
	}
	if user.ParentID != nil {
		Fail(c, 400, "shadow users cannot be enabled again")
		return
	}

	if !service.SyncIPv6ShadowAccountWithPlan(&user, payload.PlanID) {
		Fail(c, 500, "failed to create/sync IPv6 subscription")
		return
	}

	SuccessMessage(c, "IPv6 subscription enabled/synced", true)
}

func AdminIPv6SubscriptionDisable(c *gin.Context) {
	userID := parsePositiveInt(c.Param("userId"), 0)
	if userID == 0 {
		Fail(c, 400, "invalid user id")
		return
	}

	var user model.User
	if err := database.DB.First(&user, userID).Error; err != nil {
		Fail(c, 404, "user not found")
		return
	}

	if !service.DisableIPv6ShadowAccount(&user) {
		Fail(c, 500, "failed to disable IPv6 subscription")
		return
	}

	SuccessMessage(c, "IPv6 subscription disabled", true)
}

func AdminIPv6SubscriptionSyncPassword(c *gin.Context) {
	userID := parsePositiveInt(c.Param("userId"), 0)
	if userID == 0 {
		Fail(c, 400, "invalid user id")
		return
	}

	var user model.User
	if err := database.DB.First(&user, userID).Error; err != nil {
		Fail(c, 404, "user not found")
		return
	}

	if !service.SyncIPv6PasswordState(&user) {
		Fail(c, 404, "IPv6 user not found")
		return
	}

	SuccessMessage(c, "Password synced to IPv6 account", true)
}

func PluginUserOnlineDevicesGetIP(c *gin.Context) {

	user, ok := currentUser(c)
	if !ok {
		Fail(c, 401, "unauthorized")
		return
	}

	devices := service.GetUsersDevices([]int{user.ID})
	ips := devices[user.ID]
	authToken, _ := c.Get("auth_token")
	currentToken, _ := authToken.(string)
	sessions := service.GetUserSessions(user.ID, currentToken)
	currentID := currentSessionID(c)

	sessionItems := make([]gin.H, 0, len(sessions))
	for _, session := range sessions {
		sessionItems = append(sessionItems, gin.H{
			"id":           session.ID,
			"name":         session.Name,
			"user_agent":   session.UserAgent,
			"ip":           firstString(session.IP, "-"),
			"created_at":   session.CreatedAt,
			"last_used_at": session.LastUsedAt,
			"expires_at":   session.ExpiresAt,
			"is_current":   session.ID == currentID,
		})
	}
	Success(c, gin.H{
		"ips": ips,
		"session_overview": gin.H{
			"online_ip_count":      len(ips),
			"online_ips":           ips,
			"online_device_count":  len(ips),
			"device_limit":         user.DeviceLimit,
			"last_online_at":       user.LastOnlineAt,
			"active_session_count": len(sessionItems),
			"sessions":             sessionItems,
		},
	})
}

func PluginUserAddIPv6Check(c *gin.Context) {

	user, ok := currentUser(c)
	if !ok {
		Fail(c, 401, "unauthorized")
		return
	}

	var subscription model.UserIPv6Subscription
	hasSubscription := database.DB.Where("user_id = ?", user.ID).First(&subscription).Error == nil
	status := "eligible"
	if hasSubscription {
		status = firstString(subscription.Status, "active")
	}
	effectiveAllowed := true
	status, statusLabel, reason := ipv6StatusPresentation(status, effectiveAllowed)

	Success(c, gin.H{
		"allowed":      effectiveAllowed,
		"is_active":    hasSubscription && subscription.Status == "active",
		"status":       status,
		"status_label": statusLabel,
		"reason":       reason,
		"ipv6_email":   firstString(subscription.IPv6Email, service.IPv6ShadowEmail(user.Email)),
	})
}

func PluginUserAddIPv6Enable(c *gin.Context) {

	user, ok := currentUser(c)
	if !ok {
		Fail(c, 401, "unauthorized")
		return
	}

	if !service.SyncIPv6ShadowAccount(user) {
		Fail(c, 403, "your plan does not support IPv6 subscription")
		return
	}

	payload := gin.H{
		"ipv6_email": service.IPv6ShadowEmail(user.Email),
	}

	var shadowUser model.User
	if err := database.DB.Where("parent_id = ? AND email = ?", user.ID, service.IPv6ShadowEmail(user.Email)).First(&shadowUser).Error; err == nil {
		payload["shadow_user_id"] = shadowUser.ID
		token, err := utils.GenerateToken(shadowUser.ID, shadowUser.IsAdmin)
		if err == nil {
			payload["token"] = token
			payload["auth_data"] = token
			service.TrackSession(shadowUser.ID, token, c.ClientIP(), c.GetHeader("User-Agent"))
			_ = database.DB.Model(&model.User{}).Where("id = ?", shadowUser.ID).Update("last_login_at", time.Now().Unix()).Error
		}
	}

	SuccessMessage(c, "IPv6 subscription enabled/synced", payload)
}

func PluginUserAddIPv6SyncPassword(c *gin.Context) {

	user, ok := currentUser(c)
	if !ok {
		Fail(c, 401, "unauthorized")
		return
	}

	if !service.SyncIPv6PasswordState(user) {
		Fail(c, 404, "IPv6 user not found")
		return
	}

	SuccessMessage(c, "Password synced to IPv6 account", true)
}

func AdminSystemStatus(c *gin.Context) {
	Success(c, gin.H{
		"server_time": time.Now().Unix(),
		"app_name":    service.MustGetString("app_name", "XBoard"),
		"app_url":     service.GetAppURL(),
		"version":     "1.0.0",
		"go_version":  "1.21+",
	})
}

// AdminSystemQueueStats returns empty queue stats (Go app has no Horizon/queue workers).
// The React frontend polls this endpoint; returning empty data prevents 404 polling errors.
func AdminSystemQueueStats(c *gin.Context) {
	fullPath := c.FullPath()
	if strings.HasSuffix(fullPath, "/getHorizonFailedJobs") {
		c.JSON(200, gin.H{
			"data":  []any{},
			"total": 0,
		})
		return
	}
	if strings.HasSuffix(fullPath, "/getQueueWorkload") || strings.HasSuffix(fullPath, "/getQueueMasters") {
		Success(c, []any{})
		return
	}

	Success(c, gin.H{
		"status":        true,
		"wait":          gin.H{"default": 0},
		"recentJobs":    0,
		"jobsPerMinute": 0,
		"failedJobs":    0,
		"processes":     0,
		"pausedMasters": 0,
		"periods": gin.H{
			"recentJobs": 60,
			"failedJobs": 168,
		},
		"queueWithMaxThroughput": gin.H{
			"name":       "default",
			"throughput": 0,
		},
		"queueWithMaxRuntime": gin.H{
			"name":    "default",
			"runtime": 0,
		},
		"workload":     []any{},
		"masters":      []any{},
		"failed_jobs":  0,
		"total_jobs":   0,
		"pending_jobs": 0,
		"processed":    0,
		"failed":       0,
	})
}
