package web

import (
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"

	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)

// ChartData holds aggregated time-series data for Chart.js.
type ChartData struct {
	Labels       []string  `json:"labels"`
	AvgMemory    []float64 `json:"avg_memory"`
	PeakMemory   []float64 `json:"peak_memory"`
	CatalogLimit float64   `json:"catalog_limit"`
}

// handleApps renders the fleet-wide app list page.
func (s *Server) handleApps(w http.ResponseWriter, r *http.Request) {
	period := r.URL.Query().Get("period")
	since := parsePeriod(period, 7*24*time.Hour)

	sortBy := r.URL.Query().Get("sort")
	order := r.URL.Query().Get("order")

	summary, err := s.store.GetFleetAppSummary(since)
	if err != nil {
		s.logger.Printf("[ERROR] GetFleetAppSummary: %v", err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	sortFleetSummary(summary, sortBy, order)

	totalApps := len(summary)
	totalDeployments := 0
	appsWithErrors := 0
	for _, app := range summary {
		totalDeployments += app.DeploymentCount
		if app.TotalErrors > 0 {
			appsWithErrors++
		}
	}

	csrfToken := s.getCSRFToken(r)
	data := map[string]interface{}{
		"Apps":             summary,
		"Period":           period,
		"TotalApps":        totalApps,
		"TotalDeployments": totalDeployments,
		"AppsWithErrors":   appsWithErrors,
		"Sort":             sortBy,
		"Order":            order,
		"CSRFToken":        csrfToken,
	}
	if err := s.templates.ExecuteTemplate(w, "apps.html", data); err != nil {
		s.logger.Printf("[ERROR] apps.html template: %v", err)
	}
}

// handleAppDetail renders the per-app detail page.
func (s *Server) handleAppDetail(w http.ResponseWriter, r *http.Request, appName string) {
	period := r.URL.Query().Get("period")
	since := parsePeriod(period, 7*24*time.Hour)
	// Part H: ?customer=<id> narrows Known Issues to rows affecting that customer
	// (the customer page's per-app drill-down carries it). The unfiltered page stays
	// the fleet view.
	customerFilter := r.URL.Query().Get("customer")
	showDismissed := r.URL.Query().Get("show_dismissed") == "1"

	customers, _ := s.store.GetAppCustomerBreakdown(appName, since)
	history, _ := s.store.GetAppTelemetryHistory(appName, since)
	// Part F fix: the issues query gets the SAME period cutoff the chart uses (it used
	// to ignore the selector — a 24h view showed 25-day-old rows).
	issues, _ := s.store.GetAppIssues(appName, since, showDismissed, 20)
	if customerFilter != "" {
		filtered := issues[:0]
		for _, is := range issues {
			for _, c := range is.AffectedCustomers {
				if c == customerFilter {
					filtered = append(filtered, is)
					break
				}
			}
		}
		issues = filtered
	}

	// Get fleet summary to find this app's summary
	fleetAll, _ := s.store.GetFleetAppSummary(since)
	var appSummary *store.FleetAppSummary
	for i := range fleetAll {
		if fleetAll[i].AppName == appName {
			appSummary = &fleetAll[i]
			break
		}
	}

	// Suggested mem_limit: ceil(P95 * 1.2), rounded up to nearest 32M
	var suggestedLimit int
	if appSummary != nil && appSummary.P95MemoryMB > 0 {
		raw := appSummary.P95MemoryMB * 1.2
		suggestedLimit = ((int(raw) + 31) / 32) * 32
	}

	chartData := aggregateHistoryForChart(history, appSummary)

	csrfToken := s.getCSRFToken(r)
	data := map[string]interface{}{
		"AppName":        appName,
		"Summary":        appSummary,
		"Customers":      customers,
		"Issues":         issues,
		"ChartData":      chartData,
		"SuggestedLimit": suggestedLimit,
		"Period":         period,
		"CustomerFilter": customerFilter,
		"ShowDismissed":  showDismissed,
		"CSRFToken":      csrfToken,
		"Flash":          r.URL.Query().Get("flash"),
	}
	if err := s.templates.ExecuteTemplate(w, "app_detail.html", data); err != nil {
		s.logger.Printf("[ERROR] app_detail.html template: %v", err)
	}
}

// handleResetAppTelemetry deletes all telemetry and issues for an app, then redirects back.
func (s *Server) handleResetAppTelemetry(w http.ResponseWriter, r *http.Request, appName string) {
	telRows, err := s.store.DeleteAppTelemetry(appName)
	if err != nil {
		s.logger.Printf("[ERROR] Failed to reset telemetry for %s: %v", appName, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}
	issueRows, err := s.store.DeleteAppIssues(appName)
	if err != nil {
		s.logger.Printf("[ERROR] Failed to reset issues for %s: %v", appName, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}
	s.logger.Printf("[INFO] Telemetry reset for %s: %d telemetry rows, %d issues deleted", appName, telRows, issueRows)

	period := r.URL.Query().Get("period")
	target := "/apps/" + appName + "?flash=telemetry_reset"
	if period != "" {
		target += "&period=" + period
	}
	http.Redirect(w, r, target, http.StatusSeeOther)
}

// handleDismissAppIssues handles POST requests to dismiss selected or all issues for an app.
// Dismissal, not deletion (Part G): the controller re-sends recurring issues every report,
// so a hard delete reappears minutes later. A dismissed row stays hidden until a genuinely
// NEW occurrence (last_seen > dismissed_at) resurfaces it in upsertAppIssue.
func (s *Server) handleDismissAppIssues(w http.ResponseWriter, r *http.Request, appName string) {
	action := r.FormValue("action")

	var dismissedCount int64
	var err error

	switch action {
	case "all":
		dismissedCount, err = s.store.DismissAppIssues(appName)
	case "selected":
		r.ParseForm()
		idStrs := r.Form["issue_ids"]
		if len(idStrs) == 0 {
			period := r.URL.Query().Get("period")
			target := "/apps/" + appName + "?flash=no_issues_selected"
			if period != "" {
				target += "&period=" + period
			}
			http.Redirect(w, r, target, http.StatusSeeOther)
			return
		}
		ids := make([]int, 0, len(idStrs))
		for _, s := range idStrs {
			id, err := strconv.Atoi(s)
			if err == nil && id > 0 {
				ids = append(ids, id)
			}
		}
		dismissedCount, err = s.store.DismissAppIssuesByIDs(ids)
	default:
		http.Error(w, "Invalid action", http.StatusBadRequest)
		return
	}

	if err != nil {
		s.logger.Printf("[ERROR] Failed to dismiss issues for %s: %v", appName, err)
		http.Error(w, "Internal error", http.StatusInternalServerError)
		return
	}

	s.logger.Printf("[INFO] Dismissed %d issues for %s (action=%s)", dismissedCount, appName, action)

	period := r.URL.Query().Get("period")
	target := "/apps/" + appName + "?flash=issues_dismissed"
	if period != "" {
		target += "&period=" + period
	}
	http.Redirect(w, r, target, http.StatusSeeOther)
}

// parsePeriod converts a period string to a time.Time cutoff.
func parsePeriod(s string, defaultDur time.Duration) time.Time {
	switch s {
	case "24h":
		return time.Now().Add(-24 * time.Hour)
	case "7d":
		return time.Now().Add(-7 * 24 * time.Hour)
	case "30d":
		return time.Now().Add(-30 * 24 * time.Hour)
	default:
		return time.Now().Add(-defaultDur)
	}
}

// sortFleetSummary sorts the fleet summary slice in place.
func sortFleetSummary(summary []store.FleetAppSummary, sortBy, order string) {
	desc := order != "asc"
	sort.Slice(summary, func(i, j int) bool {
		var less bool
		switch sortBy {
		case "memory":
			less = summary[i].AvgMemoryMB < summary[j].AvgMemoryMB
		case "errors":
			less = summary[i].TotalErrors < summary[j].TotalErrors
		default: // deployments
			less = summary[i].DeploymentCount < summary[j].DeploymentCount
		}
		if desc {
			return !less
		}
		return less
	})
}

// aggregateHistoryForChart groups history points into hourly buckets for Chart.js.
func aggregateHistoryForChart(history []store.AppTelemetryPoint, summary *store.FleetAppSummary) ChartData {
	type bucket struct {
		avgMemSum  float64
		peakMemMax float64
		count      int
	}

	buckets := make(map[string]*bucket)
	var bucketOrder []string

	for _, p := range history {
		key := p.ReportedAt.UTC().Format("2006-01-02 15:00")
		if _, ok := buckets[key]; !ok {
			buckets[key] = &bucket{}
			bucketOrder = append(bucketOrder, key)
		}
		b := buckets[key]
		b.avgMemSum += p.MemoryAvgMB
		if p.MemoryPeakMB > b.peakMemMax {
			b.peakMemMax = p.MemoryPeakMB
		}
		b.count++
	}

	cd := ChartData{
		Labels:     make([]string, 0, len(bucketOrder)),
		AvgMemory:  make([]float64, 0, len(bucketOrder)),
		PeakMemory: make([]float64, 0, len(bucketOrder)),
	}

	for _, key := range bucketOrder {
		b := buckets[key]
		cd.Labels = append(cd.Labels, key)
		avgMem := 0.0
		if b.count > 0 {
			avgMem = b.avgMemSum / float64(b.count)
		}
		cd.AvgMemory = append(cd.AvgMemory, round2(avgMem))
		cd.PeakMemory = append(cd.PeakMemory, round2(b.peakMemMax))
	}

	if summary != nil {
		cd.CatalogLimit = parseLimitMB(summary.CatalogLimit)
	}

	return cd
}

// parseLimitMB parses a memory limit string like "512M" or "2G" to MB float64.
func parseLimitMB(s string) float64 {
	s = strings.TrimSpace(s)
	if s == "" {
		return 0
	}
	s = strings.ToUpper(s)
	if strings.HasSuffix(s, "G") {
		v, _ := strconv.ParseFloat(s[:len(s)-1], 64)
		return v * 1024
	}
	if strings.HasSuffix(s, "M") {
		v, _ := strconv.ParseFloat(s[:len(s)-1], 64)
		return v
	}
	v, _ := strconv.ParseFloat(s, 64)
	return v
}

// round2 rounds a float64 to 2 decimal places.
func round2(v float64) float64 {
	return float64(int(v*100+0.5)) / 100
}

// memoryColor returns a CSS class based on current memory vs catalog limit.
func memoryColor(currentMB float64, limitStr string) string {
	limit := parseLimitMB(limitStr)
	if limit <= 0 {
		return ""
	}
	ratio := currentMB / limit
	if ratio >= 1.0 {
		return "mem-danger"
	}
	if ratio >= 0.5 {
		return "mem-warn"
	}
	return "mem-ok"
}

// accuracyClass returns accuracy indicator CSS class for P95 vs catalog limit.
func accuracyClass(p95MB float64, limitStr string) string {
	if p95MB <= 0 || limitStr == "" {
		return ""
	}
	limit := parseLimitMB(limitStr)
	if limit <= 0 {
		return ""
	}
	if p95MB > limit {
		return "danger"
	}
	if p95MB*2 > limit {
		return "warn"
	}
	return "ok"
}

// getCSRFToken retrieves the CSRF token from the session cookie.
func (s *Server) getCSRFToken(r *http.Request) string {
	cookie, err := r.Cookie("hub_session")
	if err != nil {
		return ""
	}
	s.sessionsMu.RLock()
	defer s.sessionsMu.RUnlock()
	if sess, ok := s.sessions[cookie.Value]; ok {
		return sess.csrfToken
	}
	return ""
}
