import SwiftUI
import WidgetKit

// Layout constants — must mirror WidgetRenderer.kt on Android, otherwise
// the platforms drift apart on the same widget size.
let FALLBACK_VIRTUAL_MINUTES = 11 * 60
let MIN_HOUR_HEIGHT: CGFloat = 18
let MAX_HOUR_HEIGHT: CGFloat = 72
let MIN_BLOCK_HEIGHT: CGFloat = 16
let LESSON_GAP: CGFloat = 1.5

func realMinutesToVirtual(_ realMin: Int, periods: [WidgetPeriod]) -> CGFloat {
    guard !periods.isEmpty else { return CGFloat(realMin) }
    for p in periods where realMin >= p.startMinutes && realMin <= p.endMinutes {
        return CGFloat(p.virtualStartMinutes + (realMin - p.startMinutes))
    }
    let first = periods.first!
    if realMin < first.startMinutes {
        return CGFloat(realMin - first.startMinutes + first.virtualStartMinutes)
    }
    let last = periods.last!
    if realMin > last.endMinutes {
        return CGFloat(last.virtualEndMinutes + (realMin - last.endMinutes))
    }
    var prev = first
    for i in 1..<periods.count {
        let curr = periods[i]
        if realMin > prev.endMinutes && realMin < curr.startMinutes {
            let gap = curr.startMinutes - prev.endMinutes
            let virtualGap = curr.virtualStartMinutes - prev.virtualEndMinutes
            if gap > 0 {
                return CGFloat(prev.virtualEndMinutes) +
                    CGFloat(realMin - prev.endMinutes) * CGFloat(virtualGap) / CGFloat(gap)
            }
            return CGFloat(curr.virtualStartMinutes)
        }
        prev = curr
    }
    return 0
}

// Below these tile heights the secondary stack collapses progressively:
// shorter than ROOM_MIN drops everything, between ROOM_MIN and
// TEACHER_SEPARATE_MIN keeps the room but drops the teacher.
let BLOCK_SHOW_ROOM_MIN: CGFloat = 13
let BLOCK_SHOW_TEACHER_SEPARATE_MIN: CGFloat = 20

let MIN_SUBJECT_FONT: CGFloat = 9
let MAX_SUBJECT_FONT: CGFloat = 14
let MIN_SECONDARY_FONT: CGFloat = 7

func subjectFont(forHourHeight hourHeight: CGFloat) -> CGFloat {
    let t = max(0, min(1, (hourHeight - MIN_HOUR_HEIGHT) / (MAX_HOUR_HEIGHT - MIN_HOUR_HEIGHT)))
    return MIN_SUBJECT_FONT + t * (MAX_SUBJECT_FONT - MIN_SUBJECT_FONT)
}

struct TimetableDayView: View {
    let entry: TimetableEntry
    @Environment(\.widgetPalette) private var palette

    var body: some View {
        ZStack {
            if !entry.isLoggedIn {
                placeholder("Bitte einloggen, um den Stundenplan zu laden")
            } else if let data = entry.data {
                content(data: data)
            } else {
                placeholder("Lade…")
            }
        }
        .background(MarianumWatermark())
    }

    @ViewBuilder
    private func content(data: WidgetTimetableData) -> some View {
        VStack(alignment: .leading, spacing: 3) {
            header(data: data)
            if data.isHoliday {
                emptyState(text: data.holidayName ?? "Ferien")
            } else if data.lessons.isEmpty {
                emptyState(text: "Keine Stunden")
            } else {
                GeometryReader { geo in
                    let totalMin = CGFloat(data.periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES)
                    TimeGridView(
                        lessons: data.lessons,
                        periods: data.periods,
                        hourHeight: max(
                            MIN_HOUR_HEIGHT,
                            min(MAX_HOUR_HEIGHT, geo.size.height / max(totalMin, 60) * 60)
                        ),
                        showRoom: true,
                        showTeacher: true,
                        showTimeLabels: true
                    )
                }
            }
        }
    }

    private func header(data: WidgetTimetableData) -> some View {
        HStack(spacing: 4) {
            Text(dayLabel(for: data.anchorDate, relativeTo: entry.date))
                .font(.system(size: 13, weight: .semibold))
                .foregroundStyle(palette.textPrimary)
                .lineLimit(1)
                .minimumScaleFactor(0.7)
            Spacer(minLength: 4)
            Text("Stand: \(freshnessLabel(for: data.fetchedAt, relativeTo: entry.date))")
                .font(.system(size: 9))
                .foregroundStyle(palette.textSecondary)
                .lineLimit(1)
                .minimumScaleFactor(0.7)
        }
    }

