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

import Foundation

// Pure decision logic for repairing schedules whose `routineID` no longer resolves
// to any live routine (precedent: `SeedReconcilePlanner` / `ReminderPlanner`). No I/O
// and no `SyncEngine` dependency: the engine snapshots the schedule and routine
// caches, and applies the returned repairs.
//
// Why this exists: before schedules were repointed at seed-fork time, editing a
// starter-seed routine soft-deleted the seed and left every schedule pointing at the
// dead fixed ULID — permanently, since the in-memory `cloneRedirects` map is empty
// after a relaunch. Fork-time repointing fixes new forks; this repair pass heals the
// references existing user data already broke. It's cheap and idempotent, so it runs
// on every launch after the cache is reconciled.

// MARK: - Snapshots

/// A plain (id, routineID, routineName) snapshot of a `Schedule` cache entity.
struct ScheduleRepairRef: Sendable, Equatable {
    var id: String
    var routineID: String
    var routineName: String
}

/// A plain (id, name) snapshot of a live `Routine` cache entity.
struct RoutineRepairRef: Sendable, Equatable {
    var id: String
    var name: String
}

/// One repair to apply: rewrite this schedule's `routineID` to the matched routine.
struct ScheduleRepair: Sendable, Equatable {
    var scheduleID: String
    var newRoutineID: String
}

// MARK: - Planner

enum ScheduleRepairPlanner {
    /// The repairs for the given caches. A schedule needs repair when its `routineID`
    /// matches no live routine; the repair target is the live routine whose name equals
    /// the schedule's denormalized `routineName` — but **only when exactly one** live
    /// routine carries that name. Zero matches (routine truly gone) or several (the
    /// match would be a guess) leave the schedule alone — the edit form's
    /// "no longer available" flow is the deliberate fallback there.
    static func plans(
        schedules: [ScheduleRepairRef],
        liveRoutines: [RoutineRepairRef]
    ) -> [ScheduleRepair] {
        let liveIDs = Set(liveRoutines.map(\.id))

        // Routine name → its sole live id, with names carried by several routines
        // tracked separately (an ambiguous name must never be a repair target).
        var idByName: [String: String] = [:]
        var ambiguousNames: Set<String> = []
        for routine in liveRoutines {
            if idByName[routine.name] != nil {
                ambiguousNames.insert(routine.name)
            } else {
                idByName[routine.name] = routine.id
            }
        }

        return schedules.compactMap { schedule in
            guard !liveIDs.contains(schedule.routineID),
                  !ambiguousNames.contains(schedule.routineName),
                  let newID = idByName[schedule.routineName]
            else { return nil }
            return ScheduleRepair(scheduleID: schedule.id, newRoutineID: newID)
        }
    }
}
