// EXERCISE 02 — FIXED VARIANT.  The repair is one critical section around the
// whole read-modify-write, and a type the compiler can check.
//
// THE SCENARIO
//   A download manager tallies bytes and chunks from several transfer threads.
//
// THE REPAIR
//   The two counters move inside a single `Mutex` (Synchronization, macOS 15+),
//   which owns them. There is no way to read or write either counter without
//   holding the lock, because `withLock` is the only door. `@unchecked
//   Sendable` is gone with it: `Mutex` is `Sendable` on its own terms, so the
//   compiler checks this type rather than taking our word for it.
//
//   Note what did NOT change: the workload, the thread count, the iteration
//   count. Only the boundary around the mutation moved.
//
//   The two counters are deliberately kept in ONE Mutex rather than two. Two
//   independent locks would still make each counter individually correct while
//   letting a reader observe a byte total that does not match the chunk total.
//
// Build (plain):  swiftc -swift-version 6 -Onone stats.swift -o stats_fixed
// Build (TSan):   swiftc -swift-version 6 -Onone -g -sanitize=thread \
//                        stats.swift -o stats_fixed_tsan
// Run:            ./stats_fixed
//
// Expected: correct=true on every run, with lostChunks=0, and no Thread
// Sanitizer output at all.
//
// Bounded: fixed iteration counts, no blocking waits, plus a watchdog thread
// that force-exits after WATCHDOG_S.

import Foundation
import Synchronization

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

/// The reason this fixture cannot stall a validation run.
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: - the shared tally

/// Two counters updated from every transfer thread, behind one lock that owns
/// them both.
///
/// `bytesReceived += bytes` is still a load, an add and a store. The difference
/// is that the whole sequence now happens inside a critical section, so no
/// other thread can load the same value between our load and our store.
final class DownloadStats: Sendable {
    private struct Totals { var bytes = 0; var chunks = 0 }
    private let totals = Mutex(Totals())

    func record(bytes: Int) {
        totals.withLock { t in
            t.bytes += bytes
            t.chunks += 1
        }
    }

    func snapshot() -> (bytes: Int, chunks: Int) {
        totals.withLock { ($0.bytes, $0.chunks) }
    }
}

// MARK: - the workload

let threads = 8
let perThread = 200_000
let chunkBytes = 4

func runTransfers() -> (bytes: Int, chunks: Int) {
    let stats = DownloadStats()
    let group = DispatchGroup()
    for _ in 0..<threads {
        DispatchQueue.global(qos: .userInitiated).async(group: group) {
            for _ in 0..<perThread {
                stats.record(bytes: chunkBytes)
            }
        }
    }
    group.wait()
    return stats.snapshot()
}

armWatchdog(WATCHDOG_S, "EX02 build=fixed")

let expectedChunks = threads * perThread
let expectedBytes = expectedChunks * chunkBytes
let observed = runTransfers()

print("EX02 build=fixed threads=\(threads) perThread=\(perThread) "
    + "expectedChunks=\(expectedChunks) observedChunks=\(observed.chunks) "
    + "lostChunks=\(expectedChunks - observed.chunks) "
    + "expectedBytes=\(expectedBytes) observedBytes=\(observed.bytes) "
    + "correct=\(observed.chunks == expectedChunks && observed.bytes == expectedBytes)")
