#!/usr/bin/perl
#
# DW::User::DVersion::Migrate8To9 - Handling dversion 8 to 9 migration
#
# Authors:
#      Andrea Nall <anall@andreanall.com>
#
# Copyright (c) 2010 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::User::DVersion::Migrate8To9;

use strict;
use warnings;

require 'ljlib.pl';
use LJ::User;
use Time::HiRes qw( usleep );

sub do_upgrade {
    my $readonly_bit;

    foreach ( keys %LJ::CAP ) {
        if (   $LJ::CAP{$_}->{'_name'} eq "_moveinprogress"
            && $LJ::CAP{$_}->{'readonly'} == 1 )
        {
            $readonly_bit = $_;
            last;
        }
    }
    unless ( defined $readonly_bit ) {
        die
"Won't move user without %LJ::CAP capability class named '_moveinprogress' with readonly => 1\n";
    }

    my $logpropid  = LJ::get_prop( log  => 'picture_keyword' )->{id};
    my $talkpropid = LJ::get_prop( talk => 'picture_keyword' )->{id};

    my $logpropid_map  = LJ::get_prop( log  => 'picture_mapid' )->{id};
    my $talkpropid_map = LJ::get_prop( talk => 'picture_mapid' )->{id};

    my $BLOCK_INSERT = 25;

    my ($u) = @_;

    return 0 if $u->readonly;

    return 1 if $u->dversion >= 9;

    # we really cannot have the user doing things during this process
    $u->modify_caps( [$readonly_bit], [] );

    # wait a quarter of a second, give any request the user might be doing a chance to stop
    # as the user changing things could lead to slight data loss re. userpic selection
    # on entries and comments
    usleep(250000);

    # do this in an eval so, in case something dies, we don't leave the user locked
    my $rv = 0;

    eval {
        # Unfortunately, we need to iterate over all clusters to get a list
        # of used keywords so we can give proper ids to everything,
        # even removed keywords
        my %keywords;
        my %to_update;
        if ( $u->is_individual ) {
            foreach my $cluster_id (@LJ::CLUSTERS) {
                my $dbcm_o = LJ::get_cluster_master($cluster_id);

                my $entries = $dbcm_o->selectall_arrayref(
                    q{
                    SELECT log2.journalid AS journalid, 
                           log2.jitemid AS jitemid,
                           logprop2.value AS value
                    FROM logprop2 
                    INNER JOIN log2 
                        ON ( logprop2.journalid = log2.journalid 
                            AND logprop2.jitemid = log2.jitemid ) 
                    WHERE posterid = ? 
                        AND propid=?
                    }, undef, $u->id, $logpropid
                );
                die $dbcm_o->errstr if $dbcm_o->err;
                my $comments = $dbcm_o->selectall_arrayref(
                    q{
                    SELECT talkprop2.journalid AS journalid,
                           talkprop2.jtalkid AS jtalkid,
                           talkprop2.value AS value
                    FROM talkprop2
                    INNER JOIN talk2
                        ON ( talkprop2.journalid = talk2.journalid
                            AND talkprop2.jtalkid = talk2.jtalkid )
                    WHERE posterid = ?
                        AND tpropid=?
                    }, undef, $u->id, $talkpropid
                );
                die $dbcm_o->errstr if $dbcm_o->err;

                $to_update{$cluster_id} = {
                    entries  => $entries,
                    comments => $comments,
                };
                $keywords{ $_->[2] }->{count}++ foreach ( @$entries, @$comments );
            }
        }

        my $origmap = $u->selectall_hashref(
            q{
            SELECT kwid, picid FROM userpicmap2 WHERE userid=?
            }, "kwid", undef, $u->id
        );
        die $u->errstr if $u->err;

        my $picmap = $u->selectall_hashref(
            q{
            SELECT picid, state FROM userpic2 WHERE userid=?
            }, "picid", undef, $u->id
        );
        die $u->errstr if $u->err;

        my %outrows;

        my %kwid_map;

        foreach my $k ( keys %keywords ) {
            if ( $k =~ m/^pic#(\d+)$/ ) {
                my $picid = $1;
                next if !exists $picmap->{$picid} || $picmap->{$picid}->{state} eq 'X';
                $keywords{$k}->{kwid}  = undef;
                $keywords{$k}->{picid} = $picid;
                $outrows{$picid}->{0}++;
            }
            else {
                my $kwid = $u->get_keyword_id( $k, 1 );
                $kwid_map{$kwid} = $k;
                my $picid = $origmap->{$kwid}->{picid};
                $keywords{$k}->{kwid}  = $kwid;
                $keywords{$k}->{picid} = $picid;
                $outrows{ $picid || 0 }->{$kwid}++;
            }
        }

        foreach my $r ( values %$origmap ) {
            $outrows{ $r->{picid} }->{ $r->{kwid} }++ if $r->{picid} && $r->{kwid};
        }

        {
            my ( @bind, @vals );

            # flush rows to destination table
            my $flush = sub {
                return unless @bind;

                # insert data
                my $bind = join( ",", @bind );
                $u->do( "REPLACE INTO userpicmap3 (userid,mapid,kwid,picid) VALUES $bind",
                    undef, @vals );
                die $u->errstr if $u->err;

                # reset values
                @bind = ();
                @vals = ();
            };

            foreach my $picid ( sort { $a <=> $b } keys %outrows ) {
                foreach my $kwid ( sort { $a <=> $b } keys %{ $outrows{$picid} } ) {
                    next if $kwid == 0 && $picid == 0;
                    push @bind, "(?,?,?,?)";
                    my $mapid   = LJ::alloc_user_counter( $u, 'Y' );
                    my $keyword = $kwid == 0 ? "pic#$picid" : $kwid_map{$kwid};

        # if $keyword is undef, this isn't used on any entries, so we don't care about the mapid
        # however, if $kwid is undef, this is a pic#xxx keyword, and had to have existed on an entry
                    $keywords{$keyword}->{mapid} = $mapid if defined $keyword;
                    push @vals, ( $u->id, $mapid, $kwid || undef, $picid || undef );
                    $flush->() if @bind > $BLOCK_INSERT;
                }
            }
            $flush->();
        }

        if ( $u->is_individual ) {
            foreach my $cluster_id (@LJ::CLUSTERS) {
                next unless $to_update{$cluster_id};
                my $data = $to_update{$cluster_id};

                my $dbcm_o = LJ::get_cluster_master($cluster_id);

                {
                    my ( @bind, @vals );

                    # flush rows to destination table
                    my $flush = sub {
                        return unless @bind;

                        # insert data
                        my $bind = join( ",", @bind );
                        $dbcm_o->do(
                            "REPLACE INTO logprop2 (journalid,jitemid,propid,value) VALUES $bind",
                            undef, @vals );
                        die $u->errstr if $u->err;

                        # reset values
                        @bind = ();
                        @vals = ();
                    };

                    foreach my $entry ( @{ $data->{entries} } ) {
                        next unless $keywords{ $entry->[2] }->{mapid};
                        push @bind, "(?,?,?,?)";
                        push @vals,
                            (
                            $entry->[0], $entry->[1], $logpropid_map,
                            $keywords{ $entry->[2] }->{mapid}
                            );
                        $flush->() if @bind > $BLOCK_INSERT;
                    }
                    $flush->();

                    foreach my $entry ( @{ $data->{entries} } ) {
                        LJ::MemCache::delete(
                            [ $entry->[0], "logprop:" . $entry->[0] . ":" . $entry->[1] ] );
                    }
                }
                {
                    my ( @bind, @vals );

                    # flush rows to destination table
                    my $flush = sub {
                        return unless @bind;

                        # insert data
                        my $bind = join( ",", @bind );
                        $dbcm_o->do(
                            "REPLACE INTO talkprop2 (journalid,jtalkid,tpropid,value) VALUES $bind",
                            undef, @vals
                        );
                        die $u->errstr if $u->err;

                        # reset values
                        @bind = ();
                        @vals = ();
                    };

                    foreach my $comment ( @{ $data->{comments} } ) {
                        next unless $keywords{ $comment->[2] }->{mapid};
                        push @bind, "(?,?,?,?)";
                        push @vals,
                            (
                            $comment->[0], $comment->[1], $talkpropid_map,
                            $keywords{ $comment->[2] }->{mapid}
                            );
                        $flush->() if @bind > $BLOCK_INSERT;
                    }
                    $flush->();

                    foreach my $comment ( @{ $data->{comments} } ) {
                        LJ::MemCache::delete(
                            [ $comment->[0], "talkprop:" . $comment->[0] . ":" . $comment->[1] ] );
                    }
                }
            }
        }

        $rv = 1;
    };

    my $err = $@;

    # okay, we're done, the user can do things again
    $u->modify_caps( [], [$readonly_bit] );

    die $err if $err;

    return $rv;
}

sub upgrade_to_dversion_9 {

    # If user has been purged, go ahead and update version
    # Otherwise move their data
    my $ok = $_[0]->is_expunged ? 1 : do_upgrade(@_);

    $_[0]->update_self( { 'dversion' => 9 } ) if $ok;

    LJ::Userpic->delete_cache( $_[0] );

    return $ok;
}

*LJ::User::upgrade_to_dversion_9 = \&upgrade_to_dversion_9;
