import Foundation

public enum CodexDashboardSourceKind: String, Codable, Sendable {
    case liveWeb
    case cachedDashboard
}

public enum CodexDashboardDisposition: String, Codable, Sendable {
    case attach
    case displayOnly
    case failClosed
}

public enum CodexDashboardAllowedEffect: String, Codable, CaseIterable, Hashable, Sendable {
    case usageBackfill
    case creditsAttachment
    case refreshGuardSeed
    case historicalBackfill
    case cachedDashboardReuse
}

public enum CodexDashboardCleanup: String, Codable, CaseIterable, Hashable, Sendable {
    case dashboardSnapshot
    case dashboardDerivedUsage
    case dashboardDerivedCredits
    case dashboardRefreshGuardSeed
    case dashboardCache
}

public struct CodexDashboardKnownOwnerCandidate: Equatable, Hashable, Sendable {
    public let identity: CodexIdentity
    public let normalizedEmail: String?
    public let sourceIsolationIdentifier: String?

    public init(
        identity: CodexIdentity,
        normalizedEmail: String?,
        sourceIsolationIdentifier: String? = nil)
    {
        self.identity = identity
        self.normalizedEmail = normalizedEmail
        self.sourceIsolationIdentifier = sourceIsolationIdentifier
    }

    public func hash(into hasher: inout Hasher) {
        switch self.identity {
        case let .providerAccount(id):
            hasher.combine("providerAccount")
            hasher.combine(id)
        case let .emailOnly(normalizedEmail):
            hasher.combine("emailOnly")
            hasher.combine(normalizedEmail)
        case .unresolved:
            hasher.combine("unresolved")
        }
        hasher.combine(self.normalizedEmail)
        hasher.combine(self.sourceIsolationIdentifier)
    }
}

public struct CodexDashboardOwnershipProofContext: Equatable, Sendable {
    public let currentIdentity: CodexIdentity
    public let expectedScopedEmail: String?
    public let trustedCurrentUsageEmail: String?
    public let dashboardSignedInEmail: String?
    public let knownOwners: [CodexDashboardKnownOwnerCandidate]

    public init(
        currentIdentity: CodexIdentity,
        expectedScopedEmail: String?,
        trustedCurrentUsageEmail: String?,
        dashboardSignedInEmail: String?,
        knownOwners: [CodexDashboardKnownOwnerCandidate])
    {
        self.currentIdentity = currentIdentity
        self.expectedScopedEmail = expectedScopedEmail
        self.trustedCurrentUsageEmail = trustedCurrentUsageEmail
        self.dashboardSignedInEmail = dashboardSignedInEmail
        self.knownOwners = knownOwners
    }
}

public struct CodexDashboardRoutingHints: Equatable, Sendable {
    public let targetEmail: String?
    public let lastKnownDashboardRoutingEmail: String?

    public init(targetEmail: String?, lastKnownDashboardRoutingEmail: String?) {
        self.targetEmail = targetEmail
        self.lastKnownDashboardRoutingEmail = lastKnownDashboardRoutingEmail
    }
}

public struct CodexDashboardAuthorityInput: Equatable, Sendable {
    public let sourceKind: CodexDashboardSourceKind
    public let proof: CodexDashboardOwnershipProofContext
    public let routing: CodexDashboardRoutingHints

    public init(
        sourceKind: CodexDashboardSourceKind,
        proof: CodexDashboardOwnershipProofContext,
        routing: CodexDashboardRoutingHints)
    {
        self.sourceKind = sourceKind
        self.proof = proof
        self.routing = routing
    }
}

public enum CodexDashboardDecisionReason: Equatable, Sendable {
    case exactProviderAccountMatch
    case trustedEmailMatchNoCompetingOwner
    case trustedContinuityNoCompetingOwner
    case wrongEmail(expected: String?, actual: String?)
    case sameEmailAmbiguity(email: String)
    case unresolvedWithoutTrustedEvidence
    case providerAccountMissingScopedEmail
    case providerAccountLacksExactOwnershipProof
    case missingDashboardSignedInEmail
}

