import Foundation

/// What the player may do right now, and why.
///
/// The paid half of this is Apple's answer, not ours, and is passed in. The free half is counted by
/// `TrialTally`.
public enum Entitlement: Sendable, Equatable {
    /// A current subscription. Everything is open.
    case subscribed
    /// Not paying, with free hands still to play.
    case trial(handsLeft: Int)
    /// Not paying, and the free hands are gone.
    case lapsed

    /// Whether a new hand may be dealt.
    public var mayPlay: Bool {
        switch self {
        case .subscribed: return true
        case .trial(let left): return left > 0
        case .lapsed: return false
        }
    }

    /// Whether to show what somebody has already played. Always true: hands somebody played are
    /// theirs, and holding their own history hostage to a payment is a thing this app does not do.
    /// Only new hands are gated.
    public var maySeeHistory: Bool { true }
}

/// Somewhere a single number survives the app closing.
///
/// A protocol rather than `UserDefaults` directly so the count can be tested without touching the
/// machine's real preferences, and so the store can be swapped for the keychain later without
/// anything above here noticing — see `TrialTally` for why that swap may eventually be wanted.
public protocol TallyStore: AnyObject {
    func integer(forKey key: String) -> Int
    func set(_ value: Int, forKey key: String)
}

/// `UserDefaults` already has exactly this shape.
extension UserDefaults: TallyStore {}

/// A `TallyStore` that forgets everything when it goes out of scope. For tests.
public final class InMemoryTally: TallyStore {
    private var values: [String: Int] = [:]
    public init() {}
    public func integer(forKey key: String) -> Int { values[key] ?? 0 }
    public func set(_ value: Int, forKey key: String) { values[key] = value }
}

/// How many hands somebody has played against their free allowance.
///
/// Kept as its own number rather than counted off the saved hands (Ken, 2026-08-09). The reason
/// that matters: `HistoryStore.clear` — the "Start over" button — throws away every saved hand, and
/// a count derived from those hands would reset with them. Start over is for resetting your poker
/// record. It is not for resetting your bill, and this tally deliberately survives it.
///
/// The cost of keeping it separate is that it can drift from the history: lose a write, restore a
/// backup, and the tally and the hands disagree. That is accepted. When they disagree the tally is
/// the one that decides whether somebody may play, because it is the one that cannot be cleared
/// from inside the app.
///
/// What it does NOT survive is deleting the app, which takes `UserDefaults` with it. If free hands
/// being farmed by reinstalling ever becomes a real problem, move the storage to the keychain —
/// that survives deletion — by writing a `TallyStore` that reads and writes there. Nothing above
/// this line would change. It is not worth doing pre-emptively for a $9.99 subscription.
public final class TrialTally {
    private let store: TallyStore
    private let key: String

    public init(store: TallyStore = UserDefaults.standard, key: String = AppVariant.current.tallyKey) {
        self.store = store
        self.key = key
    }

    /// How many hands have been spent. Never goes down.
    public var handsPlayed: Int { max(0, store.integer(forKey: key)) }

    /// Count a hand that has been played.
    ///
    /// Takes the record rather than being a bare increment so there is exactly one place that
    /// decides which hands count, and the caller cannot get it wrong by forgetting.
    @discardableResult
    public func record(_ hand: HandRecord) -> Int {
        guard FreeTrial.counts(hand) else { return handsPlayed }
        let next = handsPlayed + 1
        store.set(next, forKey: key)
        return next
    }

    /// What somebody is entitled to right now.
    public func entitlement(subscribed: Bool) -> Entitlement {
        FreeTrial.entitlement(subscribed: subscribed, handsPlayed: handsPlayed)
    }

    /// Wipe the count. Deliberately NOT called by "Start over" — this exists for tests, and for a
    /// support reply to somebody who was charged wrongly.
    public func reset() { store.set(0, forKey: key) }
}

/// The rule: how many hands are free, which ones count, and when to say something about it.
public enum FreeTrial {

    /// The free allowance.
    ///
    /// Set to 100 by Ken on 2026-09-11, down from 300, with the shape of the free product changed
    /// at the same time. The free product is no longer "a settled rating" — that needs
    /// `Coach.handsToSettle` hands and more — it is "your most common habit, named": see
    /// `FreeView`. A hundred hands is enough for a loud habit to clear its own floor (playing too
    /// many hands, limping, folding the big blind), and the assessment's forty hands are the
    /// designed way to get there: assessment → the habit named → sixty more hands of table, drill
    /// or re-check → the paywall.
    ///
    /// It is a product decision, it lives in one place, and every test derives from it rather than
    /// writing the number out.
    public static let freeHands = 100

    /// Whether a hand eats into the free allowance. Every hand does.
    ///
    /// The assessment used to be exempt as "the demonstration". With the allowance at a hundred
    /// that would have made the free tier 140 hands, and "play 100 hands" is what was asked for;
    /// the assessment is forty hands of the product, and its report is where the free habit is
    /// first named. Drills and re-checks count too: an uncounted drill would be an unlimited free
    /// table for anybody who noticed.
    public static func counts(_ hand: HandRecord) -> Bool { true }

    /// How many free hands are left, never below zero.
    public static func handsLeft(played: Int) -> Int { max(0, freeHands - max(0, played)) }

    /// What somebody is entitled to, given Apple's answer about their subscription and the tally.
    ///
    /// A subscriber is `.subscribed` regardless of the count — somebody who subscribes, lapses and
    /// comes back does not find their old hands held against them.
    public static func entitlement(subscribed: Bool, handsPlayed: Int) -> Entitlement {
        if subscribed { return .subscribed }
        let left = handsLeft(played: handsPlayed)
        return left > 0 ? .trial(handsLeft: left) : .lapsed
    }

    /// When to warn somebody the free hands are running out, so the paywall is not the first they
    /// hear of the price. Nobody enjoys being stopped mid-sitting by a bill.
    public static let warnWithHandsLeft = 20

    /// Whether to say something about the allowance now. False for a subscriber, false while the
    /// end is far off, and false once it has been hit — at that point the paywall is the message
    /// and a warning stacked on top of it is noise.
    public static func shouldWarn(_ entitlement: Entitlement) -> Bool {
        if case .trial(let left) = entitlement { return left <= warnWithHandsLeft }
        return false
    }
}
