//
// TodayView.swift
// Workouts
//
// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
//

import SwiftUI
import SwiftData

/// The Today board — the today-projection of the user's schedules, grouped by goal.
/// A schedule row shows its routine, its recurrence, and today's status for that
/// routine; tapping starts (or opens) that routine's workout for today.
struct TodayView: View {
    @Environment(SyncEngine.self) private var sync
    @Environment(AppServices.self) private var services

    @Query(sort: [SortDescriptor(\Schedule.order), SortDescriptor(\Schedule.createdAt)])
    private var schedules: [Schedule]

    @Query(sort: [SortDescriptor(\Routine.order), SortDescriptor(\Routine.name)])
    private var routines: [Routine]

    @Query(sort: \Workout.start, order: .reverse)
    private var workouts: [Workout]

    /// ID of the workout to push into — a just-started one (the navigator polls for
    /// its cache entity) or an existing today-workout (resolves immediately). One
    /// id-based path for both, so no destination ever retains a `@Model`.
    @State private var pendingWorkoutID: String?
    @State private var scheduleToEdit: Schedule?
    @State private var showingAddSchedule = false
    @State private var showingDayPicker = false

    /// The day the board is projecting — today by default; the calendar toolbar
    /// button navigates to any other day.
    @State private var selectedDate = Date()

    private var isViewingToday: Bool { selectedDate.isSameDay(as: Date()) }

    /// The viewed day is strictly before today (day-precision).
    private var isViewingPastDay: Bool { selectedDate < Calendar.current.startOfDay(for: Date()) }

    /// Schedules due on the viewed day: `.daily` always qualifies; `.fixedDays` only
    /// when the viewed day's calendar weekday is one of the schedule's days; `.once`
    /// only on its own day. A dateless `.once` (shouldn't happen) shows everywhere
    /// rather than nowhere.
    private var visibleSchedules: [Schedule] {
        schedules.filter { schedule in
            switch schedule.recurrenceEnum {
            case .daily:
                return true
            case .fixedDays:
                let weekday = Calendar.current.component(.weekday, from: selectedDate)
                return (schedule.weekdays ?? []).contains(weekday)
            case .once:
                guard let date = schedule.date else { return true }
                return date.isSameDay(as: selectedDate)
            }
        }
    }

    /// Workouts on the viewed day whose routine isn't already shown via a schedule row
    /// above — e.g. an ad hoc "Start Now" workout, or a routine whose schedule isn't due
    /// today. Matched the same way `todaysWorkout` matches a workout to a schedule: both
    /// sides live-resolved through the seed clone-on-edit redirect. Each is then filed
    /// under its `inferredGoal(for:)` group, falling back to Unassigned.
    private var orphanWorkouts: [Workout] {
        let scheduledRoutineIDs = Set(visibleSchedules.map { sync.currentRoutineID(for: $0.routineID) })
        return workouts.filter { workout in
            guard workout.start.isSameDay(as: selectedDate) else { return false }
            guard let routineID = workout.routineID else { return true }
            return !scheduledRoutineIDs.contains(sync.currentRoutineID(for: routineID))
        }
    }

    /// Non-empty goal groups in the user's chosen goal order. A group exists when the
    /// viewed day has due schedules for that goal, orphan workouts inferred into it
    /// (see `inferredGoal(for:)`), or both — so an ad hoc strength run surfaces a
    /// Strength header even on a day with no strength schedule due.
    private var goalGroups: [(kind: GoalKind, schedules: [Schedule], orphans: [Workout])] {
        let orphansByGoal = Dictionary(grouping: orphanWorkouts) { inferredGoal(for: $0) }
        return GoalKind.orderedCases().compactMap { kind in
            let matches = visibleSchedules.filter { $0.goalKind == kind }
            let orphans = orphansByGoal[kind] ?? []
            return (matches.isEmpty && orphans.isEmpty) ? nil : (kind, matches, orphans)
        }
    }

    /// Schedules with no goal, or a goal raw value this version doesn't know.
    private var unassignedSchedules: [Schedule] {
        visibleSchedules.filter { $0.goalKind == nil }
    }

