import Foundation
import os

/// B3 · Rotation timing of an installed enrolled (Secure-Enclave) identity. The
/// scheduler reads this to decide whether to pre-empt expiry. The private key
/// itself never leaves the Secure Enclave, so only advisory timing surfaces.
public struct DeviceRenewalState: Equatable, Sendable {
    /// The enrolled device id — drives `POST /device/:id/renew`.
    public let deviceId: String
    /// Leaf `notAfter` (hard expiry — the TLS stack rejects past it).
    public let notAfter: Date?
    /// When to renew from the same hardware key (~2/3 of the lifetime).
    public let renewAfter: Date?

    public init(deviceId: String, notAfter: Date?, renewAfter: Date?) {
        self.deviceId = deviceId
        self.notAfter = notAfter
        self.renewAfter = renewAfter
    }

    /// Is the leaf due for renewal as of `now`? A missing `renewAfter` NEVER
    /// triggers (fail-safe — the TLS stack is the real gate; the scheduler only
    /// pre-empts expiry). Mirrors `EnrollmentResult.isRenewalDue`.
    public func isRenewalDue(asOf now: Date) -> Bool {
        guard let renewAfter else { return false }
        return now >= renewAfter
    }
}

/// B3 · The silent rotation scheduler: on a trigger (app foreground / launch) it
/// checks the installed identity's rotation timing and, if the renew window has
/// opened, drives an mTLS renew against the SAME Secure-Enclave key. This is the
/// "one bootstrap tap, then never again" half of zero-touch — after the first
/// login+enroll, the cert renews itself with no human action.
///
/// Deliberately pure and transport-free: `renewalState` and `performRenew` are
/// injected closures, so the whole decision table is unit-testable with fakes
/// (no keychain, no network). Production wires `renewalState` to the keychain
/// store and `performRenew` to an mTLS `DeviceEnrollmentClient` renew (nil
/// bearer — the current cert authenticates).
///
/// Failure posture: a keychain read fault or a renew error is surfaced as
/// `.failed` and logged (never a secret), NEVER a crash — the existing cert
/// stays valid until `notAfter`, so a transient renew failure is recoverable on
/// the next trigger.
public struct CertificateRotationScheduler: Sendable {
    /// Current rotation timing; `nil` = nothing enrolled to renew.
    private let renewalState: @Sendable () throws -> DeviceRenewalState?
    /// The mTLS renew operation (re-CSR from the SE key → replace the leaf).
    private let performRenew: @Sendable () async throws -> ClientCertificateSummary?
    private let now: @Sendable () -> Date

    public init(
        renewalState: @escaping @Sendable () throws -> DeviceRenewalState?,
        performRenew: @escaping @Sendable () async throws -> ClientCertificateSummary?,
        now: @escaping @Sendable () -> Date = { Date() }
    ) {
        self.renewalState = renewalState
        self.performRenew = performRenew
        self.now = now
    }

    /// The outcome of a single scheduler pass — total over the decision table.
    public enum Outcome: Equatable, Sendable {
        /// No enrolled identity (fresh install / legacy `.p12`): nothing to do.
        case notEnrolled
        /// Enrolled but the renew window has not opened yet.
        case notDue(renewAfter: Date?)
        /// The renew succeeded; the new leaf is installed for the next handshake.
        case renewed
        /// A read/renew error occurred; logged, cert still valid until expiry.
        case failed
    }

    /// Run one pass: read timing → decide → renew if due. Idempotent and safe to
    /// call on every foreground; `.notDue` is the common (cheap) case.
    public func runIfDue() async -> Outcome {
        let state: DeviceRenewalState?
        do {
            state = try renewalState()
        } catch {
            RotationLog.log.error(
                "rotation: renewal-state read failed: \(String(describing: error), privacy: .public)"
            )
            return .failed
        }
        guard let state else { return .notEnrolled }
        guard state.isRenewalDue(asOf: now()) else {
            return .notDue(renewAfter: state.renewAfter)
        }
        do {
            _ = try await performRenew()
            RotationLog.log.info("rotation: device certificate renewed silently")
            return .renewed
        } catch {
            // No secrets in the log — only the error shape (never token/cert bytes).
            RotationLog.log.error(
                "rotation: silent renew failed: \(String(describing: error), privacy: .public)"
            )
            return .failed
        }
    }
}

private enum RotationLog {
    static let log = Logger(subsystem: "com.yaojia.webterm", category: "cert-rotation")
}
