#!/usr/bin/perl
#
# DW::Shop::Item::Account
#
# Represents a paid account that someone is purchasing.
#
# Authors:
#      Mark Smith <mark@dreamwidth.org>
#      Janine Smith <janine@netrophic.com>
#
# Copyright (c) 2009 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself.  For a copy of the license, please reference
# 'perldoc perlartistic' or 'perldoc perlgpl'.
#

package DW::Shop::Item::Account;

use base 'DW::Shop::Item';

use strict;
use DateTime;
use DW::InviteCodes;
use DW::Pay;

=head1 NAME

DW::Shop::Item::Account - Represents a paid account that someone is purchasing. See
the documentation for DW::Shop::Item for usage examples and description of methods
inherited from that base class.

=head1 API

=head2 C<< $class->new( [ %args ] ) >>

Instantiates an account of some sort to be purchased.

Arguments:
=item ( see DW::Shop::Item ),
=item months => number of months of paid time,
=item class => type of paid account,
=item random => 1 (if gifting paid time to a random user),
=item anonymous_target => 1 (if random user should be anonymous, not identified)

=cut

# override
sub new {
    my ( $class, %args ) = @_;

    if ( $args{anonymous_target} ) {
        return undef unless $args{anonymous_target} == 1;
    }

    if ( $args{random} ) {
        return undef unless $args{random} == 1;
    }

    my $self = $class->SUPER::new(%args);

    if ($self) {
        $self->{months} = $LJ::SHOP{ $self->{type} }->[1];
        $self->{class}  = $LJ::SHOP{ $self->{type} }->[2];
    }

    return $self;
}

# override
sub _apply {
    my $self = $_[0];

    return $self->_apply_email  if $self->t_email;
    return $self->_apply_userid if $self->t_userid;

    # something weird, just kill this item!
    $self->{applied} = 1;
    return 1;
}

