// EXERCISE 04 — FIXED VARIANT.  The repair is to treat the shared PREDICATE as
// the state and the wakeup as nothing more than a hint.
//
// THE SCENARIO
//   A settings loader publishes a configuration and announces it. A consumer
//   waits for that announcement.
//
// THE REPAIR — one rule, applied in two places
//   Never wait on a condition variable without a loop around the predicate:
//
//       while !predicate { cond.wait(until: deadline) }
//
//   That single line fixes both symptoms at once, and it is worth seeing why
//   they are the same bug:
//
//   * The check BEFORE the first wait is what makes a lost announcement
//     harmless. If the thing already happened, the loop body never runs and the
//     consumer proceeds immediately. An NSCondition stores nothing, so an
//     announcement delivered before anyone was listening is simply gone; the
//     predicate is what survives it.
//
//   * The re-check AFTER each wakeup is what makes a wakeup safe to act on. A
//     wakeup means "the predicate MAY have changed", never "the item is yours".
//     Another consumer may have taken it first, and the kernel is permitted to
//     wake you for no reason at all.
//
//   `signal()` is also upgraded to `broadcast()` where more than one waiter can
//   be satisfied by one state change; with a correct predicate loop, an extra
//   wakeup costs a re-check rather than a defect.
//
// Build: swiftc -swift-version 6 -O handoff.swift -o handoff_fixed
// Run:   ./handoff_fixed
//
// Expected: partA returns essentially instantly, partB reports
// wouldHaveCrashed=0 and consumes every item, and correct=true.
//
// Bounded: every wait has a finite deadline, every loop has an iteration cap,
// and a watchdog force-exits after WATCHDOG_S.

import Foundation
import Synchronization

let WATCHDOG_S: Double = 60
let EXIT_WATCHDOG: Int32 = 75

func armWatchdog(_ seconds: Double, _ label: String) {
    let t = Thread {
        Thread.sleep(forTimeInterval: seconds)
        fputs("\nWATCHDOG: \(label) exceeded \(seconds)s — forcing _exit(\(EXIT_WATCHDOG)).\n", stderr)
        fflush(stderr); _exit(EXIT_WATCHDOG)
    }
    t.stackSize = 512 * 1024; t.start()
}

// MARK: - Part A — the announcement that arrived before anyone was listening
//
// The loader publishes the configuration and signals. No consumer has started
// yet. The consumer then begins, checks the predicate, and finds it already
// satisfied, so it never waits at all.

func partA() -> (waitedMs: Int, dataWasAlreadyPublished: Bool) {
    let cond = NSCondition()
    var configuration: String? = nil

    // The loader finishes entirely before the consumer starts.
    cond.lock()
    configuration = "{\"theme\":\"dark\"}"
    cond.signal()
    cond.unlock()

    let deadline = Date().addingTimeInterval(1.5)
    let start = Date()

    cond.lock()
    // The predicate is the state. Ask about it first; wait only while it is
    // false; re-ask after every wakeup.
    while configuration == nil {
        if !cond.wait(until: deadline) { break }      // finite deadline
    }
    let published = configuration != nil
    cond.unlock()

    let waitedMs = Int(Date().timeIntervalSince(start) * 1000)
    print("  partA waitedMs=\(waitedMs) dataWasAlreadyPublished=\(published)")
    return (waitedMs, published)
}

// MARK: - Part B — a wakeup treated as a promise

/// A bounded queue guarded by one NSCondition. The condition's lock protects
/// `items`; the condition itself only announces that `items` may have changed.
final class WorkQueue: @unchecked Sendable {
    private let cond = NSCondition()
    private var items: [Int] = []
    private var closed = false
    private let stats = Mutex(Stats())

    struct Stats { var wouldHaveCrashed = 0; var spuriousAbsorbed = 0 }
    var snapshot: Stats { stats.withLock { $0 } }

    func publish(_ v: Int) {
        cond.lock(); defer { cond.unlock() }
        items.append(v)
        cond.broadcast()          // an extra wakeup now costs one re-check
    }

    func close() {
        cond.lock(); defer { cond.unlock() }
        closed = true
        cond.broadcast()          // termination must wake EVERYONE
    }

