import CodexBarCoreimport Foundation enum CodexDisplacedLivePreservationNoneReason: Equatable { case liveMissing case targetMatchesLiveAuthIdentity} enum CodexDisplacedLivePreservationRejectReason: Equatable { case liveUnreadable case liveAPIKeyOnlyUnsupported case liveIdentityMissingForPreservation case conflictingReadableManagedHome} enum CodexDisplacedLivePreservationImportReason: Equatable { case noExistingManagedDestination} enum CodexDisplacedLivePreservationRefreshReason: Equatable { case readableHomeIdentityMatch case readableHomeIdentityMatchUsingPersistedEmailFallback} enum CodexDisplacedLivePreservationRepairReason: Equatable { case persistedProviderMatchWithMissingHome case persistedProviderMatchWithUnreadableHome case persistedLegacyEmailMatch} enum CodexDisplacedLivePreservationPlan { case none(reason: CodexDisplacedLivePreservationNoneReason) case reject(reason: CodexDisplacedLivePreservationRejectReason) case importNew(reason: CodexDisplacedLivePreservationImportReason) case refreshExisting( destination: PreparedStoredManagedAccount, reason: CodexDisplacedLivePreservationRefreshReason) case repairExisting( destination: PreparedStoredManagedAccount, reason: CodexDisplacedLivePreservationRepairReason)} struct CodexDisplacedLivePreservationPlanner { func makePlan(context: PreparedPromotionContext) -> CodexDisplacedLivePreservationPlan { switch context.live.homeState { case .missing: return .none(reason: .liveMissing) case .unreadable: return .reject(reason: .liveUnreadable) case .apiKeyOnly: return .reject(reason: .liveAPIKeyOnlyUnsupported) case .readable: break } guard let liveAuthIdentity = context.live.authIdentity else { return .reject(reason: .liveIdentityMissingForPreservation) } if let targetAuthIdentity = context.target.authIdentity, CodexIdentityMatcher.matches( targetAuthIdentity.identity, lhsEmail: targetAuthIdentity.email, liveAuthIdentity.identity, rhsEmail: liveAuthIdentity.email) { return .none(reason: .targetMatchesLiveAuthIdentity) } let candidates = context.storedManagedAccounts.filter { $0.persisted.id != context.target.persisted.id } if let destination = self.findReadableHomeMatch(in: candidates, liveAuthIdentity: liveAuthIdentity) { let reason: CodexDisplacedLivePreservationRefreshReason = if liveAuthIdentity.email == nil { .readableHomeIdentityMatchUsingPersistedEmailFallback } else { .readableHomeIdentityMatch } return .refreshExisting(destination: destination, reason: reason) } if self.hasConflictingReadableHome(in: candidates, liveAuthIdentity: liveAuthIdentity) { return .reject(reason: .conflictingReadableManagedHome) } if let repaired = self.findPersistedRepairMatch(in: candidates, liveAuthIdentity: liveAuthIdentity) { return .repairExisting(destination: repaired.destination, reason: repaired.reason) } guard liveAuthIdentity.identity != .unresolved, liveAuthIdentity.email != nil else { return .reject(reason: .liveIdentityMissingForPreservation) } return .importNew(reason: .noExistingManagedDestination) } private func findReadableHomeMatch( in candidates: [PreparedStoredManagedAccount], liveAuthIdentity: PreparedIdentity) -> PreparedStoredManagedAccount? { candidates.first { candidate in guard let candidateAuthIdentity = candidate.authIdentity else { return false } return CodexIdentityMatcher.matches( candidateAuthIdentity.identity, lhsEmail: candidateAuthIdentity.email, liveAuthIdentity.identity, rhsEmail: liveAuthIdentity.email) } } private func findPersistedRepairMatch( in candidates: [PreparedStoredManagedAccount], liveAuthIdentity: PreparedIdentity) -> (destination: PreparedStoredManagedAccount, reason: CodexDisplacedLivePreservationRepairReason)? { switch liveAuthIdentity.identity { case let .providerAccount(id): let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(id) if let destination = candidates.first(where: { guard $0.persisted.providerAccountID == providerAccountID else { return false } guard let liveEmail = liveAuthIdentity.email else { return true } return $0.persisted.email == liveEmail }), let reason = self.providerRepairReason(for: destination) { return (destination, reason) } if let liveEmail = liveAuthIdentity.email, let destination = candidates.first(where: { $0.persisted.providerAccountID == nil && $0.persisted.email == liveEmail }) { return (destination, .persistedLegacyEmailMatch) } return nil case let .emailOnly(normalizedEmail): guard let destination = candidates.first(where: { $0.persisted.providerAccountID == nil && $0.persisted.email == normalizedEmail }) else { return nil } return (destination, .persistedLegacyEmailMatch) case .unresolved: return nil } } private func hasConflictingReadableHome( in candidates: [PreparedStoredManagedAccount], liveAuthIdentity: PreparedIdentity) -> Bool { guard case let .providerAccount(id) = liveAuthIdentity.identity else { return false } let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(id) return candidates.contains { candidate in guard candidate.persisted.providerAccountID == providerAccountID else { return false } if let liveEmail = liveAuthIdentity.email, candidate.persisted.email != liveEmail { return false } guard case .readable = candidate.homeState else { return false } guard let candidateAuthIdentity = candidate.authIdentity else { return false } return !CodexIdentityMatcher.matches( candidateAuthIdentity.identity, lhsEmail: candidateAuthIdentity.email, liveAuthIdentity.identity, rhsEmail: liveAuthIdentity.email) } } private func providerRepairReason( for destination: PreparedStoredManagedAccount) -> CodexDisplacedLivePreservationRepairReason? { switch destination.homeState { case .missing: .persistedProviderMatchWithMissingHome case .unreadable: .persistedProviderMatchWithUnreadableHome case .readable: nil } }}
import CodexBarCore
import Foundation
enum CodexDisplacedLivePreservationNoneReason: Equatable {
case liveMissing
case targetMatchesLiveAuthIdentity
}
enum CodexDisplacedLivePreservationRejectReason: Equatable {
case liveUnreadable
case liveAPIKeyOnlyUnsupported
case liveIdentityMissingForPreservation
case conflictingReadableManagedHome
}
enum CodexDisplacedLivePreservationImportReason: Equatable {
case noExistingManagedDestination
}
enum CodexDisplacedLivePreservationRefreshReason: Equatable {
case readableHomeIdentityMatch
case readableHomeIdentityMatchUsingPersistedEmailFallback
}
enum CodexDisplacedLivePreservationRepairReason: Equatable {
case persistedProviderMatchWithMissingHome
case persistedProviderMatchWithUnreadableHome
case persistedLegacyEmailMatch
}
enum CodexDisplacedLivePreservationPlan {
case none(reason: CodexDisplacedLivePreservationNoneReason)
case reject(reason: CodexDisplacedLivePreservationRejectReason)
case importNew(reason: CodexDisplacedLivePreservationImportReason)
case refreshExisting(
destination: PreparedStoredManagedAccount,
reason: CodexDisplacedLivePreservationRefreshReason)
case repairExisting(
destination: PreparedStoredManagedAccount,
reason: CodexDisplacedLivePreservationRepairReason)
}
struct CodexDisplacedLivePreservationPlanner {
func makePlan(context: PreparedPromotionContext) -> CodexDisplacedLivePreservationPlan {
switch context.live.homeState {
case .missing:
return .none(reason: .liveMissing)
case .unreadable:
return .reject(reason: .liveUnreadable)
case .apiKeyOnly:
return .reject(reason: .liveAPIKeyOnlyUnsupported)
case .readable:
break
}
guard let liveAuthIdentity = context.live.authIdentity else {
return .reject(reason: .liveIdentityMissingForPreservation)
}
if let targetAuthIdentity = context.target.authIdentity,
CodexIdentityMatcher.matches(
targetAuthIdentity.identity,
lhsEmail: targetAuthIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
{
return .none(reason: .targetMatchesLiveAuthIdentity)
}
let candidates = context.storedManagedAccounts.filter { $0.persisted.id != context.target.persisted.id }
if let destination = self.findReadableHomeMatch(in: candidates, liveAuthIdentity: liveAuthIdentity) {
let reason: CodexDisplacedLivePreservationRefreshReason =
if liveAuthIdentity.email == nil {
.readableHomeIdentityMatchUsingPersistedEmailFallback
} else {
.readableHomeIdentityMatch
}
return .refreshExisting(destination: destination, reason: reason)
}
if self.hasConflictingReadableHome(in: candidates, liveAuthIdentity: liveAuthIdentity) {
return .reject(reason: .conflictingReadableManagedHome)
}
if let repaired = self.findPersistedRepairMatch(in: candidates, liveAuthIdentity: liveAuthIdentity) {
return .repairExisting(destination: repaired.destination, reason: repaired.reason)
}
guard liveAuthIdentity.identity != .unresolved, liveAuthIdentity.email != nil else {
return .reject(reason: .liveIdentityMissingForPreservation)
}
return .importNew(reason: .noExistingManagedDestination)
}
private func findReadableHomeMatch(
in candidates: [PreparedStoredManagedAccount],
liveAuthIdentity: PreparedIdentity)
-> PreparedStoredManagedAccount?
{
candidates.first { candidate in
guard let candidateAuthIdentity = candidate.authIdentity else { return false }
return CodexIdentityMatcher.matches(
candidateAuthIdentity.identity,
lhsEmail: candidateAuthIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
}
}
private func findPersistedRepairMatch(
in candidates: [PreparedStoredManagedAccount],
liveAuthIdentity: PreparedIdentity)
-> (destination: PreparedStoredManagedAccount, reason: CodexDisplacedLivePreservationRepairReason)?
{
switch liveAuthIdentity.identity {
case let .providerAccount(id):
let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(id)
if let destination = candidates.first(where: {
guard $0.persisted.providerAccountID == providerAccountID else { return false }
guard let liveEmail = liveAuthIdentity.email else { return true }
return $0.persisted.email == liveEmail
}),
let reason = self.providerRepairReason(for: destination)
{
return (destination, reason)
}
if let liveEmail = liveAuthIdentity.email,
let destination = candidates.first(where: {
$0.persisted.providerAccountID == nil && $0.persisted.email == liveEmail
})
{
return (destination, .persistedLegacyEmailMatch)
}
return nil
case let .emailOnly(normalizedEmail):
guard let destination = candidates.first(where: {
$0.persisted.providerAccountID == nil && $0.persisted.email == normalizedEmail
}) else {
return nil
}
return (destination, .persistedLegacyEmailMatch)
case .unresolved:
return nil
}
}
private func hasConflictingReadableHome(
in candidates: [PreparedStoredManagedAccount],
liveAuthIdentity: PreparedIdentity)
-> Bool
{
guard case let .providerAccount(id) = liveAuthIdentity.identity else {
return false
}
let providerAccountID = ManagedCodexAccount.normalizeProviderAccountID(id)
return candidates.contains { candidate in
guard candidate.persisted.providerAccountID == providerAccountID else { return false }
if let liveEmail = liveAuthIdentity.email, candidate.persisted.email != liveEmail {
return false
}
guard case .readable = candidate.homeState else { return false }
guard let candidateAuthIdentity = candidate.authIdentity else { return false }
return !CodexIdentityMatcher.matches(
candidateAuthIdentity.identity,
lhsEmail: candidateAuthIdentity.email,
liveAuthIdentity.identity,
rhsEmail: liveAuthIdentity.email)
}
}
private func providerRepairReason(
for destination: PreparedStoredManagedAccount)
-> CodexDisplacedLivePreservationRepairReason?
{
switch destination.homeState {
case .missing:
.persistedProviderMatchWithMissingHome
case .unreadable:
.persistedProviderMatchWithUnreadableHome
case .readable:
nil
}
}
}