package handlers

import (
	"context"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/gorilla/websocket"
	"github.com/skip2/go-qrcode"
	"golang.org/x/crypto/ssh"
	localmiddleware "maintainarr/internal/middleware"
	"maintainarr/internal/models"
	"maintainarr/internal/services"
	"maintainarr/internal/views"
)

type Handler struct {
	repo     *services.Repository
	auth     *services.AuthService
	sessions *services.SessionService
	nodes    *services.NodeService
	renderer *views.Renderer
	org      models.Organization
	baseURL  string
	upgrader websocket.Upgrader
}

type dashboardData struct {
	Groups    []dashboardNodeGroup
	Ungrouped []models.Node
}

type dashboardNodeGroup struct {
	Name  string
	Nodes []models.Node
}

type nodePageData struct {
	Node         *models.Node
	CommandRuns  []models.CommandRun
	ConsoleToken string
}

type settingsData struct {
	ThemeVariables template.CSS
	CurrentTheme   string
	CurrentMode    string
}

type jobsPageData struct {
	Jobs   []models.AutomationJob
	Nodes  []models.Node
	Groups []models.VMGroup
	Tags   []string
	Runs   []models.CommandRun
}

func New(repo *services.Repository, auth *services.AuthService, sessions *services.SessionService, nodes *services.NodeService, renderer *views.Renderer, org models.Organization, baseURL string) *Handler {
	return &Handler{
		repo:     repo,
		auth:     auth,
		sessions: sessions,
		nodes:    nodes,
		renderer: renderer,
		org:      org,
		baseURL:  baseURL,
		upgrader: websocket.Upgrader{
			CheckOrigin: func(r *http.Request) bool { return true },
		},
	}
}

func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
	if user := localmiddleware.CurrentUser(r); user != nil {
		http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
		return
	}
	http.Redirect(w, r, "/login", http.StatusSeeOther)
}

func (h *Handler) LoginPage(w http.ResponseWriter, r *http.Request) {
	h.render(w, r, "login", "Sign In", nil)
}

func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	user, err := h.repo.GetUserByEmail(r.Context(), strings.TrimSpace(r.FormValue("email")))
	if err != nil || h.auth.ComparePassword(user.PasswordHash, r.FormValue("password")) != nil {
		h.render(w, r, "login", "Sign In", map[string]any{"Error": "Invalid credentials"})
		return
	}

	session, _ := h.sessions.Get(r)
	if user.OTPEnabled {
		session.Values["pending_user_id"] = user.ID
		delete(session.Values, "user_id")
		_ = session.Save(r, w)
		http.Redirect(w, r, "/login/otp", http.StatusSeeOther)
		return
	}

	session.Values["user_id"] = user.ID
	delete(session.Values, "pending_user_id")
	_ = session.Save(r, w)

	if !user.OTPEnabled {
		http.Redirect(w, r, "/setup-2fa", http.StatusSeeOther)
		return
	}

	http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}

func (h *Handler) LoginOTPPage(w http.ResponseWriter, r *http.Request) {
	h.render(w, r, "login_otp", "Two-Factor Verification", nil)
}

func (h *Handler) LoginOTP(w http.ResponseWriter, r *http.Request) {
	session, _ := h.sessions.Get(r)
	pendingUserID, ok := session.Values["pending_user_id"].(int64)
	if !ok {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}

	user, err := h.repo.GetUserByID(r.Context(), pendingUserID)
	if err != nil {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}

	if !h.auth.VerifyOTP(user.OTPSecret, strings.TrimSpace(r.FormValue("code"))) {
		h.render(w, r, "login_otp", "Two-Factor Verification", map[string]any{"Error": "Invalid OTP code"})
		return
	}

	session.Values["user_id"] = user.ID
	delete(session.Values, "pending_user_id")
	_ = session.Save(r, w)
	http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}

func (h *Handler) RegisterPage(w http.ResponseWriter, r *http.Request) {
	h.render(w, r, "register", "Register", nil)
}