# internal application sub, do not call
sub _apply_userid {
    my $self = $_[0];
    return 1 if $self->applied;

    # will need this later
    my $fu = LJ::load_userid( $self->from_userid );
    unless ( $self->anonymous || $self->from_name || $fu ) {
        warn "Failed to apply: NOT anonymous, no from_name, no from_user!\n";
        return 0;
    }

    # need this user
    my $u = LJ::load_userid( $self->t_userid )
        or return 0;

    # try to add the paid time to the user
    LJ::statushistory_add(
        $u->id,
        $self->from_userid,
        'paidstatus',
        sprintf(
            'Order #%d: applied %d months of %s.',
            $self->cartid, $self->months, $self->class_name
        )
    );
    DW::Pay::add_paid_time( $u, $self->class, $self->months )
        or return 0;

    {
        # By definition, things from anonymous purchasers are gifts.
        my @tags = (
            'gift:' .      ( $fu && $fu->equals($u) ? 'no'  : 'yes' ),
            'anonymous:' . ( $self->anonymous       ? 'yes' : 'no' ),
            'type:' . $self->class,
            'target:account'
        );
        DW::Stats::increment( 'dw.shop.paid_account.applied',
            1, [ @tags, 'months:' . $self->months ] );
        DW::Stats::increment( 'dw.shop.paid_account.applied_months', $self->months, [@tags] );
    }

    # we're applied now, regardless of what happens with the email
    $self->{applied} = 1;

    # look up the account's new expiration date
    my $expdate;
    my $paid_status = DW::Pay::get_paid_status($u);

    if ( $paid_status && !$self->permanent ) {
        my $exptime = DateTime->from_epoch( epoch => $paid_status->{expiretime} );
        $expdate = $exptime->ymd;
    }

    # now we have to mail this code
    my ( $body, $subj );
    my $accounttype_string =
        $self->permanent
        ? LJ::Lang::ml( 'shop.email.accounttype.permanent', { type => $self->class_name } )
        : LJ::Lang::ml( 'shop.email.accounttype',
        { type => $self->class_name, nummonths => $self->months } );
    $subj = LJ::Lang::ml( "shop.email.acct.subject", { sitename => $LJ::SITENAME } );

    if ( $u->is_community ) {
        my $maintus = LJ::load_userids( $u->maintainer_userids );
        foreach my $maintu ( values %$maintus ) {
            my $emailtype = $fu && $maintu->equals($fu) ? 'self' : 'other';
            $emailtype = 'anon'     if $self->anonymous;
            $emailtype = 'explicit' if $self->from_name;

            $body =
                LJ::Lang::ml( "shop.email.acct.body.start", { touser => $maintu->display_name } );
            $body .= "\n";
            $body .= LJ::Lang::ml(
                "shop.email.comm.$emailtype",
                {
                    fromuser => $fu ? $fu->display_name : '',
                    commname => $u->display_name,
                    sitename => $LJ::SITENAME,
                    fromname => $self->from_name,
                }
            );

            $body .=
                LJ::Lang::ml( "shop.email.acct.body.type", { accounttype => $accounttype_string } );
            $body .= "\n";
            $body .= LJ::Lang::ml( "shop.email.acct.body.expires", { date => $expdate } )
                if $expdate;
            $body .= "\n";
            $body .= LJ::Lang::ml( "shop.email.acct.body.note", { reason => $self->reason } )
                if $self->reason;

            $body .= LJ::Lang::ml("shop.email.comm.close");
            $body .= LJ::Lang::ml( "shop.email.acct.body.end", { sitename => $LJ::SITENAME } );

            # send the email to the maintainer
            LJ::send_mail(
                {
                    to       => $maintu->email_raw,
                    from     => $LJ::ACCOUNTS_EMAIL,
                    fromname => $LJ::SITENAME,
                    subject  => $subj,
                    body     => $body
                }
            );
        }
    }
    else {
        my $emailtype;
        if ( $self->random ) {
            $emailtype = $self->anonymous ? 'random_anon' : 'random';
        }
        else {
            $emailtype = $fu && $u->equals($fu) ? 'self' : 'other';
            $emailtype = 'anon' if $self->anonymous;
            $emailtype = 'explicit' if $self->from_name;
        }

        $body = LJ::Lang::ml( "shop.email.acct.body.start", { touser => $u->display_name } );
        $body .= "\n";
        $body .= LJ::Lang::ml(
            "shop.email.user.$emailtype",
            {
                fromuser => $fu ? $fu->display_name : '',
                sitename => $LJ::SITENAME,
                fromname => $self->from_name,
            }
        );

        $body .=
            LJ::Lang::ml( "shop.email.acct.body.type", { accounttype => $accounttype_string } );
        $body .= "\n";
        $body .= LJ::Lang::ml( "shop.email.acct.body.expires", { date => $expdate } )
            if $expdate;
        $body .= "\n";
        $body .= LJ::Lang::ml( "shop.email.acct.body.note", { reason => $self->reason } )
            if $self->reason;

        $body .= LJ::Lang::ml("shop.email.user.close");
        $body .= LJ::Lang::ml( "shop.email.acct.body.end", { sitename => $LJ::SITENAME } );

        # send the email to the user
        LJ::send_mail(
            {
                to       => $u->email_raw,
                from     => $LJ::ACCOUNTS_EMAIL,
                fromname => $LJ::SITENAME,
                subject  => $subj,
                body     => $body
            }
        );
    }

    # tell the caller we're happy
    return 1;
}

