import Foundation

/// The rider-facing ride lifecycle  documents/lifecycle-and-screens.md §1.2.
///
/// Modelled as an explicit machine rather than a pile of booleans because this is the
/// flow where a wrong state either charges someone incorrectly or strands them. Each
/// case maps to exactly one screen, so an impossible combination can't be represented.
///
///   idle → validating → unlocking → active → confirmEnd → photo → ending → completed
///                    ↘ blocked             ↘ (cancel/expire) → idle
enum RideFlowState {

    /// No ride. The rider is browsing or scanning.
    case idle

    /// Allocation request in flight.
    case validating

    /// The rider must accept the privacy policy (and terms) before this ride can start.
    ///
    /// Carries the pending code so accepting continues into the ride they asked for rather than
    /// dropping them back on the map. booking-service enforces the same rule (451), so this is
    /// the step, not the lock.
    case consent(documents: [LegalDocument], pendingQrCode: String, submitting: Bool, failed: Bool)

    /// PENDING_UNLOCK. The HTTP 202 is NOT confirmation  we wait for `lock.status`
    /// to flip the ride to ACTIVE, observed by polling. `elapsedSeconds` drives the
    /// "taking longer than usual" copy.
    case unlocking(ride: RideResponse, elapsedSeconds: Int)

    /// ACTIVE. Live timer, running fare, scooter battery.
    case active(ride: RideResponse, elapsedSeconds: Int, runningFare: Double, batteryPercent: Int?)

    /// Pre-charge summary  the rider sees what they'll pay before committing.
    case confirmEnd(ride: RideResponse, elapsedSeconds: Int, estimatedFare: Double, balanceAfter: Double?)

    /// Mandatory parking photo. booking-service rejects an end without one (400),
    /// so this state cannot be skipped.
    case photo(ride: RideResponse, captured: Data?, uploading: Bool)

    /// End request in flight: zone check → lock close → fare → wallet debit.
    case ending(ride: RideResponse)

    /// COMPLETED. Receipt + rating.
    case completed(ride: RideResponse, invoice: InvoiceResponse, rating: Int, ratingSubmitted: Bool)

    /// A refusal the rider can act on. Never a raw error string.
    case blocked(reason: BlockedReason)
}

/// Why the ride couldn't start or end, in terms the rider can act on.
///
/// booking-service returns a `ProblemDetail` with an HTTP status and an English `detail`
/// string  it has no machine-readable error code yet (tracked in
/// documents/lifecycle-and-screens.md, Part 5). Until it does, we classify on status +
/// message keyword here so the rider always gets localised, actionable copy.
enum BlockedReason {
    /// 402  wallet below the minimum. Offer top-up inline.
    case insufficientBalance

    /// 451: a document must be accepted first. Handled as a step, never as a dead end.
    case consentRequired

    /// 402 while ending. Different from the above in the only way that matters to the rider: the
    /// ride is still running and the scooter is still theirs, so "top up to start a ride" is the
    /// wrong sentence. The fare can outrun the wallet on a long ride.
    case balanceToEnd
    /// 409  another rider won the allocation race (the DB unique index rejected ours).
    case scooterInUse
    /// 409  not AVAILABLE: maintenance, or no lock paired.
    case scooterUnavailable
    /// 400  outside an allowed zone at ride end.
    case outsideZone
    /// 400  end attempted without a parking photo. Shouldn't reach the rider.
    case photoRequired
    /// 404  unknown QR code.
    case scooterNotFound
    /// The lock never confirmed and the allocation expired. The rider was NOT charged.
    case unlockTimeout
    /// Transport failure, 5xx, or anything unclassified.
    case unknown

    /// Maps a booking-service failure onto an actionable reason.
    static func classify(_ error: Error) -> BlockedReason {
        // `error` is an existential  it has to be cast before the case can be matched.
        guard let apiError = error as? APIError,
              case let .http(status, message) = apiError else { return .unknown }
        let detail = message.lowercased()
        switch status {
        case 451: return .consentRequired
        case 402: return .insufficientBalance
        case 404: return .scooterNotFound
        case 409: return detail.contains("active ride") ? .scooterInUse : .scooterUnavailable
        case 400 where detail.contains("photo"): return .photoRequired
        case 400 where detail.contains("zone"): return .outsideZone
        default: return .unknown
        }
    }

    /// Localised title / body keys. Every reason says what happened AND what to do next.
    var copy: (title: String, body: String) {
        switch self {
        case .insufficientBalance: return ("blocked_balance_title", "blocked_balance_body")
        case .consentRequired: return ("blocked_consent_title", "blocked_consent_body")
        case .balanceToEnd: return ("blocked_end_balance_title", "blocked_end_balance_body")
        case .scooterInUse: return ("blocked_in_use_title", "blocked_in_use_body")
        case .scooterUnavailable: return ("blocked_unavailable_title", "blocked_unavailable_body")
        case .outsideZone: return ("blocked_zone_title", "blocked_zone_body")
        case .photoRequired: return ("blocked_photo_title", "blocked_photo_body")
        case .scooterNotFound: return ("blocked_not_found_title", "blocked_not_found_body")
        case .unlockTimeout: return ("blocked_timeout_title", "blocked_timeout_body")
        case .unknown: return ("blocked_unknown_title", "blocked_unknown_body")
        }
    }

    /// The primary action's label  "find another" only where another scooter helps.
    var actionKey: String {
        switch self {
        case .consentRequired: return "consent_accept"
        case .insufficientBalance: return "action_top_up"
        case .balanceToEnd: return "action_top_up"
        case .scooterInUse, .scooterUnavailable, .unlockTimeout: return "action_find_another"
        default: return "action_try_again"
        }
    }
}