func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	password := r.FormValue("password")
	if len(password) < 8 {
		h.render(w, r, "register", "Register", map[string]any{"Error": "Password must be at least 8 characters"})
		return
	}

	key, err := h.auth.NewOTP(strings.TrimSpace(r.FormValue("email")))
	if err != nil {
		http.Error(w, "failed to generate OTP", http.StatusInternalServerError)
		return
	}

	hash, err := h.auth.HashPassword(password)
	if err != nil {
		http.Error(w, "failed to hash password", http.StatusInternalServerError)
		return
	}

	role := models.RoleViewer
	count, err := h.repo.CountUsers(r.Context())
	if err == nil && count == 0 {
		role = models.RoleAdmin
	}

	user := &models.User{
		Organization: h.org.ID,
		Name:         strings.TrimSpace(r.FormValue("name")),
		Email:        strings.TrimSpace(r.FormValue("email")),
		PasswordHash: hash,
		Role:         role,
		OTPSecret:    key.Secret(),
		OTPEnabled:   false,
	}
	if err := h.repo.CreateUser(r.Context(), user); err != nil {
		h.render(w, r, "register", "Register", map[string]any{"Error": "Registration failed. Email may already exist."})
		return
	}

	session, _ := h.sessions.Get(r)
	session.Values["user_id"] = user.ID
	_ = session.Save(r, w)
	http.Redirect(w, r, "/setup-2fa", http.StatusSeeOther)
}

func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
	session, _ := h.sessions.Get(r)
	session.Options.MaxAge = -1
	_ = session.Save(r, w)
	http.Redirect(w, r, "/login", http.StatusSeeOther)
}

func (h *Handler) SetupOTPPage(w http.ResponseWriter, r *http.Request) {
	user := localmiddleware.CurrentUser(r)
	if user == nil {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}

	uri := h.auth.BuildOTPURI(user.Email, user.OTPSecret)
	png, _ := qrcode.Encode(uri, qrcode.Medium, 256)
	dataURI := "data:image/png;base64," + encodeBase64(png)

	h.render(w, r, "setup_otp", "Secure Your Account", map[string]any{
		"Secret": user.OTPSecret,
		"QRCode": dataURI,
	})
}

func (h *Handler) SetupOTP(w http.ResponseWriter, r *http.Request) {
	user := localmiddleware.CurrentUser(r)
	if user == nil {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
		return
	}

	if !h.auth.VerifyOTP(user.OTPSecret, strings.TrimSpace(r.FormValue("code"))) {
		uri := h.auth.BuildOTPURI(user.Email, user.OTPSecret)
		png, _ := qrcode.Encode(uri, qrcode.Medium, 256)
		h.render(w, r, "setup_otp", "Secure Your Account", map[string]any{
			"Error":  "Invalid code. Check your authenticator app.",
			"Secret": user.OTPSecret,
			"QRCode": "data:image/png;base64," + encodeBase64(png),
		})
		return
	}

	if err := h.repo.EnableUserOTP(r.Context(), user.ID); err != nil {
		http.Error(w, "failed to enable otp", http.StatusInternalServerError)
		return
	}

	http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}

func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
	user := localmiddleware.CurrentUser(r)
	h.render(w, r, "dashboard", "Fleet Dashboard", nil, user)
}

func (h *Handler) DashboardNodes(w http.ResponseWriter, r *http.Request) {
	h.renderer.RenderPageTemplate(w, "dashboard", "dashboardNodes", views.ViewData{
		User:    localmiddleware.CurrentUser(r),
		Content: h.dashboardData(r.Context()),
	})
}

func (h *Handler) CreateNode(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseMultipartForm(10 << 20); err != nil {
		http.Error(w, "invalid form", http.StatusBadRequest)
		return
	}

	ip := strings.TrimSpace(r.FormValue("ip_address"))
	username := strings.TrimSpace(r.FormValue("ssh_username"))
	password := r.FormValue("ssh_password")
	name := strings.TrimSpace(r.FormValue("name"))
	if name == "" {
		name = ip
	}

	if ip == "" || username == "" {
		http.Error(w, "ip address and username are required", http.StatusBadRequest)
		return
	}

	keyName, err := saveKeyUpload(r, "ssh_key")
	if err != nil {
		http.Error(w, "failed to save uploaded key", http.StatusInternalServerError)
		return
	}
	if password == "" && keyName == "" {
		http.Error(w, "provide a password or upload a key", http.StatusBadRequest)
		return
	}

	notes := strings.TrimSpace(r.FormValue("notes"))
	tag := strings.TrimSpace(r.FormValue("tag"))
	var groupID *int64
	if raw := strings.TrimSpace(r.FormValue("group_id")); raw != "" {
		parsed, err := strconv.ParseInt(raw, 10, 64)
		if err == nil {
			groupID = &parsed
		}
	}
	if keyName != "" {
		if notes != "" {
			notes += "\n"
		}
		notes += "Uploaded key: " + keyName
	}

	node := &models.Node{
		OrganizationID:     h.org.ID,
		GroupID:            groupID,
		Tag:                tag,
		Name:               name,
		Distro:             "Linux",
		Hostname:           ip,
		IPAddress:          ip,
		SSHPort:            22,
		SSHUsername:        username,
		SSHPassword:        password,
		AutoUpdatesEnabled: r.FormValue("auto_updates_enabled") == "on",
		Notes:              notes,
	}

	if err := h.nodes.SaveNode(r.Context(), node); err != nil {
		http.Error(w, "failed to create node", http.StatusInternalServerError)
		return
	}

	if node.SSHUsername != "" && node.SSHPassword != "" {
		_, _ = h.nodes.RefreshNodeInventory(r.Context(), node)
		_, _ = h.nodes.RefreshNodeStats(r.Context(), node)
	}

	http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}