public struct CodexDashboardAuthorityDecision: Equatable, Sendable {
    public let disposition: CodexDashboardDisposition
    public let reason: CodexDashboardDecisionReason
    public let allowedEffects: Set<CodexDashboardAllowedEffect>
    public let cleanup: Set<CodexDashboardCleanup>

    public init(
        disposition: CodexDashboardDisposition,
        reason: CodexDashboardDecisionReason,
        allowedEffects: Set<CodexDashboardAllowedEffect>,
        cleanup: Set<CodexDashboardCleanup>)
    {
        self.disposition = disposition
        self.reason = reason
        self.allowedEffects = allowedEffects
        self.cleanup = cleanup
    }
}

public enum CodexDashboardPolicyError: LocalizedError, Equatable, Sendable {
    case displayOnly(CodexDashboardAuthorityDecision)

    public var errorDescription: String? {
        switch self {
        case .displayOnly:
            "Codex dashboard may be displayed, but it cannot be attached to the active account."
        }
    }
}

public enum CodexDashboardAuthority {
    /// Evaluates whether a Codex dashboard snapshot may attach to the active account.
    ///
    /// App callers may keep `.displayOnly` dashboard data visible, but must not attach usage,
    /// credits, refresh guards, or historical backfill.
    ///
    /// CLI callers should surface `.displayOnly` as `CodexDashboardPolicyError.displayOnly`
    /// instead of treating it as a generic failure.
    ///
    /// Cached dashboard callers may restore data only when the decision includes
    /// `.cachedDashboardReuse` in `allowedEffects`.
    public static func evaluate(_ input: CodexDashboardAuthorityInput) -> CodexDashboardAuthorityDecision {
        let proof = input.proof
        let currentIdentity = Self.normalizeIdentity(proof.currentIdentity)
        let expectedScopedEmail = CodexIdentityResolver.normalizeEmail(proof.expectedScopedEmail)
        let trustedCurrentUsageEmail = CodexIdentityResolver.normalizeEmail(proof.trustedCurrentUsageEmail)
        let dashboardSignedInEmail = CodexIdentityResolver.normalizeEmail(proof.dashboardSignedInEmail)
        let knownOwners = Self.normalizeKnownOwners(proof.knownOwners)

        // Routing hints are intentionally excluded from ownership proof. They may help fetch or route
        // dashboard requests, but they must never influence attach/display/fail-closed policy.

        guard let dashboardSignedInEmail else {
            return Self.makeDecision(
                disposition: .failClosed,
                reason: .missingDashboardSignedInEmail,
                sourceKind: input.sourceKind)
        }

        if let expectedScopedEmail, dashboardSignedInEmail != expectedScopedEmail {
            return Self.makeDecision(
                disposition: .failClosed,
                reason: .wrongEmail(expected: expectedScopedEmail, actual: dashboardSignedInEmail),
                sourceKind: input.sourceKind)
        }

        switch currentIdentity {
        case let .providerAccount(id):
            guard expectedScopedEmail != nil else {
                return Self.makeDecision(
                    disposition: .failClosed,
                    reason: .providerAccountMissingScopedEmail,
                    sourceKind: input.sourceKind)
            }
            if Self.knownOwnerCount(for: dashboardSignedInEmail, in: knownOwners) > 1 {
                return Self.makeDecision(
                    disposition: .displayOnly,
                    reason: .sameEmailAmbiguity(email: dashboardSignedInEmail),
                    sourceKind: input.sourceKind)
            }
            let exactMatch = knownOwners.contains { candidate in
                candidate.identity == .providerAccount(id: id) && candidate.normalizedEmail == dashboardSignedInEmail
            }
            if exactMatch {
                return Self.makeDecision(
                    disposition: .attach,
                    reason: .exactProviderAccountMatch,
                    sourceKind: input.sourceKind)
            }
            return Self.makeDecision(
                disposition: .failClosed,
                reason: .providerAccountLacksExactOwnershipProof,
                sourceKind: input.sourceKind)

        case let .emailOnly(normalizedEmail):
            guard dashboardSignedInEmail == normalizedEmail else {
                return Self.makeDecision(
                    disposition: .failClosed,
                    reason: .wrongEmail(expected: normalizedEmail, actual: dashboardSignedInEmail),
                    sourceKind: input.sourceKind)
            }
            if Self.knownOwnerCount(for: normalizedEmail, in: knownOwners) > 1 {
                return Self.makeDecision(
                    disposition: .displayOnly,
                    reason: .sameEmailAmbiguity(email: normalizedEmail),
                    sourceKind: input.sourceKind)
            }
            return Self.makeDecision(
                disposition: .attach,
                reason: .trustedEmailMatchNoCompetingOwner,
                sourceKind: input.sourceKind)

        case .unresolved:
            guard let trustedCurrentUsageEmail else {
                return Self.makeDecision(
                    disposition: .failClosed,
                    reason: .unresolvedWithoutTrustedEvidence,
                    sourceKind: input.sourceKind)
            }
            guard dashboardSignedInEmail == trustedCurrentUsageEmail else {
                return Self.makeDecision(
                    disposition: .failClosed,
                    reason: .wrongEmail(expected: trustedCurrentUsageEmail, actual: dashboardSignedInEmail),
                    sourceKind: input.sourceKind)
            }
            if Self.knownOwnerCount(for: trustedCurrentUsageEmail, in: knownOwners) > 1 {
                return Self.makeDecision(
                    disposition: .displayOnly,
                    reason: .sameEmailAmbiguity(email: trustedCurrentUsageEmail),
                    sourceKind: input.sourceKind)
            }
            return Self.makeDecision(
                disposition: .attach,
                reason: .trustedContinuityNoCompetingOwner,
                sourceKind: input.sourceKind)
        }
    }

