package web

import (
	"fmt"
	"hash/crc32"
	"log"
	"strings"
	"sync"
	"time"

	"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
	"gitea.dooplex.hu/admin/felhom-controller/internal/config"
	"gitea.dooplex.hu/admin/felhom-controller/internal/monitor"
	"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)

// Alert represents a persistent dashboard alert banner.
type Alert struct {
	ID       string   // unique identifier for filtering
	Level    string   // "error", "warning", "info"
	Message  string   // Hungarian display text
	Link     string   // optional link to relevant page
	LinkText string   // link display text
	PageOnly []string // if non-empty, only show on these pages (e.g., ["dashboard", "monitoring"])
	Inline   bool     // if true, rendered by page template inline, not in layout banner
}

// AlertManager generates and stores dashboard alerts from health check results.
// Alerts are state-based (not event-based) — they reflect current system state
// and are regenerated after each health check cycle.
type AlertManager struct {
	mu              sync.RWMutex
	alerts          []Alert
	logger          *log.Logger
	hubPushStatusFn func() HubPushStatusData
	// agentChannelAlert is set/cleared by the channel-health checker (out-of-band from Refresh, which
	// is health-report-driven). nil = channel up. It is prepended in GetAlerts so a dead controller→
	// agent link (disk/storage UI broken) is always visible regardless of the health-report cycle.
	agentChannelAlert *Alert
}

// NewAlertManager creates a new AlertManager.
func NewAlertManager(logger *log.Logger) *AlertManager {
	return &AlertManager{
		logger: logger,
	}
}

// SetHubPushStatus sets the hub push status callback for generating hub alerts.
func (am *AlertManager) SetHubPushStatus(fn func() HubPushStatusData) {
	am.mu.Lock()
	am.hubPushStatusFn = fn
	am.mu.Unlock()
}

// SetAgentChannelAlert sets (down=true) or clears (down=false) the controller→agent channel-down
// dashboard banner. Called by the channel-health checker each probe (idempotent). msg is the short
// Hungarian display line.
func (am *AlertManager) SetAgentChannelAlert(down bool, msg string) {
	am.mu.Lock()
	defer am.mu.Unlock()
	if !down {
		am.agentChannelAlert = nil
		return
	}
	am.agentChannelAlert = &Alert{
		ID:       "agent-channel-down",
		Level:    "error",
		Message:  msg,
		Link:     "/settings",
		LinkText: "Beállítások",
	}
}

// Refresh regenerates alerts from the latest health check report and config state.
// Called after each health check cycle (every 5 minutes) and on storage state changes.
func (am *AlertManager) Refresh(report *monitor.HealthReport, cfg *config.Config, backupMgr *backup.Manager, updateAvailable bool, latestVersion string, storagePaths ...[]settings.StoragePath) {
	var alerts []Alert

	// Disconnected storage alerts (top-level error banners on all pages)
	if len(storagePaths) > 0 {
		for _, sp := range storagePaths[0] {
			if sp.Disconnected {
				label := sp.Label
				if label == "" {
					label = sp.Path
				}
				alerts = append(alerts, Alert{
					ID:       "storage-disconnected-" + simpleHash(sp.Path),
					Level:    "error",
					Message:  fmt.Sprintf("Meghajtó leválasztva: %s (%s)", label, sp.Path),
					Link:     "/settings",
					LinkText: "Beállítások",
				})
			}
		}
	}

	// From health check issues (critical)
	for _, issue := range report.Issues {
		alerts = append(alerts, Alert{
			ID:       "health-" + simpleHash(issue),
			Level:    "error",
			Message:  issue,
			Link:     "/monitoring",
			LinkText: "Rendszermonitor",
		})
	}

	// From health check warnings
	for _, w := range report.Warnings {
		alert := Alert{
			ID:       "health-" + simpleHash(w),
			Level:    "warning",
			Message:  w,
			Link:     "/monitoring",
			LinkText: "Rendszermonitor",
		}
		// Disk-related warnings rendered inline under storage bars, not in top banner
		if strings.Contains(w, "meghajtón") || strings.Contains(w, "adattároló") || strings.Contains(w, "meghajtó") {
			alert.ID = "disk-not-separate"
			alert.PageOnly = []string{"dashboard", "monitoring"}
			alert.Inline = true
		}
		alerts = append(alerts, alert)
	}

	// Hub connection status
	if !cfg.Hub.Enabled || cfg.Hub.URL == "" {
		alerts = append(alerts, Alert{
			ID:       "hub-disabled",
			Level:    "warning",
			Message:  "Hub kapcsolat kikapcsolva — a központi monitoring nem aktív",
			Link:     "/monitoring",
			LinkText: "Rendszermonitor",
		})
	} else if am.hubPushStatusFn != nil {
		ps := am.hubPushStatusFn()
		if ps.LastError != "" && (ps.LastSuccess.IsZero() || time.Since(ps.LastSuccess) > 30*time.Minute) {
			alerts = append(alerts, Alert{
				ID:       "hub-unreachable",
				Level:    "error",
				Message:  fmt.Sprintf("Hub nem elérhető — utolsó hiba: %s", ps.LastError),
				Link:     "/monitoring",
				LinkText: "Rendszermonitor",
			})
		}
	}

	// Backup disabled
	if !cfg.Backup.Enabled {
		alerts = append(alerts, Alert{
			ID:       "backup-disabled",
			Level:    "warning",
			Message:  "A biztonsági mentés nincs bekapcsolva",
			Link:     "/settings",
			LinkText: "Beállítások",
		})
	}

	// Update available
	if updateAvailable && latestVersion != "" {
		alerts = append(alerts, Alert{
			ID:       "update-available",
			Level:    "info",
			Message:  fmt.Sprintf("Új controller verzió elérhető: %s", latestVersion),
			Link:     "/settings",
			LinkText: "Frissítés",
		})
	}

	// Sort: errors first, then warnings, then info
	sortAlerts(alerts)

	am.mu.Lock()
	am.alerts = alerts
	am.mu.Unlock()
}

// GetAlerts returns a copy of the current alerts, optionally excluding specific IDs.
func (am *AlertManager) GetAlerts(excludeIDs ...string) []Alert {
	am.mu.RLock()
	defer am.mu.RUnlock()

	if len(am.alerts) == 0 && am.agentChannelAlert == nil {
		return nil
	}

	exclude := make(map[string]bool, len(excludeIDs))
	for _, id := range excludeIDs {
		exclude[id] = true
	}

	var result []Alert
	// Channel-down is prepended (highest priority — the agent link being dead breaks disk/storage UI).
	if am.agentChannelAlert != nil && !exclude[am.agentChannelAlert.ID] {
		result = append(result, *am.agentChannelAlert)
	}
	for _, a := range am.alerts {
		if exclude[a.ID] {
			continue
		}
		result = append(result, a)
	}

	// Cap at 5 visible alerts
	if len(result) > 5 {
		overflow := len(result) - 5
		result = result[:5]
		result = append(result, Alert{
			ID:      "overflow",
			Level:   "info",
			Message: fmt.Sprintf("+ %d további figyelmeztetés", overflow),
			Link:    "/monitoring",
		})
	}

	return result
}

// GetInlineAlerts returns alerts marked as Inline for a specific page.
func (am *AlertManager) GetInlineAlerts(page string) []Alert {
	am.mu.RLock()
	defer am.mu.RUnlock()

	var result []Alert
	for _, a := range am.alerts {
		if !a.Inline {
			continue
		}
		if len(a.PageOnly) == 0 {
			result = append(result, a)
			continue
		}
		for _, p := range a.PageOnly {
			if p == page {
				result = append(result, a)
				break
			}
		}
	}
	return result
}

// simpleHash returns a short deterministic hash for deduplication.
func simpleHash(s string) string {
	return fmt.Sprintf("%08x", crc32.ChecksumIEEE([]byte(s)))
}

// sortAlerts sorts alerts by severity: error > warning > info.
func sortAlerts(alerts []Alert) {
	levelOrder := map[string]int{"error": 0, "warning": 1, "info": 2}
	for i := 1; i < len(alerts); i++ {
		for j := i; j > 0 && levelOrder[alerts[j].Level] < levelOrder[alerts[j-1].Level]; j-- {
			alerts[j], alerts[j-1] = alerts[j-1], alerts[j]
		}
	}
}

func countLevel(alerts []Alert, level string) int {
	n := 0
	for _, a := range alerts {
		if a.Level == level {
			n++
		}
	}
	return n
}