func (h *Handler) NodeOverview(w http.ResponseWriter, r *http.Request) {
	user := localmiddleware.CurrentUser(r)
	nodeID, err := strconv.ParseInt(chi.URLParam(r, "nodeID"), 10, 64)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	node, err := h.nodes.GetNode(r.Context(), h.org.ID, nodeID)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	if node.SSHUsername != "" && node.SSHPassword != "" && h.nodes.StatsStale(node, time.Second) {
		_, _ = h.nodes.RefreshNodeStats(r.Context(), node)
	}

	runs := h.listRuns(r.Context(), nodeID)
	h.render(w, r, "node", node.Name, nodePageData{
		Node:        node,
		CommandRuns: runs,
	}, user)
}

func (h *Handler) NodeStatsAPI(w http.ResponseWriter, r *http.Request) {
	nodeID, err := strconv.ParseInt(chi.URLParam(r, "nodeID"), 10, 64)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	node, err := h.nodes.GetNode(r.Context(), h.org.ID, nodeID)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	if node.SSHUsername != "" && node.SSHPassword != "" && h.nodes.StatsStale(node, time.Second) {
		_, _ = h.nodes.RefreshNodeStats(r.Context(), node)
	}

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(map[string]any{
		"cpu_usage":      node.CPUUsage,
		"ram_usage":      node.RAMUsage,
		"disk_usage":     node.DiskUsage,
		"uptime_seconds": node.UptimeSeconds,
		"last_seen_at":   node.LastSeenAt,
	})
}

func (h *Handler) NodeConsole(w http.ResponseWriter, r *http.Request) {
	user := localmiddleware.CurrentUser(r)
	nodeID, err := strconv.ParseInt(chi.URLParam(r, "nodeID"), 10, 64)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	node, err := h.nodes.GetNode(r.Context(), h.org.ID, nodeID)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	h.render(w, r, "console", "SSH Console", map[string]any{"Node": node}, user)
}

func (h *Handler) NodeConsoleWebSocket(w http.ResponseWriter, r *http.Request) {
	nodeID, err := strconv.ParseInt(chi.URLParam(r, "nodeID"), 10, 64)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	node, err := h.nodes.GetNode(r.Context(), h.org.ID, nodeID)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	conn, err := h.upgrader.Upgrade(w, r, nil)
	if err != nil {
		return
	}
	defer conn.Close()

	config := &ssh.ClientConfig{
		User:            node.SSHUsername,
		Auth:            []ssh.AuthMethod{ssh.Password(node.SSHPassword)},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Timeout:         15 * time.Second,
	}

	client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", node.IPAddress, node.SSHPort), config)
	if err != nil {
		_ = conn.WriteMessage(websocket.TextMessage, []byte("SSH connection failed: "+err.Error()))
		return
	}
	defer client.Close()

	session, err := client.NewSession()
	if err != nil {
		_ = conn.WriteMessage(websocket.TextMessage, []byte("SSH session failed: "+err.Error()))
		return
	}
	defer session.Close()

	stdin, _ := session.StdinPipe()
	stdout, _ := session.StdoutPipe()
	stderr, _ := session.StderrPipe()

	_ = session.RequestPty("xterm", 24, 80, ssh.TerminalModes{})
	if err := session.Shell(); err != nil {
		_ = conn.WriteMessage(websocket.TextMessage, []byte("SSH shell failed: "+err.Error()))
		return
	}

	copyOutput := func(reader httpReader) {
		buffer := make([]byte, 2048)
		for {
			n, readErr := reader.Read(buffer)
			if n > 0 {
				_ = conn.WriteMessage(websocket.TextMessage, buffer[:n])
			}
			if readErr != nil {
				return
			}
		}
	}

	go copyOutput(stdout)
	go copyOutput(stderr)

	for {
		_, message, readErr := conn.ReadMessage()
		if readErr != nil {
			return
		}
		if _, writeErr := stdin.Write(message); writeErr != nil {
			return
		}
	}
}

