package ui

import (
	"context"
	"fmt"
	"log/slog"
	"net/http"
	"time"

	"github.com/go-chi/chi/v5"
	"nerd-monitor/internal/api"
	"nerd-monitor/internal/auth"
	"nerd-monitor/internal/store"
	"nerd-monitor/views"
)

// Handler serves UI pages.
type Handler struct {
	store       *store.Store
	auth        *auth.Manager
	broadcaster interface {
		AddClient(chan string)
		RemoveClient(chan string)
	}
}

// New creates a new UI handler.
func New(s *store.Store, a *auth.Manager) *Handler {
	return &Handler{
		store:       s,
		auth:        a,
		broadcaster: api.GetAPIBroadcaster(),
	}
}

// Dashboard renders the dashboard page.
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
	slog.Debug("Incoming dashboard request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)

	slog.Debug("Rendering dashboard", "agentCount", len(h.store.GetAllAgents()), "staleAgentCount", len(h.getStaleAgents()))
	agents := h.store.GetAllAgents()
	staleAgents := h.getStaleAgents()

	component := views.Dashboard(agents, staleAgents)
	component.Render(context.Background(), w)
	slog.Debug("Dashboard rendered successfully")
}

// AgentDetail renders the agent detail page.
func (h *Handler) AgentDetail(w http.ResponseWriter, r *http.Request) {
	slog.Debug("Incoming agent detail request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)

	agentID := chi.URLParam(r, "id")
	slog.Debug("Retrieving agent detail", "agentID", agentID)
	agent := h.store.GetAgent(agentID)

	if agent == nil {
		slog.Warn("Agent not found for detail view", "agentID", agentID)
		http.NotFound(w, r)
		return
	}

	component := views.AgentDetail(agent)
	component.Render(context.Background(), w)
	slog.Debug("Agent detail rendered successfully", "agentID", agentID)
}

// Login renders the login page.
func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
	slog.Debug("Incoming login request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)

	if r.Method == http.MethodGet {
		slog.Debug("Rendering login page")
		component := views.LoginPage("")
		component.Render(context.Background(), w)
		return
	}

	if r.Method == http.MethodPost {
		username := r.FormValue("username")
		password := r.FormValue("password")

		slog.Debug("Attempting login", "username", username, "remoteAddr", r.RemoteAddr)
		token, err := h.auth.Login(username, password)
		if err != nil {
			slog.Warn("Login failed - invalid credentials", "username", username, "remoteAddr", r.RemoteAddr, "error", err)
			component := views.LoginPage("Invalid credentials")
			component.Render(context.Background(), w)
			return
		}

		slog.Info("Login successful", "username", username, "remoteAddr", r.RemoteAddr)
		http.SetCookie(w, &http.Cookie{
			Name:     "session_token",
			Value:    token,
			Path:     "/",
			MaxAge:   86400,
			HttpOnly: true,
			SameSite: http.SameSiteLaxMode,
		})

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

	slog.Warn("Invalid method for login", "method", r.Method)
	http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}

// Logout handles logout.
func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) {
	slog.Debug("Incoming logout request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)

	if r.Method != http.MethodPost {
		slog.Warn("Invalid method for logout", "method", r.Method)
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	cookie, err := r.Cookie("session_token")
	if err == nil {
		slog.Info("Logging out user", "remoteAddr", r.RemoteAddr)
		h.auth.Logout(cookie.Value)
	} else {
		slog.Debug("Logout attempted without valid session", "remoteAddr", r.RemoteAddr)
	}

	http.SetCookie(w, &http.Cookie{
		Name:     "session_token",
		Value:    "",
		Path:     "/",
		MaxAge:   -1,
		HttpOnly: true,
	})

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

// RemoveStaleAgents handles bulk removal of stale agents.
func (h *Handler) RemoveStaleAgents(w http.ResponseWriter, r *http.Request) {
	slog.Debug("Incoming remove stale agents request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)

	if r.Method != http.MethodPost {
		slog.Warn("Invalid method for remove stale agents", "method", r.Method)
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	agentIDs := r.Form["agent_ids"]
	slog.Info("Removing stale agents", "count", len(agentIDs), "agentIDs", agentIDs)
	for _, id := range agentIDs {
		h.store.DeleteAgent(id)
	}

	slog.Info("Stale agents removed successfully", "count", len(agentIDs))
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

// DeleteAgent handles single agent deletion.
func (h *Handler) DeleteAgent(w http.ResponseWriter, r *http.Request) {
	slog.Debug("Incoming delete agent request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)

	if r.Method != http.MethodPost {
		slog.Warn("Invalid method for delete agent", "method", r.Method)
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	agentID := chi.URLParam(r, "id")
	slog.Info("Deleting agent", "agentID", agentID)
	h.store.DeleteAgent(agentID)

	slog.Info("Agent deleted successfully", "agentID", agentID)
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

// UpdateAgentHostname handles hostname updates for agents.
func (h *Handler) UpdateAgentHostname(w http.ResponseWriter, r *http.Request) {
	slog.Debug("Incoming update agent hostname request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)

	if r.Method != http.MethodPost {
		slog.Warn("Invalid method for update agent hostname", "method", r.Method)
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	agentID := chi.URLParam(r, "id")
	hostname := r.FormValue("hostname")

	if hostname == "" {
		slog.Warn("Empty hostname provided for update", "agentID", agentID)
		http.Error(w, "Hostname cannot be empty", http.StatusBadRequest)
		return
	}

	slog.Info("Updating agent hostname", "agentID", agentID, "newHostname", hostname)
	err := h.store.UpdateHostname(agentID, hostname)
	if err != nil {
		slog.Warn("Failed to update hostname - agent not found", "agentID", agentID, "error", err)
		http.NotFound(w, r)
		return
	}

	slog.Info("Agent hostname updated successfully", "agentID", agentID, "hostname", hostname)
	http.Redirect(w, r, "/agents/"+agentID, http.StatusSeeOther)
}

// getStaleAgents returns agents that haven't reported in 6 months.
func (h *Handler) getStaleAgents() []*store.AgentStats {
	const staleThreshold = 6 * 30 * 24 * time.Hour
	allAgents := h.store.GetAllAgents()

	var stale []*store.AgentStats
	for _, agent := range allAgents {
		if time.Since(agent.LastSeen) > staleThreshold {
			stale = append(stale, agent)
		}
	}

	return stale
}

// GetDashboardTable returns HTML fragment for the agent table.
func (h *Handler) GetDashboardTable(w http.ResponseWriter, r *http.Request) {
	slog.Debug("HTMX dashboard table request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)

	agents := h.store.GetAllAgents()

	w.Header().Set("Content-Type", "text/html")
	w.Header().Set("Cache-Control", "no-cache")

	if len(agents) == 0 {
		w.Write([]byte(`<div class="alert alert-info">
			<strong>ℹ️ No agents connected</strong>
			<p style="margin-top: 0.5rem; font-size: 0.875rem;">
				Start an agent to see its statistics appear here.
			</p>
		</div>`))
		return
	}

	w.Write([]byte(`<div class="card">
		<div class="card-title">Active Agents</div>
		<table>
			<thead>
				<tr>
					<th>Hostname</th>
					<th>CPU Usage</th>
					<th>Memory</th>
					<th>Disk</th>
					<th>Last Seen</th>
				</tr>
			</thead>
			<tbody>`))

	for _, agent := range agents {
		// Determine online status (agent is online if reported within last 15 seconds)
		isOnline := time.Since(agent.LastSeen) < 15*time.Second
		statusClass := "status-red"
		statusText := "Offline"
		if isOnline {
			statusClass = "status-green"
			statusText = "Online"
		}

		row := fmt.Sprintf(`<tr>
			<td>
				<div style="display: flex; align-items: center; gap: 0.5rem;">
					<span class="status-badge %s" style="margin: 0;">%s</span>
					<a href="/agents/%s" style="color: #3b82f6; text-decoration: none;">%s</a>
				</div>
			</td>
			<td>%.1f%%</td>
			<td>%s / %s</td>
			<td>%s / %s</td>
			<td class="timestamp">%s</td>
		</tr>`,
			statusClass,
			statusText,
			agent.ID,
			agent.Hostname,
			agent.CPUUsage,
			formatBytes(agent.RAMUsage),
			formatBytes(agent.RAMTotal),
			formatBytes(agent.DiskUsage),
			formatBytes(agent.DiskTotal),
			agent.LastSeen.Format("2006-01-02 15:04:05"))
		w.Write([]byte(row))
	}

	w.Write([]byte(`</tbody></table></div>`))
}

// GetAgentStats returns HTML fragment for agent statistics.
func (h *Handler) GetAgentStats(w http.ResponseWriter, r *http.Request) {
	slog.Debug("HTMX agent stats request", "method", r.Method, "path", r.URL.Path, "remoteAddr", r.RemoteAddr)

	agentID := chi.URLParam(r, "id")
	if agentID == "" {
		slog.Warn("Missing agent ID in stats request", "remoteAddr", r.RemoteAddr)
		http.Error(w, "missing agent id", http.StatusBadRequest)
		return
	}

	agent := h.store.GetAgent(agentID)
	if agent == nil {
		slog.Warn("Agent not found for stats request", "agentID", agentID)
		http.Error(w, "agent not found", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "text/html")
	w.Header().Set("Cache-Control", "no-cache")

	// Return just the stats content that should be updated
	statsHTML := fmt.Sprintf(`<div style="font-size: 2rem; margin: 1rem 0;">%.1f%%</div>
	<div class="progress-bar">
		<div class="progress-fill" style="width: %.1f%%"></div>
	</div>
	<div style="margin-top: 1rem; font-size: 0.875rem; color: #94a3b8;">
		Last updated: %s
	</div>`,
		agent.CPUUsage,
		agent.CPUUsage,
		agent.LastSeen.Format("2006-01-02 15:04:05"))

	w.Write([]byte(statsHTML))
}

// Events provides Server-Sent Events for real-time updates.
func (h *Handler) Events(w http.ResponseWriter, r *http.Request) {
	slog.Info("SSE connection established", "remoteAddr", r.RemoteAddr)

	// Set headers for Server-Sent Events
	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	w.Header().Set("Access-Control-Allow-Origin", "*")

	// Create a channel for this client
	clientChan := make(chan string, 10)
	h.broadcaster.AddClient(clientChan)

	// Clean up when client disconnects
	go func() {
		<-r.Context().Done()
		slog.Debug("SSE client disconnected", "remoteAddr", r.RemoteAddr)
		h.broadcaster.RemoveClient(clientChan)
	}()

	// Send initial connection event
	fmt.Fprintf(w, "event: connected\ndata: {}\n\n")
	w.(http.Flusher).Flush()

	// Send a test event after 2 seconds
	go func() {
		time.Sleep(2 * time.Second)
		select {
		case clientChan <- "event: test\ndata: {\"message\": \"SSE working\"}\n\n":
		default:
		}
	}()

	// Listen for broadcast messages
	for {
		select {
		case <-r.Context().Done():
			return
		case message := <-clientChan:
			slog.Debug("Sending SSE message to client", "remoteAddr", r.RemoteAddr, "message", message)
			fmt.Fprintf(w, "%s\n", message)
			w.(http.Flusher).Flush()
		}
	}
}

// formatBytes converts bytes to human-readable format.
func formatBytes(bytes uint64) string {
	const (
		kb = 1024
		mb = kb * 1024
		gb = mb * 1024
	)

	switch {
	case bytes >= gb:
		return fmt.Sprintf("%.1f GB", float64(bytes)/float64(gb))
	case bytes >= mb:
		return fmt.Sprintf("%.1f MB", float64(bytes)/float64(mb))
	case bytes >= kb:
		return fmt.Sprintf("%.1f KB", float64(bytes)/float64(kb))
	default:
		return fmt.Sprintf("%d B", bytes)
	}
}