    private func emptyState(text: String) -> some View {
        VStack {
            Spacer()
            Text(text)
                .font(.caption)
                .foregroundStyle(palette.textSecondary)
            Spacer()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    private func placeholder(_ message: String) -> some View {
        VStack(spacing: 4) {
            Text("Marianum Stundenplan")
                .font(.system(size: 14, weight: .semibold))
                .foregroundStyle(palette.textPrimary)
            Text(message)
                .font(.caption)
                .foregroundStyle(palette.textSecondary)
                .multilineTextAlignment(.center)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct TimeGridView: View {
    let lessons: [WidgetLesson]
    let periods: [WidgetPeriod]
    let hourHeight: CGFloat
    let showRoom: Bool
    let showTeacher: Bool
    let showTimeLabels: Bool
    /// Week-widget passes 3 for narrow columns; day-widget keeps 7.
    var horizontalPadding: CGFloat = 7

    @Environment(\.widgetPalette) private var palette

    private var totalVirtualMinutes: Int {
        periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES
    }

    private var totalHeight: CGFloat {
        CGFloat(totalVirtualMinutes) * hourHeight / 60.0
    }

    /// Below this per-hour height the two-line label collapses to a single
    /// period number — time + number overlap otherwise.
    private var compactLabels: Bool { hourHeight < 26 }

    var body: some View {
        HStack(alignment: .top, spacing: 0) {
            if showTimeLabels {
                // 28pt matches the Week widget's time-label column so the
                // visual rhythm is identical across both widgets.
                timeLabelsColumn
                    .frame(width: 28, alignment: .topTrailing)
                Rectangle()
                    .fill(palette.divider)
                    .frame(width: 1)
            }
            ZStack(alignment: .top) {
                gridLines
                breakBlocks
                ForEach(lessons.indices, id: \.self) { idx in
                    lessonBlock(lessons[idx])
                }
            }
            .frame(maxWidth: .infinity, minHeight: totalHeight, alignment: .top)
        }
    }

    private var timeLabelsColumn: some View {
        ZStack(alignment: .topTrailing) {
            ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
                Rectangle()
                    .fill(palette.divider)
                    .frame(height: 1)
                    .offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
            }
            ForEach(periods, id: \.startMinutes) { period in
                VStack(alignment: .trailing, spacing: -2) {
                    if compactLabels {
                        Text("\(period.name).")
                            .font(.system(size: 9, weight: .bold))
                            .foregroundStyle(palette.textPrimary)
                            .lineLimit(1)
                    } else {
                        Text(formatHm(period.startMinutes))
                            .font(.system(size: 9))
                            .foregroundStyle(palette.textPrimary)
                            .lineLimit(1)
                        Text("\(period.name).")
                            .font(.system(size: 7, weight: .bold))
                            .foregroundStyle(palette.textSecondary)
                            .lineLimit(1)
                    }
                }
                .padding(.trailing, 4)
                .offset(y: CGFloat(period.virtualStartMinutes) * hourHeight / 60.0)
            }
        }
        .frame(height: totalHeight, alignment: .topTrailing)
    }

    private var gridLines: some View {
        // `.frame(height: totalHeight, alignment: .top)` — without the
        // explicit `.top`, SwiftUI's frame modifier defaults to `.center`.
        // Every Rectangle here has layout-y=0 (the `.offset()`s shift the
        // rendered position only, not layout), so the inner ZStack's
        // natural height is just 1pt; a 1pt ZStack centred in a 300pt
        // frame anchors at the middle, and every line then renders
        // relative to that midpoint — the grid visually starts halfway
        // down the column and the later periods clip past the bottom.
        ZStack(alignment: .top) {
            ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
                Rectangle()
                    .fill(palette.divider)
                    .frame(height: 1)
                    .offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
            }
        }
        .frame(height: totalHeight, alignment: .top)
    }

    private var breakBlocks: some View {
        // Same `.center`-vs-`.top` bug as gridLines — see comment above.
        ZStack(alignment: .top) {
            ForEach(0..<max(0, periods.count - 1), id: \.self) { i in
                let curr = periods[i]
                let next = periods[i + 1]
                let virtualGap = next.virtualStartMinutes - curr.virtualEndMinutes
                if virtualGap > 0 {
                    Rectangle()
                        .fill(palette.breakBlock)
                        .frame(height: CGFloat(virtualGap) * hourHeight / 60.0)
                        .padding(.horizontal, 1)
                        .offset(y: CGFloat(curr.virtualEndMinutes) * hourHeight / 60.0)
                }
            }
        }
        .frame(height: totalHeight, alignment: .top)
    }

    private func formatHm(_ minutes: Int) -> String {
        String(format: "%02d:%02d", minutes / 60, minutes % 60)
    }

    @ViewBuilder
    private func lessonBlock(_ lesson: WidgetLesson) -> some View {
        let cal = Calendar.current
        let comps = cal.dateComponents([.hour, .minute], from: lesson.start)
        let startMinutes = (comps.hour ?? 0) * 60 + (comps.minute ?? 0)
        let durationMinutes = max(15, Int(lesson.end.timeIntervalSince(lesson.start) / 60))
        let virtualStart = realMinutesToVirtual(startMinutes, periods: periods)
        let virtualEnd = realMinutesToVirtual(startMinutes + durationMinutes, periods: periods)

        if virtualEnd > virtualStart {
            let top = virtualStart * hourHeight / 60.0 + LESSON_GAP / 2
            let height = max(
                MIN_BLOCK_HEIGHT,
                (virtualEnd - virtualStart) * hourHeight / 60.0 - LESSON_GAP
            )
            let subjectSize = subjectFont(forHourHeight: hourHeight)
            let secondarySize = max(MIN_SECONDARY_FONT, subjectSize - 2)
            let room = lesson.room
            let teacher = lesson.teacher ?? lesson.originalTeacher
            let hasSecondary = (room?.isEmpty == false) || (teacher?.isEmpty == false)
            HStack(alignment: .top, spacing: 4) {
                Text(subjectLabel(lesson))
                    .font(.system(size: subjectSize, weight: .semibold))
                    .foregroundStyle(.white)
                    .lineLimit(hasSecondary ? 1 : 2)
                    .minimumScaleFactor(0.5)
                if hasSecondary {
                    Spacer(minLength: 0)
                    VStack(alignment: .trailing, spacing: -1) {
                        if showRoom && height >= BLOCK_SHOW_ROOM_MIN {
                            if let room, !room.isEmpty {
                                Text(room)
                                    .font(.system(size: secondarySize))
                                    .foregroundStyle(.white.opacity(0.85))
                                    .lineLimit(1)
                                    .minimumScaleFactor(0.5)
                            }
                            if showTeacher,
                               height >= BLOCK_SHOW_TEACHER_SEPARATE_MIN,
                               let teacher,
                               !teacher.isEmpty {
                                Text(teacher)
                                    .font(.system(size: secondarySize))
                                    .foregroundStyle(.white.opacity(0.7))
                                    .lineLimit(1)
                                    .minimumScaleFactor(0.5)
                            }
                        }
                    }
                }
            }
            .padding(.horizontal, horizontalPadding)
            .padding(.vertical, 3)
            .frame(maxWidth: .infinity, alignment: .topLeading)
            .frame(height: height, alignment: .topLeading)
            .background(blockColor(lesson))
            .cornerRadius(6)
            .overlay(alignment: .bottomLeading) {
                // Separate fixed-size badge so the +N hint stays readable
                // when the subject autoshrinks on narrow tiles.
                if let count = lesson.siblingCount, count > 0 {
                    Text("+\(count)")
                        .font(.system(size: 12, weight: .bold))
                        .foregroundStyle(.white)
                        .padding(.leading, horizontalPadding)
                        .padding(.bottom, 2)
                }
            }
            .overlay {
                // CrossPainter parity: clip cross to the rounded shape so
                // the diagonals don't bleed past the corners.
                if lesson.status == .cancelled {
                    ZStack {
                        RoundedRectangle(cornerRadius: 6)
                            .stroke(Color.red.opacity(0.78), lineWidth: 1.5)
                        GeometryReader { geo in
                            Path { p in
                                p.move(to: .zero)
                                p.addLine(to: CGPoint(x: geo.size.width, y: geo.size.height))
                                p.move(to: CGPoint(x: geo.size.width, y: 0))
                                p.addLine(to: CGPoint(x: 0, y: geo.size.height))
                            }
                            .stroke(Color.red.opacity(0.78), lineWidth: 3)
                        }
                    }
                    .clipShape(RoundedRectangle(cornerRadius: 6))
                }
            }
            .padding(.horizontal, showRoom ? 2 : 1)
            .offset(y: top)
        }
    }

    private func subjectLabel(_ lesson: WidgetLesson) -> String {
        !lesson.subjectShort.isEmpty
            ? lesson.subjectShort
            : (lesson.subjectLong ?? "—")
    }

    /// Mirrors lesson_color.dart + custom_event_colors.dart so the widget
    /// matches the in-app calendar exactly.
    private func blockColor(_ lesson: WidgetLesson) -> Color {
        if lesson.status == .event, let custom = lesson.customColor {
            switch custom {
            case "orange": return Color(red: 239/255.0, green: 108/255.0, blue: 0/255.0)
            case "red": return Color(red: 153/255.0, green: 51/255.0, blue: 51/255.0)
            case "green": return Color(red: 76/255.0, green: 175/255.0, blue: 80/255.0)
            case "blue": return Color(red: 33/255.0, green: 150/255.0, blue: 243/255.0)
            default: break
            }
        }
        switch lesson.status {
        // `.ongoing` deliberately collapses into the regular red — the
        // widget timeline only ticks every ~30min, so highlighting the
        // "current" lesson would be stale most of the day and misleads
        // more than it helps.
        case .regular, .past, .ongoing:
            return Color(red: 153/255.0, green: 51/255.0, blue: 51/255.0)
        case .cancelled: return .black
        case .irregular: return Color(red: 143/255.0, green: 25/255.0, blue: 179/255.0)
        case .teacherChanged: return Color(red: 41/255.0, green: 99/255.0, blue: 155/255.0)
        case .event: return Color(red: 239/255.0, green: 108/255.0, blue: 0/255.0)
        case .duty: return Color(red: 0/255.0, green: 121/255.0, blue: 107/255.0)
        }
    }
}

/// Period boundaries deduped: adjacent periods share a line, periods on
/// either side of a break get their own (bracketing the break block).
func periodBoundaries(_ periods: [WidgetPeriod]) -> [Int] {
    var seen = Set<Int>()
    var result: [Int] = []
    for p in periods {
        for v in [p.virtualStartMinutes, p.virtualEndMinutes] {
            if seen.insert(v).inserted { result.append(v) }
        }
    }
    return result.sorted()
}

/// Fixed-locale formatters cached once — DateFormatter setup is the
/// expensive part and the multi-entry timelines render up to ~8 entries per
/// reload. Only touched from WidgetKit's archival rendering, so the shared
/// instances are safe.
enum WidgetDateFormatters {
    static let shortDate = make("dd.MM.")
    static let weekdayShort = make("EE")
    static let weekdayDate = make("EEEE · dd.MM.")
    static let time = make("HH:mm")
    static let dateTime = make("dd.MM. HH:mm")

    private static func make(_ format: String) -> DateFormatter {
        let f = DateFormatter()
        f.locale = Locale(identifier: "de_DE")
        f.dateFormat = format
        return f
    }
}

/// `now` is the timeline entry's date, not `Date()`: WidgetKit archives
/// entries ahead of time, so wall-clock reads would be wrong for every
/// entry after the first.
func dayLabel(for date: Date, relativeTo now: Date) -> String {
    let cal = Calendar.current
    let today = cal.startOfDay(for: now)
    let anchor = cal.startOfDay(for: date)
    if anchor == today {
        return "Heute · \(shortDate(date))"
    }
    if let tomorrow = cal.date(byAdding: .day, value: 1, to: today), anchor == tomorrow {
        return "Morgen · \(shortDate(date))"
    }
    return WidgetDateFormatters.weekdayDate.string(from: date)
}

func shortDate(_ date: Date) -> String {
    WidgetDateFormatters.shortDate.string(from: date)
}

func freshnessLabel(for fetchedAt: Date, relativeTo now: Date) -> String {
    let cal = Calendar.current
    let today = cal.startOfDay(for: now)
    let fetchedDay = cal.startOfDay(for: fetchedAt)
    if fetchedDay == today {
        return WidgetDateFormatters.time.string(from: fetchedAt)
    }
    if let yesterday = cal.date(byAdding: .day, value: -1, to: today),
       fetchedDay == yesterday {
        return "gestern \(WidgetDateFormatters.time.string(from: fetchedAt))"
    }
    return WidgetDateFormatters.dateTime.string(from: fetchedAt)
}