    private static func normalizeIdentity(_ identity: CodexIdentity) -> CodexIdentity {
        switch identity {
        case let .providerAccount(id):
            if let normalizedID = CodexIdentityResolver.normalizeAccountID(id) {
                return .providerAccount(id: normalizedID)
            }
            return .unresolved
        case let .emailOnly(normalizedEmail):
            if let normalizedEmail = CodexIdentityResolver.normalizeEmail(normalizedEmail) {
                return .emailOnly(normalizedEmail: normalizedEmail)
            }
            return .unresolved
        case .unresolved:
            return .unresolved
        }
    }

    private static func normalizeKnownOwners(
        _ candidates: [CodexDashboardKnownOwnerCandidate])
        -> Set<CodexDashboardKnownOwnerCandidate>
    {
        Set(candidates.map { candidate in
            let identity = self.normalizeIdentity(candidate.identity)
            let sourceIsolationIdentifier: String? = switch identity {
            case .providerAccount:
                nil
            case .emailOnly, .unresolved:
                candidate.sourceIsolationIdentifier
            }
            return CodexDashboardKnownOwnerCandidate(
                identity: identity,
                normalizedEmail: CodexIdentityResolver.normalizeEmail(candidate.normalizedEmail),
                sourceIsolationIdentifier: sourceIsolationIdentifier)
        })
    }

    private static func knownOwnerCount(
        for email: String,
        in candidates: Set<CodexDashboardKnownOwnerCandidate>) -> Int
    {
        candidates.count { $0.normalizedEmail == email }
    }

    private static func makeDecision(
        disposition: CodexDashboardDisposition,
        reason: CodexDashboardDecisionReason,
        sourceKind: CodexDashboardSourceKind) -> CodexDashboardAuthorityDecision
    {
        CodexDashboardAuthorityDecision(
            disposition: disposition,
            reason: reason,
            allowedEffects: self.allowedEffects(disposition: disposition, sourceKind: sourceKind),
            cleanup: disposition == .attach ? [] : Set(CodexDashboardCleanup.allCases))
    }

    private static func allowedEffects(
        disposition: CodexDashboardDisposition,
        sourceKind: CodexDashboardSourceKind) -> Set<CodexDashboardAllowedEffect>
    {
        guard disposition == .attach else { return [] }

        switch sourceKind {
        case .liveWeb:
            return [
                .usageBackfill,
                .creditsAttachment,
                .refreshGuardSeed,
                .historicalBackfill,
            ]
        case .cachedDashboard:
            return [.cachedDashboardReuse]
        }
    }
}
