// Package scheduler enthält den Display-Zeitplan-Scheduler.
// Er prüft jede Minute ob ein Screen ein- oder ausgeschaltet werden soll.
package scheduler

import (
	"context"
	"log/slog"
	"time"

	"git.az-it.net/az/morz-infoboard/server/backend/internal/store"
)

// DisplayCommander sendet einen Display-Befehl per MQTT.
type DisplayCommander interface {
	SendDisplayCommand(screenSlug, action string) error
}

// ScreenSlugGetter lädt den Slug für eine Screen-ID.
type ScreenSlugGetter interface {
	GetByID(ctx context.Context, id string) (*store.Screen, error)
}

// DisplayStateGetter lädt den zuletzt gemeldeten Display-Zustand.
type DisplayStateGetter interface {
	GetDisplayState(ctx context.Context, screenID string) (string, error)
}

// AllScreensLister lädt alle bekannten Screens.
type AllScreensLister interface {
	ListAll(ctx context.Context) ([]*store.Screen, error)
}

// Run startet den Scheduler-Loop. Blockiert bis ctx abgebrochen wird.
func Run(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, globalOverrides *store.GlobalOverrideStore, notifier DisplayCommander) {
	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			check(ctx, schedules, screens, globalOverrides, notifier)
		case <-ctx.Done():
			return
		}
	}
}

// Reconcile läuft alle 5 Minuten und gleicht Ist- und Soll-Zustand ab.
func Reconcile(ctx context.Context, schedules *store.ScreenScheduleStore, screens AllScreensLister, states DisplayStateGetter, globalOverrides *store.GlobalOverrideStore, notifier DisplayCommander) {
	ticker := time.NewTicker(5 * time.Minute)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			reconcile(ctx, schedules, screens, states, globalOverrides, notifier)
		case <-ctx.Done():
			return
		}
	}
}

func reconcile(ctx context.Context, schedules *store.ScreenScheduleStore, allScreens AllScreensLister, states DisplayStateGetter, globalOverrides *store.GlobalOverrideStore, notifier DisplayCommander) {
	now := time.Now()

	screenList, err := allScreens.ListAll(ctx)
	if err != nil {
		slog.Error("reconciler: list all screens failed", "err", err)
		return
	}

	globalOverride, err := globalOverrides.Get(ctx)
	if err != nil {
		slog.Warn("reconciler: get global override failed", "err", err)
		// nicht fatal — ohne Override fortfahren
	}

	for _, screen := range screenList {
		sc, err := schedules.Get(ctx, screen.ID)
		if err != nil {
			slog.Warn("reconciler: get schedule failed", "screen_id", screen.ID, "err", err)
			continue
		}

		want, shouldControl := resolveDesiredState(*sc, globalOverride, now)
		if !shouldControl {
			continue
		}

		got, err := states.GetDisplayState(ctx, screen.ID)
		if err != nil {
			slog.Warn("reconciler: get display state failed", "screen_id", screen.ID, "err", err)
			continue
		}

		if got == want {
			continue
		}

		action := "display_" + want
		if err := notifier.SendDisplayCommand(screen.Slug, action); err != nil {
			slog.Error("reconciler: send command failed", "screen_id", screen.ID, "action", action, "err", err)
		} else {
			slog.Info("reconciler: corrected display state", "screen_id", screen.ID, "slug", screen.Slug, "was", got, "want", want)
		}
	}
}

// desiredState berechnet den Soll-Zustand für einen einfachen Tagesplan.
// Unterstützt Mitternacht-Überschreitungen (z.B. onTime="22:00", offTime="06:00").
func desiredState(onTime, offTime, now string) string {
	if onTime <= offTime {
		// Normaler Fall: z.B. 06:00–22:00
		if now >= onTime && now < offTime {
			return "on"
		}
		return "off"
	}
	// Mitternacht-Überschreitung: z.B. 22:00–06:00
	if now >= onTime || now < offTime {
		return "on"
	}
	return "off"
}