    /// Orphan workouts whose routine no schedule anywhere assigns a goal.
    private var unassignedOrphanWorkouts: [Workout] {
        orphanWorkouts.filter { inferredGoal(for: $0) == nil }
    }

    /// The goal an orphan workout belongs to: the goal of any schedule — due today or
    /// not — referencing the same routine (through the clone redirect on both sides).
    /// The workout was "ad hoc" only in how it was launched; the routine's schedule
    /// still tells us what the user trains it for. Multiple schedules with different
    /// goals tie-break by the user's goal order. Nil = truly unassigned.
    private func inferredGoal(for workout: Workout) -> GoalKind? {
        guard let rid = workout.routineID else { return nil }
        let liveID = sync.currentRoutineID(for: rid)
        let goals = schedules
            .filter { sync.currentRoutineID(for: $0.routineID) == liveID }
            .compactMap(\.goalKind)
        let order = GoalKind.orderedCases()
        return goals.min {
            (order.firstIndex(of: $0) ?? .max) < (order.firstIndex(of: $1) ?? .max)
        }
    }

    /// Motivational openers for the board title — each reads as a phrase completed
    /// by "Today" ("Seize Today"). One per day, rotating by day-of-era: stable all
    /// day, fresh the next.
    private static let greetings: [String] = [
        "Seize",
        "Own",
        "Win",
        "Conquer",
        "Show Up",
        "Stay the Course",
        "Make It Count",
        "Do the Work",
        "Start Strong",
        "No Excuses",
        "Begin Again",
        "Keep the Streak",
    ]

    private var greetingLine: String {
        let day = Calendar.current.ordinality(of: .day, in: .era, for: Date()) ?? 0
        return Self.greetings[day % Self.greetings.count]
    }

    var body: some View {
        NavigationStack {
            List {
                ForEach(goalGroups, id: \.kind) { group in
                    Section {
                        ForEach(group.schedules) { scheduleRow($0) }
                        ForEach(group.orphans) { orphanWorkoutRow($0) }
                    } header: {
                        Label(group.kind.displayName, systemImage: group.kind.systemImage)
                            .foregroundStyle(Color.color(from: group.kind.colorName))
                    }
                }

                if !unassignedSchedules.isEmpty || !unassignedOrphanWorkouts.isEmpty {
                    Section("Unassigned") {
                        ForEach(unassignedSchedules) { scheduleRow($0) }
                        ForEach(unassignedOrphanWorkouts) { orphanWorkoutRow($0) }
                    }
                }
            }
            .overlay {
                if schedules.isEmpty && orphanWorkouts.isEmpty {
                    ContentUnavailableView(
                        "No Schedules Yet",
                        systemImage: "calendar.badge.plus",
                        description: Text("Plan your week by adding a schedule for a routine.")
                    )
                } else if visibleSchedules.isEmpty && orphanWorkouts.isEmpty {
                    ContentUnavailableView(
                        "Nothing Planned",
                        systemImage: "calendar",
                        description: Text("Looks like a rest day. Tap + to plan or start a workout.")
                    )
                }
            }
            .navigationTitle(isViewingToday ? "\(greetingLine) Today" : selectedDate.formatDate())
            .toolbar {
                ToolbarItemGroup(placement: .topBarLeading) {
                    Button {
                        shiftDay(by: -1)
                    } label: {
                        Label("Previous Day", systemImage: "chevron.backward")
                    }
                    Button {
                        showingDayPicker = true
                    } label: {
                        Label("Go to Day", systemImage: "calendar")
                    }
                    Button {
                        shiftDay(by: 1)
                    } label: {
                        Label("Next Day", systemImage: "chevron.forward")
                    }
                    // Quick way home — redundant on today itself, so hidden there.
                    if !isViewingToday {
                        Button("Today") {
                            withAnimation { selectedDate = Date() }
                        }
                    }
                }
                ToolbarItem(placement: .topBarTrailing) {
                    Button {
                        showingAddSchedule = true
                    } label: {
                        Label("Add Workout", systemImage: "plus")
                    }
                }
            }
            // Row taps land here for both cases: a fresh start (entity arrives a beat
            // after the file write) and an existing today-workout (resolves at once).
            .navigatesToStartedWorkout(pendingWorkoutID: $pendingWorkoutID)
            .safeAreaInset(edge: .bottom) {
                SyncStatusBanner()
                    .padding(.horizontal)
                    .padding(.bottom, 8)
            }
            .sheet(isPresented: $showingDayPicker) {
                DayPickerSheet(selectedDate: $selectedDate, workoutDays: workoutDays)
            }
            .sheet(isPresented: $showingAddSchedule) {
                // Seed a would-be one-off with the day on screen, so navigating to a
                // day and tapping + plans that day. "Now" (offered only on today) hands
                // the routine back here to start immediately.
                ScheduleAddEditView(defaultDate: selectedDate, onStartNow: startNow)
            }
            .sheet(item: $scheduleToEdit) { schedule in
                ScheduleAddEditView(schedule: schedule)
            }
        }
    }

