struct Machine{M<:Model, T<:Number}
    model::M
    preference::Preference{T}
    users::Int
    items::Int
end

model(machine::Machine) = machine.model
users(machine::Machine) = machine.users
items(machine::Machine) = machine.items
preference(machine::Machine) = machine.preference

checkuser(machine::Machine, user::Int) = (user >= 0 && user <= users(machine)) ? user : error("invalid user id ($user)")
checkitem(machine::Machine, item::Int) = (item >= 0 && item <= items(machine)) ? item : error("invalid item id ($item)")

function train!(machine::Machine, dataset::Dataset; max_epochs::Int = 1000)
    m = model(machine)
    error = Inf
    for epoch in 1:max_epochs
        update!(m, dataset)
        e = objective(m, dataset)

        if e > error
            break
        end

        error = e
    end
end

function machine(model::Model, dataset::Dataset)
    u = users(dataset)
    v = items(dataset)
    pref = preference(dataset)
    return Machine(model, pref, u, v)
end

function Base.getindex(machine::Machine{M, T}, user::Int, item::Int) where {M<:Model, T<:Number}
    checkuser(machine, user)
    checkitem(machine, item)
    
    m = model(machine)
    pref = preference(machine)
    value = predict(m, pref, user, item)

    if !ismissing(value)
        return PredictRating(correct(value, pref), pref)
    end

    return MissingRating{T}()
end

function Base.getindex(machine::Machine{M, T}, dataset::Dataset{T}) where {M<:Model, T<:Number}
    predicts = Vector{UserPreference{T}}(undef, length(dataset))

    for i in 1:length(dataset)
        u, v, r = dataset[i]
        predicts[i] = UserPreference{T}(u, v, machine[u, v])
    end

    return predicts
end