// resolveDesiredState ermittelt den Soll-Zustand eines Screens unter Berücksichtigung
// aller Prioritätsstufen:
//  1. per-Screen override_on_until (höchste Priorität)
//  2. globaler Override
//  3. Wochenende (Sa/So)
//  4. normaler Zeitplan
//
// Gibt ("", false) zurück wenn keine Automatisierung aktiv ist.
func resolveDesiredState(sc store.ScreenSchedule, globalOverride *store.GlobalOverride, now time.Time) (desired string, shouldControl bool) {
	// 1. Per-Screen-Override: überschreibt alles
	if sc.OverrideOnUntil != nil && now.Before(*sc.OverrideOnUntil) {
		return "on", true
	}
	// 2. Globaler Override
	if globalOverride != nil && now.Before(globalOverride.Until) {
		return globalOverride.Type, true
	}
	// 3. Wochenende: Zeitpläne werden ignoriert, Monitore bleiben aus
	wd := now.Weekday()
	if wd == time.Saturday || wd == time.Sunday {
		return "off", true
	}
	// 4. Normaler Zeitplan
	if !sc.ScheduleEnabled || (sc.PowerOnTime == "" && sc.PowerOffTime == "") {
		return "", false
	}
	return desiredState(sc.PowerOnTime, sc.PowerOffTime, now.Format("15:04")), true
}

// check prüft alle aktiven Zeitpläne und sendet ggf. Befehle.
func check(ctx context.Context, schedules *store.ScreenScheduleStore, screens ScreenSlugGetter, globalOverrides *store.GlobalOverrideStore, notifier DisplayCommander) {
	// Uses process-local timezone — ensure TZ env var is set in the container (e.g. Europe/Berlin).
	now := time.Now()
	nowStr := now.Format("15:04")

	// Wochenende: keine Einschalte-Kommandos senden
	isWeekend := now.Weekday() == time.Saturday || now.Weekday() == time.Sunday

	// Globaler Override "off" aktiv?
	globalOverrideOff := false
	if o, err := globalOverrides.Get(ctx); err == nil && o != nil && now.Before(o.Until) && o.Type == "off" {
		globalOverrideOff = true
	}

	enabled, err := schedules.ListEnabled(ctx)
	if err != nil {
		slog.Error("scheduler: list enabled schedules failed", "err", err)
		return
	}

	for _, sc := range enabled {
		screen, err := screens.GetByID(ctx, sc.ScreenID)
		if err != nil {
			slog.Warn("scheduler: screen not found", "screen_id", sc.ScreenID, "err", err)
			continue
		}

		var action string
		if sc.PowerOnTime != "" && sc.PowerOnTime == nowStr {
			action = "display_on"
		} else if sc.PowerOffTime != "" && sc.PowerOffTime == nowStr {
			action = "display_off"
		}

		if action == "" {
			continue
		}

		// display_on unterdrücken wenn per-Screen-Override, Wochenende oder globaler Override "off"
		if action == "display_on" {
			if sc.OverrideOnUntil != nil && now.Before(*sc.OverrideOnUntil) {
				// per-Screen Override aktiv → Kommando trotzdem senden (Override = on)
			} else if isWeekend {
				slog.Info("scheduler: display_on unterdrückt (Wochenende)", "screen_id", sc.ScreenID)
				continue
			} else if globalOverrideOff {
				slog.Info("scheduler: display_on unterdrückt (globaler Override off)", "screen_id", sc.ScreenID)
				continue
			}
		}

		if err := notifier.SendDisplayCommand(screen.Slug, action); err != nil {
			slog.Error("scheduler: send command failed", "screen_id", sc.ScreenID, "action", action, "err", err)
		} else {
			slog.Info("scheduler: display command sent", "screen_id", sc.ScreenID, "slug", screen.Slug, "action", action)
		}
	}
}
