import SwiftUI
import WidgetKit

@main
struct MarianumWidgetBundle: WidgetBundle {
    var body: some Widget {
        TimetableDayWidget()
        TimetableWeekWidget()
    }
}

// MARK: - Day widget

struct TimetableDayWidget: Widget {
    let kind: String = "TimetableDayWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: TimetableDayProvider()) { entry in
            TimetableDayView(entry: entry).widgetSurface(entry: entry)
        }
        .configurationDisplayName("Marianum · Heute")
        .description("Stundenplan und Vertretungen für den anstehenden Schultag.")
        // Only .systemLarge: iOS gives us no tall-portrait family, so the
        // 162×162 .systemSmall can't fit a readable 11-period day grid,
        // and .systemMedium is short-wide which crunches the labels.
        // Single useful size beats three crippled ones.
        .supportedFamilies([.systemLarge])
    }
}

struct TimetableDayProvider: TimelineProvider {
    func placeholder(in context: Context) -> TimetableEntry {
        TimetableEntry.placeholder()
    }

    func getSnapshot(in context: Context, completion: @escaping (TimetableEntry) -> Void) {
        completion(TimetableEntry.current(variant: .day))
    }

    func getTimeline(
        in context: Context,
        completion: @escaping (Timeline<TimetableEntry>) -> Void
    ) {
        let now = Date()
        // 30 min mirrors the Dart workmanager cadence. iOS treats this as
        // advisory; the boundary entries below keep the rendered day correct
        // even when no reload is granted, and the "Stand:" label tells the
        // user when the underlying data is stale.
        let next = Calendar.current.date(byAdding: .minute, value: 30, to: now) ?? now
        completion(Timeline(entries: TimetableEntry.dayEntries(now: now), policy: .after(next)))
    }
}

// MARK: - Week widget

struct TimetableWeekWidget: Widget {
    let kind: String = "TimetableWeekWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: TimetableWeekProvider()) { entry in
            TimetableWeekView(entry: entry).widgetSurface(entry: entry)
        }
        .configurationDisplayName("Marianum · Woche")
        .description("Stundenplan und Vertretungen für die ganze Schulwoche.")
        // .systemMedium is dropped — 5 day columns plus a time-label column
        // in a 4×2 cell-strip yields per-column widths well below the
        // subject-only threshold and the labels in the time sidebar overlap.
        .supportedFamilies([.systemLarge, .systemExtraLarge])
    }
}

struct TimetableWeekProvider: TimelineProvider {
    func placeholder(in context: Context) -> TimetableEntry {
        TimetableEntry.placeholder()
    }

    func getSnapshot(in context: Context, completion: @escaping (TimetableEntry) -> Void) {
        completion(TimetableEntry.current(variant: .week))
    }

    func getTimeline(
        in context: Context,
        completion: @escaping (Timeline<TimetableEntry>) -> Void
    ) {
        let now = Date()
        let next = Calendar.current.date(byAdding: .minute, value: 30, to: now) ?? now
        completion(Timeline(entries: TimetableEntry.weekEntries(now: now), policy: .after(next)))
    }
}

// MARK: - Entry

enum TimetableVariant { case day, week }

struct TimetableEntry: TimelineEntry {
    let date: Date
    let variant: TimetableVariant
    let data: WidgetTimetableData?
    let isLoggedIn: Bool
    let themeMode: String

    static func placeholder() -> TimetableEntry {
        TimetableEntry(
            date: Date(),
            variant: .day,
            data: nil,
            isLoggedIn: true,
            themeMode: "system"
        )
    }

    static func current(variant: TimetableVariant) -> TimetableEntry {
        let isLoggedIn = WidgetDataLoader.isLoggedIn()
        let data = isLoggedIn
            ? (variant == .day ? WidgetDataLoader.loadDay() : WidgetDataLoader.loadWeek())
            : nil
        return TimetableEntry(
            date: Date(),
            variant: variant,
            data: data,
            isLoggedIn: isLoggedIn,
            themeMode: WidgetDataLoader.themeMode()
        )
    }

    /// Day timeline derived from the 14-day week payload, so the widget shows
    /// the right day even when iOS grants no reload for days.
    static func dayEntries(now: Date) -> [TimetableEntry] {
        entries(now: now, variant: .day) { week, date in
            TimetableAnchor.slice(week: week, forDay: TimetableAnchor.resolveDayAnchor(date))
        }
    }

    /// Week timeline: re-anchoring performs the Friday-evening/weekend jump
    /// into next week from cached data, and the midnight entries keep the
    /// "Stand:" freshness label honest.
    static func weekEntries(now: Date) -> [TimetableEntry] {
        entries(now: now, variant: .week) { week, date in
            TimetableAnchor.retarget(week: week, anchorDate: TimetableAnchor.resolveWeekAnchor(date))
        }
    }

    /// Shared timeline skeleton: one entry now plus one per anchor boundary
    /// (midnight rollover, 17:00 cutoff). Boundaries that cannot change the
    /// render — same anchor and same calendar day for the header labels —
    /// are dropped.
    private static func entries(
        now: Date,
        variant: TimetableVariant,
        transform: (WidgetTimetableData, Date) -> WidgetTimetableData
    ) -> [TimetableEntry] {
        guard WidgetDataLoader.isLoggedIn(), let week = WidgetDataLoader.loadWeek() else {
            // Logged out, or no v2 week snapshot yet (fresh app update):
            // fall back to the legacy single-entry payload.
            return [TimetableEntry.current(variant: variant)]
        }
        let theme = WidgetDataLoader.themeMode()
        let cal = Calendar.current
        var result: [TimetableEntry] = []
        for date in [now] + TimetableAnchor.boundaryDates(from: now) {
            let data = transform(week, date)
            if let previous = result.last, let previousData = previous.data,
               cal.isDate(previousData.anchorDate, inSameDayAs: data.anchorDate),
               cal.isDate(previous.date, inSameDayAs: date) {
                continue
            }
            result.append(TimetableEntry(
                date: date,
                variant: variant,
                data: data,
                isLoggedIn: true,
                themeMode: theme
            ))
        }
        return result
    }
}

extension View {
    /// Applies the user's chosen light/dark override on top of the system
    /// scheme so the widget honours the in-app theme setting.
    @ViewBuilder
    func widgetThemeOverride(_ mode: String) -> some View {
        switch mode {
        case "light": self.environment(\.colorScheme, .light)
        case "dark": self.environment(\.colorScheme, .dark)
        default: self
        }
    }

    /// Wraps the widget view in the Marianum palette + container background
    /// so all subviews can read `\.widgetPalette` and so the widget renders
    /// in our warm off-white / dark-clay instead of system grey.
    @ViewBuilder
    func widgetSurface(entry: TimetableEntry) -> some View {
        WidgetSurface(entry: entry) { self }
    }
}

private struct WidgetSurface<Content: View>: View {
    let entry: TimetableEntry
    @ViewBuilder let content: () -> Content
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        let palette = WidgetPalette.resolve(
            themeMode: entry.themeMode,
            colorScheme: colorScheme
        )
        return AnyView(
            background(content: content(), palette: palette)
                .environment(\.widgetPalette, palette)
                .widgetThemeOverride(entry.themeMode)
        )
    }

    @ViewBuilder
    private func background<C: View>(content: C, palette: WidgetPalette) -> some View {
        if #available(iOS 17.0, *) {
            content.containerBackground(palette.background, for: .widget)
        } else {
            content.background(palette.background)
        }
    }
}
