#!/usr/bin/perl
#
# DW::VirtualGiftTransaction - Support virtual gift transactions
#
# Authors:
#      Jen Griffin <kareila@livejournal.com>
#
# Copyright (c) 2012-2013 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::VirtualGiftTransaction;

use strict;
use warnings;

use DW::Shop::Cart;
use DW::VirtualGift;

# Because events use this module, Perl warns about redefined subroutines.
{
    no warnings 'redefine';
    use LJ::Event::VgiftDelivered;
}

# IMPLEMENTATION: a blessed hashref with some or all of the following keys:
#
# From database: transid, rcptid, vgiftid, buyerid, cartid, delivery_t,
#                accepted, delivered, expired
#
# From associated shop cart: anon, from, reason, from_text
#
# For convenience: id, user, vgift, buyer, timestamp (more useful forms)
#
# Uniqueness of a transaction is determined from rcptid + transid.
#
#
# USAGE:
#
# DW::VirtualGiftTransaction->load( user => u/uid, id => transid );
# -- loads an existing transaction, returns object
#
# DW::VirtualGiftTransaction->save( user => u/uid, vgift => vgiftid );
# -- saves a new transaction, returns transaction ID
#
# DW::VirtualGiftTransaction->list( user => u/uid, profile => 1/0 );
# -- returns the list of transaction objects for the given user
#
#
# Methods for transaction objects:
#
# Properties: id, u, url
# Queries: is_delivered, is_accepted, is_expired, is_anonymous
# Actions: remove, expire, accept, deliver, notify_delivered
# Display: view, from_html, from_text

sub save {
    my ( $class, %opts ) = @_;

    # opts: user => (u or userid) - mandatory
    #       vgift => (obj or id) - mandatory
    #       cartid => (cartid) - optional
    #       buyer => (u or userid) - optional
    #       time => (epoch seconds) - optional (defaults to current time)

    my $vg    = $opts{vgift} or return;
    my $vgift = ref $vg ? $vg : DW::VirtualGift->new($vg);
    return unless $vgift && $vgift->id;

    my $u       = LJ::want_user( $opts{user} ) or return;
    my $id      = LJ::alloc_user_counter( $u, 'V' ) or return;
    my $secs    = $opts{time} || time;
    my $buyer   = LJ::want_user( $opts{buyer} );
    my $buyerid = $buyer ? $buyer->id : 0;

    $u->do(
        'INSERT INTO vgift_trans (transid, rcptid, vgiftid, buyerid,'
            . ' cartid, delivery_t) VALUES (?, ?, ?, ?, ?, ?)',
        undef, $id, $u->id, $vgift->id, $buyerid, $opts{cartid}, $secs
    );
    die $u->errstr if $u->err;

    # update the vgift_counts table
    $vgift->mark_sold;

    # memcache expiration for list of all transactions
    LJ::MemCache::delete( $class->_transaction_list_memkey($u) );

    return $id;    # not object
}

sub list {
    my ( $class, %opts ) = @_;
    my $u = LJ::want_user( $opts{user} ) or return;

    my $memkey = $class->_transaction_list_memkey($u);
    my $data   = LJ::MemCache::get($memkey);

    unless ( defined $data ) {

        # Note: we pretend undelivered gifts don't exist yet.
        $data = $u->selectcol_arrayref(
            "SELECT transid FROM vgift_trans"
                . " WHERE rcptid=? AND delivered='Y' ORDER BY delivery_t DESC, "
                . " transid DESC",
            undef, $u->id
        ) || [];
        die $u->errstr if $u->err;
        LJ::MemCache::set( $memkey, $data );
    }

    # transform transaction IDs to objects
    my @loaded = grep { defined } map { $class->load( user => $u, id => $_ ) } @$data;

    # do any further filtering of results in caller
    return @loaded unless $opts{profile};

    # special case: profile only shows accepted & non-expired
    return grep { $_->is_accepted && !$_->is_expired } @loaded;
}

sub load {
    my ( $class, %opts ) = @_;

    # opts: user => (u or userid) - mandatory
    #       id => (transaction id) - mandatory

    return unless defined $opts{id};
    my $id = $opts{id} + 0;
    my $u  = LJ::want_user( $opts{user} ) or return;

    my $memkey = $class->_transaction_load_memkey( $u, $id );
    my $data   = LJ::MemCache::get($memkey);

    unless ( defined $data ) {
        $data = $u->selectrow_hashref(
            'SELECT transid, rcptid, vgiftid,'
                . ' buyerid, cartid, delivery_t, accepted, delivered, expired'
                . ' FROM vgift_trans WHERE rcptid=? AND transid=?',
            undef, $u->id, $id
        ) || {};
        die $u->errstr if $u->err;

        if ( my $item = $class->_search_cart($data) ) {
            $data->{reason} = $item->reason;
            $data->{anon}   = $item->anonymous;

            # from_html takes care of the anon/email/username display
            # if the item is found.  otherwise fall back to using buyerid.
            $data->{from} =
                $item->from_html ne LJ::Lang::ml('error.nojournal') ? $item->from_html : undef;
            $data->{from_text} =
                $item->from_text ne LJ::Lang::ml('error.nojournal') ? $item->from_text : undef;
        }

        LJ::MemCache::set( $memkey, $data );
    }

    return {} unless %$data;

    # populate some extra hash keys for convenience
    $data->{id}        = $data->{transid};
    $data->{user}      = LJ::want_user( $data->{rcptid} );
    $data->{vgift}     = DW::VirtualGift->new( $data->{vgiftid} );
    $data->{buyer}     = LJ::want_user( $data->{buyerid} );
    $data->{timestamp} = LJ::mysql_time( $data->{delivery_t} );

    return $class->new($data);
}