    // MARK: - Row

    private func scheduleRow(_ schedule: Schedule) -> some View {
        let routine = resolvedRoutine(for: schedule)
        let today = todaysWorkout(for: schedule)
        return Button {
            openOrStart(schedule: schedule, routine: routine, today: today)
        } label: {
            HStack(spacing: 12) {
                Image(systemName: routine?.systemImage ?? "figure.strengthtraining.traditional")
                    .font(.title3)
                    .foregroundStyle(iconColor(routine: routine, schedule: schedule))
                    .frame(width: 32)

                VStack(alignment: .leading, spacing: 2) {
                    Text(routine?.name ?? schedule.routineName)
                        .foregroundStyle(.primary)
                    Text(schedule.recurrenceSummary)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }

                Spacer()

                if let today {
                    todayStatus(for: today)
                } else if isViewingPastDay {
                    skippedTag
                }
            }
            .contentShape(Rectangle())
        }
        .tint(.primary)
        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
            Button(role: .destructive) {
                Task { await sync.delete(schedule: schedule) }
            } label: {
                Label("Delete", systemImage: "trash")
            }
            .tint(.red)

            Button {
                scheduleToEdit = schedule
            } label: {
                Label("Edit", systemImage: "pencil")
            }
        }
    }

    /// A workout on the viewed day with no matching schedule row — an ad hoc "Start
    /// Now" run, or a routine whose schedule isn't due today. No recurrence caption
    /// (there's no schedule behind it), just the routine name and its status.
    private func orphanWorkoutRow(_ workout: Workout) -> some View {
        let routine = resolvedRoutine(id: workout.routineID)
        return Button {
            guard !workout.isDeleted else { return }
            pendingWorkoutID = workout.id
        } label: {
            HStack(spacing: 12) {
                Image(systemName: routine?.systemImage ?? "figure.strengthtraining.traditional")
                    .font(.title3)
                    .foregroundStyle(routine.map { Color.color(from: $0.color) } ?? .secondary)
                    .frame(width: 32)

                VStack(alignment: .leading, spacing: 2) {
                    Text(routine?.name ?? workout.routineName ?? Routine.unnamed)
                        .foregroundStyle(.primary)
                    Text("Ad Hoc")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }

                Spacer()

                todayStatus(for: workout)
            }
            .contentShape(Rectangle())
        }
        .tint(.primary)
    }

    /// Today's status decoration: a completed run shows a green check + its finish
    /// time; an unfinished one shows an orange "In Progress" tag.
    @ViewBuilder
    private func todayStatus(for workout: Workout) -> some View {
        if workout.status == .completed {
            HStack(spacing: 4) {
                Image(systemName: "checkmark.circle.fill")
                    .foregroundStyle(.green)
                Text((workout.end ?? workout.start).formattedTime())
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
        } else {
            Text("In Progress")
                .font(.caption2.weight(.semibold))
                .foregroundStyle(.orange)
                .padding(.horizontal, 8)
                .padding(.vertical, 3)
                .background(.orange.opacity(0.15), in: Capsule())
        }
    }

    /// Signpost on a past-day schedule row with no logged workout. Deliberately
    /// undifferentiated for now — a later pass will distinguish *abandoned* from an
    /// explicit skip carrying an excuse ("sick day"); see UX-REDESIGN.md.
    private var skippedTag: some View {
        Text("Skipped")
            .font(.caption2.weight(.semibold))
            .foregroundStyle(.secondary)
            .padding(.horizontal, 8)
            .padding(.vertical, 3)
            .background(.secondary.opacity(0.15), in: Capsule())
    }

    /// Step the viewed day back or forward (day precision; the time component is
    /// irrelevant to the board).
    private func shiftDay(by days: Int) {
        withAnimation {
            selectedDate = Calendar.current.date(byAdding: .day, value: days, to: selectedDate) ?? selectedDate
        }
    }

    // MARK: - Resolution helpers

    /// The live routine this schedule points at, following the seed clone-on-edit
    /// redirect; nil when the routine has been deleted.
    private func resolvedRoutine(for schedule: Schedule) -> Routine? {
        resolvedRoutine(id: schedule.routineID)
    }

    /// The live routine for a routine id, following the seed clone-on-edit redirect;
    /// nil when there's no id or the routine has been deleted.
    private func resolvedRoutine(id: String?) -> Routine? {
        guard let id else { return nil }
        let liveID = sync.currentRoutineID(for: id)
        return routines.first { $0.id == liveID }
    }

    /// The most recent workout started on the viewed day for this schedule's routine
    /// (comparing through the clone redirect on both sides). `workouts` is
    /// start-descending, so the first match is the newest.
    private func todaysWorkout(for schedule: Schedule) -> Workout? {
        let liveRoutineID = sync.currentRoutineID(for: schedule.routineID)
        return workouts.first { workout in
            guard let rid = workout.routineID, workout.start.isSameDay(as: selectedDate) else { return false }
            return sync.currentRoutineID(for: rid) == liveRoutineID
        }
    }

    /// Day-precision (year/month/day) components of every day with at least one
    /// logged workout — the day picker decorates these with a dot. Built as *bare*
    /// y/m/d components, not `dateComponents([.year, .month, .day], from:)` output:
    /// the latter also sets `isLeapMonth = false`, which `==` ignores but hashing
    /// doesn't — so a bare-components `Set.contains` probe (the day picker's) would
    /// miss every member.
    private var workoutDays: Set<DateComponents> {
        Set(workouts.map { workout in
            let c = Calendar.current.dateComponents([.year, .month, .day], from: workout.start)
            var day = DateComponents()
            day.year = c.year
            day.month = c.month
            day.day = c.day
            return day
        })
    }

    /// Row icon color: the routine's own color, or the goal's color as a generic
    /// fallback when the routine is gone.
    private func iconColor(routine: Routine?, schedule: Schedule) -> Color {
        if let routine { return Color.color(from: routine.color) }
        return Color.color(from: schedule.goalKind?.colorName ?? "")
    }

    /// Tap: open the viewed day's workout if one exists, otherwise start a fresh one
    /// from the resolved routine — but only when the board is on today, since a start
    /// always happens *now*. Does nothing on other days without a workout, or for an
    /// orphaned schedule (no live routine).
    private func openOrStart(schedule: Schedule, routine: Routine?, today: Workout?) {
        if let today {
            guard !today.isDeleted else { return }
            pendingWorkoutID = today.id
        } else if isViewingToday, let routine {
            Task {
                pendingWorkoutID = await WorkoutStarter.start(routine: routine, sync: sync)
            }
        }
    }

    /// The "Now" path from the add sheet: start the chosen routine immediately and
    /// navigate in, same as tapping a due schedule row on today's board.
    private func startNow(routine: Routine) {
        Task {
            pendingWorkoutID = await WorkoutStarter.start(routine: routine, sync: sync)
        }
    }
}