# internal application sub, do not call
sub _apply_email {
    my $self = $_[0];
    return 1 if $self->applied;

    # will need this later
    my $fu = LJ::load_userid( $self->from_userid );
    unless ( $self->anonymous || $fu ) {
        warn "Failed to apply: NOT anonymous and no from_user!\n";
        return 0;
    }

    {
        # By definition, things sent to email are gifts.
        my @tags = (
            'gift:yes', 'target:email',
            'anonymous:' . ( $self->anonymous ? 'yes' : 'no' ), 'type:' . $self->class
        );
        DW::Stats::increment( 'dw.shop.paid_account.applied',
            1, [ @tags, 'months:' . $self->months ] );
        DW::Stats::increment( 'dw.shop.paid_account.applied_months', $self->months, [@tags] );
    }

    my $reason = join ':', 'payment', $self->class, $self->months;
    my ($code) = DW::InviteCodes->generate( reason => $reason );
    my ($acid) = DW::InviteCodes->decode($code);

    # store in the db
    my $dbh = LJ::get_db_writer()
        or return 0;
    $dbh->do( 'INSERT INTO shop_codes (acid, cartid, itemid) VALUES (?, ?, ?)',
        undef, $acid, $self->cartid, $self->id );
    return 0
        if $dbh->err;

    # now we have to mail this code
    my ( $body, $subj );
    my $accounttype_string =
        $self->permanent
        ? LJ::Lang::ml( 'shop.email.accounttype.permanent', { type => $self->class_name } )
        : LJ::Lang::ml( 'shop.email.accounttype',
        { type => $self->class_name, nummonths => $self->months } );

    my $emailtype = $self->anonymous ? 'anon' : 'other';

    $subj = LJ::Lang::ml( "shop.email.acct.subject", { sitename => $LJ::SITENAME } );

    $body = LJ::Lang::ml( "shop.email.acct.body.start", { touser => $self->t_email } );
    $body .= "\n";
    $body .= LJ::Lang::ml(
        "shop.email.email.$emailtype",
        {
            fromuser => $fu ? $fu->display_name : '',
            sitename => $LJ::SITENAME,
        }
    );

    $body .= LJ::Lang::ml( "shop.email.acct.body.type", { accounttype => $accounttype_string } );
    $body .= LJ::Lang::ml( "shop.email.acct.body.create",
        { createurl => "$LJ::SITEROOT/create?code=$code" } );
    $body .= LJ::Lang::ml( "shop.email.acct.body.note", { reason => $self->reason } )
        if $self->reason;

    $body .= LJ::Lang::ml("shop.email.email.close");
    $body .= LJ::Lang::ml( "shop.email.acct.body.end", { sitename => $LJ::SITENAME } );

    # send the email to the user
    my $rv = LJ::send_mail(
        {
            to       => $self->t_email,
            from     => $LJ::ACCOUNTS_EMAIL,
            fromname => $LJ::SITENAME,
            subject  => $subj,
            body     => $body
        }
    );

    # if this worked, then we're applied! yay!
    if ($rv) {
        $self->{applied} = 1;
        return 1;
    }

    # else ... something naughty happened :(
    warn "Failed to send email!\n";
    return 0;
}

# override
sub unapply {
    my $self = $_[0];
    return unless $self->applied;

    # do the application process now, and if it succeeds...
    $self->{applied} = 0;
    warn "$self->{class} unapplied $self->{months} months\n";

    return 1;
}

# override
sub can_be_added {
    my ( $self, %opts ) = @_;

    my $errref   = $opts{errref};
    my $target_u = LJ::load_userid( $self->t_userid );

    # the receiving user must be a personal or community account
    if ( LJ::isu($target_u) && !$target_u->is_personal && !$target_u->is_community ) {
        $$errref = LJ::Lang::ml('shop.item.account.canbeadded.invalidjournaltype');
        return 0;
    }

    # check to see if we're over the permanent account limit
    if ( $self->permanent && DW::Pay::num_permanent_accounts_available() < 1 ) {
        $$errref = LJ::Lang::ml('shop.item.account.canbeadded.noperms');
        return 0;
    }

    # check to make sure that the target user is valid: not deleted / suspended, etc
    if ( !$opts{user_confirmed} && LJ::isu($target_u) && $target_u->is_inactive ) {
        $$errref = LJ::Lang::ml( 'shop.item.account.canbeadded.notactive',
            { user => $target_u->ljuser_display } );
        return 0;
    }

    # check to make sure the target user's current account type doesn't conflict with the item
    if ( LJ::isu($target_u) ) {
        my $account_type = DW::Pay::get_account_type($target_u);
        if ( $account_type eq 'seed' ) {

            # no paid time can be purchased for seed accounts
            $$errref = LJ::Lang::ml( 'shop.item.account.canbeadded.alreadyperm',
                { user => $target_u->ljuser_display } );
            return 0;
        }
        elsif ( !DW::Shop::Item::Account->allow_account_conversion( $target_u, $self->class ) ) {

            # premium accounts can't get normal paid time
            $$errref = LJ::Lang::ml(
                'shop.item.account.canbeadded.nopaidforpremium',
                { user => $target_u->ljuser_display }
            ) if $self->class eq 'paid';

            # paid accounts can't get premium time as a gift
            $$errref = LJ::Lang::ml(
                'shop.item.account.canbeadded.nopremiumforpaid',
                { user => $target_u->ljuser_display }
            ) if $self->class eq 'premium';

            return 0;
        }
    }

    return 1;
}