sub _search_cart {
    my ( $class, $data ) = @_;
    my $cart = DW::Shop::Cart->get_from_cartid( $data->{cartid} )
        or return;

    foreach my $item ( @{ $cart->items } ) {
        next unless ref $item eq 'DW::Shop::Item::VirtualGift';
        next unless $data->{rcptid} == $item->t_userid;
        next unless $data->{transid} == $item->vgift_transid;

        # if we get here, it's the right item
        return $item;
    }

    # we didn't find it - sadness
    return undef;
}

sub _transaction_load_memkey {
    my ( $class, $u, $id ) = @_;
    my $uid = $u->id or return;
    return [ $uid, "vgift.trans.$id" ];    # caches database row
}

sub _transaction_list_memkey {
    my ( $class, $u ) = @_;
    my $uid = $u->id or return;
    return [ $uid, "vgift.translist.$uid" ];    # caches list of transids
}

sub new {
    my ( $class, $self ) = @_;
    $class = ref $class if ref $class;
    return $self if ref $self eq $class;        # already blessed

    my ( $id, $uid ) = ( $self->{transid}, $self->{rcptid} );
    return unless ( $id && $id =~ /^\d+$/ ) && ( $uid && $uid =~ /^\d+$/ );

    bless $self, $class;
    return $self;
}

### OBJECT METHODS ###

sub id { $_[0]->{id} }
sub u  { $_[0]->{user} }

sub is_delivered { $_[0]->{delivered} eq 'Y' }
sub is_accepted  { $_[0]->{accepted} eq 'Y' }
sub is_expired   { $_[0]->{expired} eq 'Y' }

sub is_anonymous { $_[0]->{anon} ? 1 : 0 }

sub from_html {
    my ($self) = @_;
    return $self->{from} if defined $self->{from};
    return $self->{buyer}->ljuser_display if LJ::isu( $self->{buyer} );

    # undefined if neither of these is valid
}

sub from_text {
    my ($self) = @_;
    return $self->{from_text} if defined $self->{from_text};
    return $self->{buyer}->display_name if LJ::isu( $self->{buyer} );

    # undefined if neither of these is valid
}

sub _update {
    my ( $self, $sql, $expire ) = @_;
    my ( $id, $u ) = ( $self->id, $self->u );
    return unless $id  && LJ::isu($u);
    return unless $sql && $sql !~ /\?/;

    $u->do( "$sql WHERE rcptid=? AND transid=?", undef, $u->id, $id );
    die $u->errstr if $u->err;

    # memcache expiration for this one transaction
    LJ::MemCache::delete( $self->_transaction_load_memkey( $u, $id ) );

    # memcache expiration for list of all transactions
    # only needed for deliveries and deletions
    LJ::MemCache::delete( $self->_transaction_list_memkey($u) )
        if $expire;

    return 1;
}

sub remove {
    my ($self) = @_;
    return $self->_update( 'DELETE FROM vgift_trans', 1 );
}

sub accept {
    my ($self) = @_;
    return 1 if $self->is_accepted;    # already accepted
    $self->{accepted} = 'Y';           # update object in memory
    return $self->_update('UPDATE vgift_trans SET accepted="Y"');
}

sub deliver {
    my ($self) = @_;
    return 1 if $self->is_delivered;    # already delivered
    $self->{delivered} = 'Y';           # update object in memory
    return $self->_update( 'UPDATE vgift_trans SET delivered="Y", ' . 'delivery_t=UNIX_TIMESTAMP()',
        1 );
}

sub notify_delivered {
    my ($self) = @_;

    # make sure the gift was actually delivered
    return unless $self->is_delivered;

    # notify the user (no opt-out)
    my @args = ( $self->u, $self->id );
    LJ::Event::VgiftDelivered->new(@args)->fire;
}

sub url {
    my ($self) = @_;
    return unless LJ::isu( $self->u );
    return $self->u->journal_base . "/vgifts/" . $self->id;
}

sub view {

    # print mini view for profile page; standalone page should be TT
    # (expects virtualgift class in htdocs/stc/profile.css)

    my ($self) = @_;
    my $vg = $self->{vgift};
    return '' unless $vg && $vg->id;

    my $disp = $vg->img_small_html;

    # substitute the gift name if no image is set
    $disp = $vg->name_ehtml unless $disp =~ /^<img/i;

    my $url = $self->url;
    $disp = "<a href='$url'>$disp</a>" if $url;

    my $ret = "<div class='virtualgift'>$disp<br />";

    my $from_word = LJ::Lang::ml('widget.shopcart.header.from');
    my $anon_word = LJ::Lang::ml('widget.shopcart.anonymous');

    if ( $self->{anon} ) {
        $ret .= $from_word . " " . $anon_word;
    }
    elsif ( $self->{from} ) {

        # show the cached result of the cart item's from_html method
        $ret .= $from_word . " " . $self->{from};
    }
    elsif ( LJ::isu( $self->{buyer} ) ) {
        $ret .= $from_word . " " . $self->{buyer}->ljuser_display;
    }
    else {
        # if we can't show a user name, just print anonymous
        $ret .= $from_word . " " . $anon_word;
    }

    $ret .= "</div>\n";

    return $ret;
}

1;