func (h *Handler) NodeAction(w http.ResponseWriter, r *http.Request) {
	nodeID, err := strconv.ParseInt(chi.URLParam(r, "nodeID"), 10, 64)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	node, err := h.nodes.GetNode(r.Context(), h.org.ID, nodeID)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	user := localmiddleware.CurrentUser(r)
	var userID *int64
	if user != nil {
		userID = &user.ID
	}

	action := chi.URLParam(r, "action")
	output, actionErr := h.nodes.RunAction(r.Context(), node, action, userID)
	if actionErr != nil {
		http.Error(w, output+"\n"+actionErr.Error(), http.StatusBadGateway)
		return
	}

	http.Redirect(w, r, "/nodes/"+strconv.FormatInt(nodeID, 10), http.StatusSeeOther)
}

func (h *Handler) DeleteNode(w http.ResponseWriter, r *http.Request) {
	nodeID, err := strconv.ParseInt(chi.URLParam(r, "nodeID"), 10, 64)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	if err := h.nodes.DeleteNode(r.Context(), h.org.ID, nodeID); err != nil {
		if err == sql.ErrNoRows {
			http.NotFound(w, r)
			return
		}
		http.Error(w, "failed to delete node", http.StatusInternalServerError)
		return
	}

	http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}

func (h *Handler) NodeQuickCommand(w http.ResponseWriter, r *http.Request) {
	nodeID, err := strconv.ParseInt(chi.URLParam(r, "nodeID"), 10, 64)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	node, err := h.nodes.GetNode(r.Context(), h.org.ID, nodeID)
	if err != nil {
		http.NotFound(w, r)
		return
	}

	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	command := strings.TrimSpace(r.FormValue("command"))
	label := strings.TrimSpace(r.FormValue("label"))
	if command == "" {
		http.Error(w, "command required", http.StatusBadRequest)
		return
	}
	if label == "" {
		label = "custom-command"
	}

	user := localmiddleware.CurrentUser(r)
	var userID *int64
	if user != nil {
		userID = &user.ID
	}

	output, runErr := h.nodes.RunAdHocCommand(r.Context(), node, label, command, userID)
	if runErr != nil {
		http.Error(w, output, http.StatusBadGateway)
		return
	}

	http.Redirect(w, r, "/nodes/"+strconv.FormatInt(nodeID, 10), http.StatusSeeOther)
}

func (h *Handler) GroupsPage(w http.ResponseWriter, r *http.Request) {
	user := localmiddleware.CurrentUser(r)
	nodes, _ := h.nodes.ListNodes(r.Context(), h.org.ID)
	h.render(w, r, "groups", "VM Groups", map[string]any{"Nodes": nodes}, user)
}

func (h *Handler) CreateGroup(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	name := strings.TrimSpace(r.FormValue("name"))
	if name == "" {
		http.Error(w, "group name required", http.StatusBadRequest)
		return
	}

	group := &models.VMGroup{
		OrganizationID: h.org.ID,
		Name:           name,
		Description:    strings.TrimSpace(r.FormValue("description")),
		ColorToken:     "primary",
	}
	if err := h.repo.CreateGroup(r.Context(), group); err != nil {
		http.Error(w, "failed to create group", http.StatusInternalServerError)
		return
	}

	http.Redirect(w, r, "/groups", http.StatusSeeOther)
}

func (h *Handler) AutomationsPage(w http.ResponseWriter, r *http.Request) {
	user := localmiddleware.CurrentUser(r)
	jobs, _ := h.nodes.ListAutomations(r.Context(), h.org.ID)
	nodes, _ := h.nodes.ListNodes(r.Context(), h.org.ID)
	groups, _ := h.repo.ListGroups(r.Context(), h.org.ID)
	tags, _ := h.repo.ListTags(r.Context(), h.org.ID)
	runs, _ := h.nodes.ListJobRuns(r.Context(), h.org.ID)
	h.render(w, r, "automations", "Jobs", jobsPageData{
		Jobs:   jobs,
		Nodes:  nodes,
		Groups: groups,
		Tags:   tags,
		Runs:   runs,
	}, user)
}