    /// A wakeup that changes no state. Real spurious wakeups come from the
    /// kernel and are rare; injecting one makes the defect reproducible instead
    /// of a once-a-week production mystery.
    func rattle() {
        cond.lock(); defer { cond.unlock() }
        cond.broadcast()
    }

    /// Returns the next item, or nil once the queue has closed and drained.
    /// The -1 "would have trapped" path is now unreachable, and is kept only so
    /// that the two builds report the same fields.
    func take(deadline: Date) -> Int? {
        cond.lock(); defer { cond.unlock() }
        // `while`, not `if`. Re-evaluate on EVERY wakeup, because a wakeup is a
        // hint rather than a promise: another consumer may have taken the item,
        // or the kernel may have woken us for no reason.
        while items.isEmpty && !closed {
            if !cond.wait(until: deadline) { break }
            if items.isEmpty && !closed { stats.withLock { $0.spuriousAbsorbed += 1 } }
        }
        guard !items.isEmpty else {
            if closed { return nil }
            stats.withLock { $0.wouldHaveCrashed += 1 }
            return -1
        }
        return items.removeFirst()
    }
}

func partB() -> (consumed: Int, expected: Int, wouldHaveCrashed: Int, absorbed: Int) {
    let queue = WorkQueue()
    let consumers = 3
    let items = 120
    let consumed = Mutex(0)
    let deadline = Date().addingTimeInterval(8)
    let group = DispatchGroup()

    for _ in 0..<consumers {
        DispatchQueue.global(qos: .utility).async(group: group) {
            var iterations = 0
            while Date() < deadline && iterations < items * 4 {
                iterations += 1
                guard let v = queue.take(deadline: deadline) else { break }   // nil = closed
                if v >= 0 { consumed.withLock { $0 += 1 } }
            }
        }
    }

    // Every consumer is now parked on an empty queue. Rattle the condition
    // without changing any state, three times, once per consumer.
    Thread.sleep(forTimeInterval: 0.4)
    for _ in 0..<consumers { queue.rattle(); Thread.sleep(forTimeInterval: 0.05) }

    for i in 0..<items { queue.publish(i); if i % 16 == 15 { Thread.sleep(forTimeInterval: 0.002) } }
    Thread.sleep(forTimeInterval: 0.5)
    queue.close()
    group.wait()

    let s = queue.snapshot
    let got = consumed.withLock { $0 }
    print("  partB consumed=\(got) expected=\(items) "
        + "wouldHaveCrashed=\(s.wouldHaveCrashed) spuriousAbsorbed=\(s.spuriousAbsorbed)")
    return (got, items, s.wouldHaveCrashed, s.spuriousAbsorbed)
}

// MARK: - Part C — the reference contrast, identical in both builds
//
// A DispatchSemaphore is a COUNTER. Signal it with nobody waiting and the
// permit is stored; the next waiter takes it and proceeds immediately.
// An NSCondition stores NOTHING. Signal it with nobody waiting and the signal
// is gone. That single difference is the whole exercise: a permit is durable,
// an announcement is not, and neither of them is your predicate.

func partC() -> Bool {
    let sem = DispatchSemaphore(value: 0)
    sem.signal()                                      // nobody is waiting yet
    let start = Date()
    let result = sem.wait(timeout: .now() + 1.5)      // takes the stored permit
    let waitedMs = Int(Date().timeIntervalSince(start) * 1000)
    let survived = result == .success && waitedMs < 500
    print("  partC semaphorePermitSurvivedTheGap=\(survived) waitedMs=\(waitedMs)")
    return survived
}

// MARK: - Entry

armWatchdog(WATCHDOG_S, "EX04 build=fixed")

let a = partA()
let b = partB()
let c = partC()

let sleptThroughPublishedData = a.waitedMs >= 1000 && a.dataWasAlreadyPublished
print("EX04 build=fixed "
    + "sleptThroughPublishedData=\(sleptThroughPublishedData) "
    + "partAWaitedMs=\(a.waitedMs) "
    + "wouldHaveCrashed=\(b.wouldHaveCrashed) "
    + "spuriousAbsorbed=\(b.absorbed) "
    + "consumed=\(b.consumed) expected=\(b.expected) "
    + "semaphorePermitDurable=\(c) "
    + "correct=\(!sleptThroughPublishedData && b.wouldHaveCrashed == 0 && b.consumed == b.expected)")
