import Foundation
#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif

/// Deterministic replica placement (docs/CARBON_SHARDING.md §5, D2–D4): every member computes
/// the same replica set from the same roster with **no coordination** — no placement map is
/// gossiped or persisted (a cache is always recomputable).
///
///     score(m)   = SHA256(projectUUID_bytes ‖ m.hostID_bytes)   // big-endian UInt256 compare
///     base       = top-R of (eligible \ pinned) by descending score, R = 2
///     replicaSet = pinned ∪ base
///
/// Pure functions only — roster in, replica set out — so the whole policy is table-testable.
public enum CarbonPlacement {
    /// D3: replicas beyond the owner. Fewer eligible members ⇒ replicate to what exists and
    /// *warn* (`reduced redundancy` / `not protected`); never block the session.
    public static let defaultReplicaCount = 2

    /// A roster member as placement sees it (from mesh-roster gossip + key custody).
    public struct Member: Sendable, Equatable {
        public let hostID: String
        /// Macs + runners only; never phones (capability `canMirrorCarbon`).
        public let canMirrorCarbon: Bool
        /// Holds (or is queued to receive) the project key — a placement precondition
        /// (spec §4.3): a keyless member can serve blobs but not be *counted*.
        public let holdsProjectKey: Bool
        /// D4: pinned always-on members (runners default true; user may pin Macs) are always
        /// included, on top of the rendezvous winners.
        public let alwaysOn: Bool

        public init(
            hostID: String, canMirrorCarbon: Bool = true, holdsProjectKey: Bool = true,
            alwaysOn: Bool = false
        ) {
            self.hostID = hostID
            self.canMirrorCarbon = canMirrorCarbon
            self.holdsProjectKey = holdsProjectKey
            self.alwaysOn = alwaysOn
        }
    }

    /// Members eligible to hold a project's shards (spec §5.1): capability + key custody,
    /// excluding the current owner (it holds its own data implicitly and isn't counted in R).
    public static func eligible(members: [Member], ownerHostID: String) -> [Member] {
        members.filter { $0.canMirrorCarbon && $0.holdsProjectKey && $0.hostID != ownerHostID }
    }

    /// The deterministic replica set (spec §5.2): pinned ∪ top-R rendezvous winners.
    public static func replicaSet(
        projectUUID: UUID, members: [Member], ownerHostID: String,
        replicaCount: Int = defaultReplicaCount
    ) -> Set<String> {
        let eligible = eligible(members: members, ownerHostID: ownerHostID)
        let pinned = eligible.filter(\.alwaysOn)
        let unpinned = eligible.filter { !$0.alwaysOn }
        let winners = unpinned
            .map { (member: $0, score: score(projectUUID: projectUUID, hostID: $0.hostID)) }
            .sorted { lexGreater($0.score, $1.score) }
            .prefix(max(replicaCount, 0))
        return Set(pinned.map(\.hostID) + winners.map(\.member.hostID))
    }

    /// Rendezvous score: SHA-256 over the project UUID's 16 raw bytes followed by the host
    /// ID's UTF-8 bytes, compared as a big-endian unsigned integer (bytewise lexicographic).
    static func score(projectUUID: UUID, hostID: String) -> [UInt8] {
        var input = Data(capacity: 16 + hostID.utf8.count)
        let u = projectUUID.uuid
        input.append(contentsOf: [
            u.0, u.1, u.2, u.3, u.4, u.5, u.6, u.7,
            u.8, u.9, u.10, u.11, u.12, u.13, u.14, u.15,
        ])
        input.append(contentsOf: Array(hostID.utf8))
        return Array(SHA256.hash(data: input))
    }

    private static func lexGreater(_ a: [UInt8], _ b: [UInt8]) -> Bool {
        for (x, y) in zip(a, b) where x != y { return x > y }
        return false
    }
}