func (h *Handler) CreateAutomation(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	var nodeID *int64
	if raw := r.FormValue("node_id"); raw != "" {
		parsed, _ := strconv.ParseInt(raw, 10, 64)
		nodeID = &parsed
	}

	var groupID *int64
	if raw := r.FormValue("group_id"); raw != "" {
		parsed, _ := strconv.ParseInt(raw, 10, 64)
		groupID = &parsed
	}

	tag := strings.TrimSpace(r.FormValue("tag"))
	targetCount := 0
	if nodeID != nil {
		targetCount++
	}
	if groupID != nil {
		targetCount++
	}
	if tag != "" {
		targetCount++
	}
	if targetCount != 1 {
		http.Error(w, "select exactly one target: node, group, or tag", http.StatusBadRequest)
		return
	}

	commandSteps := r.Form["command_steps[]"]
	var commands []string
	for _, step := range commandSteps {
		step = strings.TrimSpace(step)
		if step != "" {
			commands = append(commands, step)
		}
	}
	if len(commands) == 0 {
		http.Error(w, "at least one command is required", http.StatusBadRequest)
		return
	}

	triggerType := r.FormValue("trigger_type")
	schedule := buildSchedule(
		triggerType,
		r.FormValue("schedule_kind"),
		r.FormValue("schedule_hour"),
		r.FormValue("schedule_minute"),
		r.FormValue("schedule_weekday"),
		r.FormValue("interval_value"),
		r.FormValue("interval_unit"),
	)

	job := &models.AutomationJob{
		OrganizationID: h.org.ID,
		NodeID:         nodeID,
		GroupID:        groupID,
		Tag:            tag,
		Name:           r.FormValue("name"),
		TriggerType:    triggerType,
		Schedule:       schedule,
		Command:        strings.Join(commands, "\n"),
		Enabled:        true,
	}
	if err := h.nodes.CreateAutomation(r.Context(), job); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	http.Redirect(w, r, "/automations", http.StatusSeeOther)
}

func (h *Handler) SettingsPage(w http.ResponseWriter, r *http.Request) {
	user := localmiddleware.CurrentUser(r)
	org := h.currentOrganization(r.Context())
	h.render(w, r, "settings", "Theme System", settingsData{
		ThemeVariables: template.CSS(themePreview),
		CurrentTheme:   org.Theme,
		CurrentMode:    org.ThemeMode,
	}, user)
}

func (h *Handler) UpdateTheme(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}

	theme := strings.TrimSpace(r.FormValue("theme"))
	mode := strings.TrimSpace(r.FormValue("mode"))
	if !isAllowedTheme(theme) || !isAllowedMode(mode) {
		http.Error(w, "invalid theme selection", http.StatusBadRequest)
		return
	}

	if err := h.repo.UpdateOrganizationTheme(r.Context(), theme, mode); err != nil {
		http.Error(w, "failed to update theme", http.StatusInternalServerError)
		return
	}

	http.Redirect(w, r, "/settings", http.StatusSeeOther)
}

func (h *Handler) render(w http.ResponseWriter, r *http.Request, page, title string, content any, user ...*models.User) {
	var currentUser *models.User
	if len(user) > 0 {
		currentUser = user[0]
	}
	if currentUser == nil {
		currentUser = localmiddleware.CurrentUser(r)
	}

	org := h.currentOrganization(r.Context())
	groups, _ := h.repo.ListGroups(r.Context(), org.ID)
	tags, _ := h.repo.ListTags(r.Context(), org.ID)

	h.renderer.Render(w, page, views.ViewData{
		Title:           title,
		Shell:           shellForPage(page),
		ThemeClass:      org.Theme,
		ThemeMode:       org.ThemeMode,
		CurrentPath:     r.URL.Path,
		User:            currentUser,
		Organization:    org,
		AvailableGroups: groups,
		AvailableTags:   tags,
		Content:         content,
	})
}

func (h *Handler) dashboardData(ctx context.Context) dashboardData {
	nodes, _ := h.nodes.ListNodes(ctx, h.org.ID)
	grouped := make(map[string][]models.Node)
	var ungrouped []models.Node
	for _, node := range nodes {
		if strings.TrimSpace(node.GroupName) == "" {
			ungrouped = append(ungrouped, node)
			continue
		}
		grouped[node.GroupName] = append(grouped[node.GroupName], node)
	}

	groupNames := make([]string, 0, len(grouped))
	for name := range grouped {
		groupNames = append(groupNames, name)
	}
	sort.Strings(groupNames)

	groups := make([]dashboardNodeGroup, 0, len(groupNames))
	for _, name := range groupNames {
		groups = append(groups, dashboardNodeGroup{
			Name:  name,
			Nodes: grouped[name],
		})
	}

	return dashboardData{
		Groups:    groups,
		Ungrouped: ungrouped,
	}
}

