#!/usr/bin/perl
# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by 
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License. 
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.

package LJ::Worker::BirthdayNotify;

use strict;
BEGIN {
    require "$ENV{LJHOME}/cgi-bin/ljlib.pl";
}

use base 'LJ::Worker::Manual';
use LJ::Event::Birthday;
use List::Util ();

# send out for notifications up to two days in the future
my $advance_notify = $LJ::BIRTHDAY_NOTIFS_ADVANCE || 2*86400; # 2 days

# delay for polling clusters that last had no users
my $delay_when_none = 15;

# how long to wait if we didn't process any at all
my $sleep_when_idle = 10;

# mapping of clusterid -> when they were last empty
my %last_empty;

# the cluster we're working on. used in several places.
my $working_clusterid;

sub work {
    my $class = shift;
    my @uids = @_;

    # pick a cluster to work on, get a lock
    my $lock;
    foreach my $cid (List::Util::shuffle(@LJ::CLUSTERS)) {
        last if @uids;

        debug("Checking cluster: $cid");
        next unless cluster_needs_work($cid);

        $lock = LJ::locker()->trylock("birthday-notify:" . $cid);
        next unless $lock;

        # got a lock and a reader
        debug("Fetching users from cluster: $cid");
        push @uids, get_users_from_cluster($cid);

        # found a clusterid to work on!
        $working_clusterid = $cid;

        last;
    }

    # when we return 0 here, we'll be considered idle
    return 0 unless @uids;

    my $us = LJ::load_userids(@uids);
    my $ct = 0;

    # a multiloader to fetch everyone's birthdays currently, so
    # we can discard notifications for people whose birthdays
    # have already passed (eg, worker backlog)
    my $bdays = LJ::User->next_birthdays(@uids);

    foreach my $u (values %$us) {
        # don't want to process read-only users (being moved?)
        next if $u->readonly;

        # if a user's data is still on this cluster, but the user isn't,
        # we want to enter lazy-cleanup mode so we don't spin on these users
        # (this is a superset of the case of expunged users)
        if ($u->clusterid != $working_clusterid) {
            remove_user_from_cluster($u, $working_clusterid);
            next;
        }

        # do this first, so we properly update even if we don't notify
        # next out if the setter failed (eg, invalid birthday)
        $u->set_next_birthday or next;

        # don't mail if their birthday's already passed
        next if $bdays->{$u->id} < time();

        next unless $u->should_fire_birthday_notif;

        debug("Firing off notification for " . $u->user);
        LJ::Event::Birthday->new($u)->fire;
        $ct++;
    }

    # return and release our lock as it falls out of scope

    return $ct;
}

sub on_idle {
    return sleep $sleep_when_idle;
}

sub debug {
    LJ::Worker::Manual->cond_debug(@_);
}


# checks if a cluster has pending notifications to send out.
sub cluster_needs_work {
    my $cluster = shift;

    # don't hammer a cluster if we don't have anyone on it.
    return if $last_empty{$cluster} && $delay_when_none > time() - $last_empty{$cluster};

    # now check if there are pending notifications on the cluster
    my $dbcr = LJ::get_cluster_def_reader($cluster);
    return unless $dbcr;

    my $ct = $dbcr->selectrow_array("SELECT userid FROM birthdays WHERE " .
                                    "nextbirthday < UNIX_TIMESTAMP() + $advance_notify LIMIT 1")+0;
    die $dbcr->errstr if $dbcr->err;
    return 1 if $ct;

    # otherwise, we had nobody. make a note of that.
    $last_empty{$cluster} = time();
    return undef;
}

sub get_users_from_cluster {
    my $cluster = shift;

    my $dbcr = LJ::get_cluster_def_reader($cluster);
    die "Unable to get cluster reader for cluster $cluster" unless $dbcr;

    my $userids = $dbcr->selectcol_arrayref("SELECT userid FROM birthdays " .
                                            "WHERE nextbirthday < UNIX_TIMESTAMP() + $advance_notify LIMIT 1000");
    die $dbcr->errstr if $dbcr->err;

    return @$userids;
}

sub remove_user_from_cluster {
    my ($u, $cid) = @_;
    debug("Cleaning up for moved user: " . $u->user);

    my $dbh = LJ::get_cluster_master($cid)
        or die "Unable to get cluster reader for cluster: $cid";

    $dbh->do("DELETE FROM birthdays WHERE userid = ?", undef, $u->id);
    die $dbh->errstr if $dbh->err;
}

################################################################################

# to be able to work on one specific user
if (@ARGV) {
    my $user = shift;
    my $u = LJ::load_user($user);
    $working_clusterid = $u->clusterid;
    __PACKAGE__->work($u->id);
} else {
    __PACKAGE__->run();
}
