package eventsources

import (
	"context"
	"fmt"
	"strings"
	"time"

	"git.jabali-panel.com/shukivaknin/jabali2/panel-api/internal/models"
	"git.jabali-panel.com/shukivaknin/jabali2/panel-api/internal/notifications"
	"git.jabali-panel.com/shukivaknin/jabali2/panel-api/internal/repository"
)

const (
	malwareTick = 30 * time.Second
)

// runMalware drains malware_events rows where notified=0 into the M14
// dispatcher. ADR-0072 + plans/m33-malware-detection.md Step 7.
//
// Severity gating:
//   - file_hit / scan_completed → settings.notify_threshold floor
//   - critical events bypass the threshold floor (operator opted into
//     "info-only" threshold should still see critical).
//
// Marks notified=1 after dispatch attempt regardless of dispatch
// outcome — the dispatcher itself records per-channel success in
// notification_history. We do not want a stuck channel (e.g. broken
// webhook) to cause the same event to re-publish forever.
func runMalware(ctx context.Context, d Deps) {
	if d.MalwareEvents == nil {
		d.Log.Debug("eventsources: malware disabled (no MalwareEvents repo)")
		return
	}
	tick := time.NewTicker(malwareTick)
	defer tick.Stop()
	malwarePass(ctx, d)
	for {
		select {
		case <-ctx.Done():
			return
		case <-tick.C:
			malwarePass(ctx, d)
		}
	}
}

func malwarePass(ctx context.Context, d Deps) {
	threshold := models.MalwareSeverityWarn
	if d.MalwareSettings != nil {
		if s, err := d.MalwareSettings.Get(ctx); err == nil && s != nil {
			threshold = s.NotifyThreshold
		}
	}
	rows, err := d.MalwareEvents.ListUnnotified(ctx, 100)
	if err != nil {
		d.Log.Warn("eventsources: list unnotified malware events failed", "err", err)
		return
	}
	for _, ev := range rows {
		if !shouldDispatchMalware(ev, threshold) {
			// Suppressed by threshold — still mark notified so we don't
			// re-evaluate the same row on every tick.
			if err := d.MalwareEvents.MarkNotified(ctx, ev.ID); err != nil {
				d.Log.Warn("eventsources: mark notified (suppressed)", "id", ev.ID, "err", err)
			}
			continue
		}
		title, body := renderMalwareNotification(ev)
		_, err := d.Queue.Publish(ctx, notifications.Envelope{
			EventKind: malwareEventKind(ev),
			Severity:  mapMalwareSeverity(ev),
			Title:     title,
			Body:      body,
			Deeplink:  "/jabali-admin/security?tab=malware",
		})
		if err != nil {
			d.Log.Warn("eventsources: publish malware envelope failed", "id", ev.ID, "err", err)
		}
		if err := d.MalwareEvents.MarkNotified(ctx, ev.ID); err != nil {
			d.Log.Warn("eventsources: mark notified failed", "id", ev.ID, "err", err)
		}
	}
}

// shouldDispatchMalware applies the severity threshold.
func shouldDispatchMalware(ev models.MalwareEvent, threshold string) bool {
	if ev.Severity == models.MalwareSeverityCritical {
		return true
	}
	switch threshold {
	case models.MalwareSeverityCritical:
		return ev.Severity == models.MalwareSeverityCritical
	case models.MalwareSeverityWarn:
		return ev.Severity == models.MalwareSeverityWarn || ev.Severity == models.MalwareSeverityCritical
	default: // info — fire everything
		return true
	}
}

// malwareEventKind selects a stable EventKind string used by
// notification_event_settings + dedupe.
func malwareEventKind(ev models.MalwareEvent) string {
	if ev.Severity == models.MalwareSeverityCritical {
		return "malware.realtime.critical"
	}
	return "malware.quarantine.added"
}

// mapMalwareSeverity bridges malware_events.severity → M14 severity
// constants. M14 uses warning/error/critical; M33 uses warn/critical/info.
func mapMalwareSeverity(ev models.MalwareEvent) string {
	switch ev.Severity {
	case models.MalwareSeverityCritical:
		return models.NotificationSeverityCritical
	case models.MalwareSeverityWarn:
		return models.NotificationSeverityWarning
	default:
		return models.NotificationSeverityInfo
	}
}

// renderMalwareNotification produces the title + body. Templates kept
// inline (single source of truth) — M14 dispatcher per-channel
// rendering uses these as the canonical text and channel adapters
// (Slack blocks, Discord embeds) build from them.
func renderMalwareNotification(ev models.MalwareEvent) (string, string) {
	user := "system"
	if ev.UserID != nil && *ev.UserID != "" {
		user = *ev.UserID
	}
	signature := "—"
	if ev.Signature != nil && *ev.Signature != "" {
		signature = *ev.Signature
	}
	title := fmt.Sprintf("Malware detected on %s: %s", user, signature)
	var b strings.Builder
	b.WriteString("Source: ")
	b.WriteString(ev.Source)
	b.WriteString(" · type: ")
	b.WriteString(ev.EventType)
	b.WriteString(" · severity: ")
	b.WriteString(ev.Severity)
	if ev.TargetPath != nil && *ev.TargetPath != "" {
		b.WriteString("\nPath: ")
		b.WriteString(*ev.TargetPath)
	}
	if ev.TargetPID != nil {
		b.WriteString(fmt.Sprintf("\nPID: %d", *ev.TargetPID))
	}
	b.WriteString("\nView: /jabali-admin/security?tab=malware")
	return title, b.String()
}

// Compile-time guard: contains+other helpers are package-level (used
// by sources.go shouldFire). Adding a no-op reference so static
// analysis sees this file compiles in isolation against repo+models.
var _ repository.MalwareEventRepository = (repository.MalwareEventRepository)(nil)