func (h *Handler) currentOrganization(ctx context.Context) models.Organization {
	org, err := h.repo.GetOrganization(ctx)
	if err != nil {
		return h.org
	}
	return org
}

func shellForPage(page string) string {
	switch page {
	case "login", "login_otp", "register":
		return "auth"
	case "console":
		return "console"
	default:
		return "app"
	}
}

func (h *Handler) listRuns(ctx context.Context, nodeID int64) []models.CommandRun {
	rows, err := h.nodesRunQuery(ctx, nodeID)
	if err != nil {
		return nil
	}
	defer rows.Close()

	var runs []models.CommandRun
	for rows.Next() {
		var run models.CommandRun
		if err := rows.Scan(&run.ID, &run.JobID, &run.NodeID, &run.Action, &run.Status, &run.Output, &run.TriggeredBy, &run.StartedAt, &run.FinishedAt); err == nil {
			runs = append(runs, run)
		}
	}
	return runs
}

func (h *Handler) nodesRunQuery(ctx context.Context, nodeID int64) (*sql.Rows, error) {
	return h.nodesDB().QueryContext(ctx, `
		SELECT id, job_id, node_id, action, status, output, triggered_by, started_at, finished_at
		FROM command_runs
		WHERE node_id = ?
		ORDER BY started_at DESC
		LIMIT 12
	`, nodeID)
}

func (h *Handler) nodesDB() *sql.DB {
	return h.repo.DB()
}

type httpReader interface {
	Read(p []byte) (n int, err error)
}

func encodeBase64(raw []byte) string {
	return base64.StdEncoding.EncodeToString(raw)
}

const themePreview = `
:root {
  --color-primary-50: 236 253 245;
  --color-primary-100: 209 250 229;
  --color-primary-200: 167 243 208;
  --color-primary-300: 110 231 183;
  --color-primary-400: 52 211 153;
  --color-primary-500: 16 185 129;
  --color-primary-600: 5 150 105;
  --color-primary-700: 4 120 87;
  --color-primary-800: 6 95 70;
  --color-primary-900: 6 78 59;
  --color-primary-950: 2 44 34;
}`

func buildSchedule(triggerType, kind, hour, minute, weekday, intervalValue, intervalUnit string) string {
	if triggerType == "triggered" {
		return ""
	}

	hour = defaultIfEmpty(hour, "0")
	minute = defaultIfEmpty(minute, "0")

	switch kind {
	case "hourly":
		return fmt.Sprintf("%s * * * *", minute)
	case "weekly":
		weekday = defaultIfEmpty(weekday, "1")
		return fmt.Sprintf("%s %s * * %s", minute, hour, weekday)
	case "interval":
		if intervalUnit == "hours" {
			value := defaultIfEmpty(intervalValue, "1")
			return fmt.Sprintf("%s */%s * * *", minute, value)
		}
		value := defaultIfEmpty(intervalValue, "30")
		return fmt.Sprintf("*/%s * * * *", value)
	default:
		return fmt.Sprintf("%s %s * * *", minute, hour)
	}
}

func defaultIfEmpty(value, fallback string) string {
	if strings.TrimSpace(value) == "" {
		return fallback
	}
	return value
}

func isAllowedTheme(value string) bool {
	switch value {
	case "dark", "light", "green", "red", "blue":
		return true
	default:
		return false
	}
}

func isAllowedMode(value string) bool {
	return value == "dark" || value == "light"
}

func saveKeyUpload(r *http.Request, field string) (string, error) {
	file, header, err := r.FormFile(field)
	if err != nil {
		if err == http.ErrMissingFile {
			return "", nil
		}
		return "", err
	}
	defer file.Close()

	if err := os.MkdirAll(filepath.Join("data", "keys"), 0o755); err != nil {
		return "", err
	}

	safeName := fmt.Sprintf("%d-%s", time.Now().UnixNano(), filepath.Base(header.Filename))
	targetPath := filepath.Join("data", "keys", safeName)
	target, err := os.Create(targetPath)
	if err != nil {
		return "", err
	}
	defer target.Close()

	if _, err := io.Copy(target, file); err != nil {
		return "", err
	}

	return safeName, nil
}