# this checks whether we can upgrade/downgrade between premium & paid
sub allow_account_conversion {
    my ( $class, $u, $to ) = @_;

    # no existing user; assume no previous conflicting account
    return 1 unless LJ::isu($u);

    # no previous paid status; assume no conflicts
    my $paid_status = DW::Pay::get_paid_status($u);
    return 1 unless $paid_status;

    my $from = DW::Pay::type_shortname( $paid_status->{typeid} );

    # ok unless we're going from premium to paid or paid to premium
    my $changing = 0;

    $changing = 1 if $from eq 'premium' && $to eq 'paid';
    $changing = 1 if $from eq 'paid'    && $to eq 'premium';

    # doesn't match either scenario, so allow it
    return 1 unless $changing;

    # allow if we're within two weeks of expiration
    return 1 if $paid_status->{expiresin} <= 3600 * 24 * 14;

    # allow upgrading to premium if the remote user owns this account
    my $remote = LJ::get_remote();
    return 1 if $to eq 'premium' && $remote && $remote->can_purchase_for($u);

    return 0;
}

# override
sub conflicts {
    my ( $self, $item ) = @_;

    # if either item are set as "does not conflict" then never say yes
    return
        if $self->cannot_conflict || $item->cannot_conflict;

    # we can only conflict with other items of our own type
    return
        if ref $self ne ref $item;

    # first see if we're talking about the same target
    # note that we're not checking email here because they may want to buy
    # multiple paid accounts and send them to all to the same email address
    # (so they can can create multiple new paid accounts)
    return
        if ( $self->t_userid && ( $self->t_userid != $item->t_userid ) )
        || ( $self->t_email );

    # target same, if both are permanent, then fail because
    # THERE CAN BE ONLY ONE
    return LJ::Lang::ml('shop.item.account.conflicts.multipleperms')
        if $self->permanent && $item->permanent;

    # otherwise ensure that the classes are the same
    return LJ::Lang::ml('shop.item.account.conflicts.differentpaid')
        if $self->class ne $item->class;

    # guess we allow it
    return undef;
}

# override
sub t_html {
    my ( $self, $opts ) = @_;

    if ( $self->anonymous_target ) {
        my $random_user_string = LJ::Lang::ml('shop.item.account.randomuser');
        if ( $opts->{admin} ) {
            my $u = LJ::load_userid( $self->t_userid );
            return "<strong>invalid userid " . $self->t_userid . "</strong>"
                unless $u;
            return "$random_user_string (" . $u->ljuser_display . ")";
        }
        else {
            return "<strong>$random_user_string</strong>";
        }
    }

    # otherwise, fall back upon default display
    return $self->SUPER::t_html($opts);
}

# override
sub name_text {
    my $self = $_[0];

    my $name = $self->class_name;

    if ( $self->cost_points > 0 ) {
        return LJ::Lang::ml( 'shop.item.account.name.perm',
            { name => $name, points => $self->cost_points } )
            if $self->permanent;
        return LJ::Lang::ml( 'shop.item.account.name',
            { name => $name, num => $self->months, points => $self->cost_points } );
    }
    else {
        return $name if $self->permanent;
        return LJ::Lang::ml( 'shop.item.account.name.nopoints',
            { name => $name, num => $self->months } );
    }
}

=head2 C<< $self->class_name >>

Return the display name of this account class.

=cut

sub class_name {
    my $self = $_[0];

    foreach my $cap ( keys %LJ::CAP ) {

        # skip "unused" elements in CAP
        next unless $LJ::CAP{$cap}->{_account_type};

        return $LJ::CAP{$cap}->{_visible_name}
            if $LJ::CAP{$cap}->{_account_type} eq $self->class;
    }

    return 'Invalid Account Class';
}

# simple accessors

=head2 C<< $self->months >>

Number of months of paid time to be applied.

=head2 C<< $self->class >>

Account class identifier; not for display.

=head2 C<< $self->permanent >>

Returns whether this item is for a permanent account, or just a normal paid.

=head2 C<< $self->random >>

Returns whether this item is for a random user.

=head2 C<< $self->anonymous_target >>

Returns whether this item for a random user should go to an anonymous user (true)
or to an identified user (false)

=cut

sub months           { return $_[0]->{months}; }
sub class            { return $_[0]->{class}; }
sub permanent        { return $_[0]->months == 99; }
sub random           { return $_[0]->{random}; }
sub anonymous_target { return $_[0]->{anonymous_target}; }

1;
