#!/usr/bin/perl
#
# DW::Controller::Importer
#
# Controller for the /tools/importer pages.
#
# Authors:
#      Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2012 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::Controller::Importer;

use strict;
use warnings;

use DW::Routing;
use DW::Task::ImportEraser;
use DW::Template;
use LJ::Hooks;

DW::Routing->register_string( '/tools/importer/erase', \&erase_handler,    app => 1 );
DW::Routing->register_string( '/tools/importer',       \&importer_handler, app => 1 );

sub importer_handler {
    my ( $ok, $rv ) = controller( authas => 1, form_auth => 1 );
    return $rv unless $ok;

    my $r      = $rv->{r};
    my $POST   = $r->post_args;
    my $u      = $rv->{u};
    my $remote = $rv->{remote};
    my $authas = $u->user ne $remote->user ? "?authas=" . $u->user : "";
    my $vars;

    $vars->{remote}             = $remote;
    $vars->{u}                  = $u;
    $vars->{allow_comm_imports} = $LJ::ALLOW_COMM_IMPORTS;
    $vars->{authas}             = $authas;
    $vars->{authas_html}        = $rv->{authas_html};
    my $depth = get_queue();

    $vars->{queue} = join( ', ', map { "$_: " . ( $depth->{ lc $_ } + 0 ) } sort keys %$depth );

    # if these aren't the same users, make sure we're allowed to do a community import
    unless ( $u->equals($remote) ) {
        return error_ml('/tools/importer/index.tt.error.cant_import_comm')
            unless $LJ::ALLOW_COMM_IMPORTS
            && ( $u->can_import_comm || $remote->can_import_comm );
    }

    return error_ml('/tools/importer/index.tt.error.notperson')
        unless $remote->is_person;

    if ( $r->did_post ) {

        return error_ml('.error.invalidform')
            unless LJ::check_form_auth( $POST->{lj_form_auth} );

        if ( $POST->{'import'} ) {
            $vars->{widget} = render_choose_source($vars);
        }
        elsif ( $POST->{'choose_source'} ) {
            my $hn = $POST->{hostname};
            return error_ml('widget.importchoosesource.error.nohostname') unless $hn;

            # be sure to sanitize the username
            my $un = LJ::trim( lc $POST->{username} );
            $un =~ s/-/_/g;

            # be sure to sanitize the usejournal, and require one if they're importing to
            # a community
            my $uj;
            if ( $u->is_community ) {
                $uj = LJ::trim( lc $POST->{usejournal} );
                $uj =~ s/-/_/g;
                return error_ml('/tools/importer/index.tt.error.missing_comm')
                    unless $uj;
            }

            my $pw = LJ::trim( $POST->{password} );
            return error_ml('widget.importchoosesource.error.nocredentials') unless $un && $pw;
            my $error = DW::Logic::Importer->set_import_data_for_user(
                $u,
                hostname   => $hn,
                username   => $un,
                password   => $pw,
                usejournal => $uj
            );
            return error_ml($error) if $error;
            $vars->{widget} = render_choose_data($vars);
        }
        elsif ( $POST->{'choose_data'} ) {
            my $has_items = 0;
            foreach my $key ( keys %$POST ) {
                if ( $key =~ /^lj_/ && $POST->{$key} ) {
                    $has_items = 1;
                    last;
                }
            }
            return error_ml('widget.importchoosedata.error.noitemsselected')
                unless $has_items;

            # if we're doing the suboption, turn on the parent option
            $POST->{lj_entries} = 1
                if $POST->{lj_entries_remap_icon};

            # if comments are on, turn entries on
            $POST->{lj_entries} = 1
                if $POST->{lj_comments};

            # okay, this is kinda hacky but turn on the right things so we can do
            # a proper entry import...
            if ( $POST->{lj_entries} ) {
                $POST->{lj_tags}         = 1;
                $POST->{lj_friendgroups} = 1;
            }

            # if friends are on, turn on groups
            $POST->{lj_friendgroups} = 1
                if $POST->{lj_friends};

            # everybody needs a verifier
            $POST->{lj_verify} = 1;

            # and finally, make sure modes that make no sense for communities are off
            if ( $u->is_community ) {
                $POST->{lj_friends}      = 0;
                $POST->{lj_friendgroups} = 0;
            }

            $vars->{widget} = render_confirm( $vars, $POST );

        }
        elsif ( $POST->{'confirm'} ) {

            # default job status
            my @jobs = (
                [ 'lj_verify',       'ready' ],
                [ 'lj_userpics',     'init' ],
                [ 'lj_bio',          'init' ],
                [ 'lj_tags',         'init' ],
                [ 'lj_friendgroups', 'init' ],
                [ 'lj_friends',      'init' ],
                [ 'lj_entries',      'init' ],
                [ 'lj_comments',     'init' ],
            );

            my %suboptions = ( lj_entries => ['lj_entries_remap_icon'], );

            # get import_data_id for the user
            my $imports = DW::Logic::Importer->get_import_data_for_user($u);

            my $id = $imports->[0]->[0];
            my $error;

            # schedule userpic, bio, and tag imports
            foreach my $item (@jobs) {
                next unless $POST->{ $item->[0] };

                my $suboption = $suboptions{ $item->[0] } || [];
                my %opts;
                foreach (@$suboption) {
                    $opts{$_} = 1 if $POST->{$_};
                }
                $error =
                    DW::Logic::Importer->set_import_items_for_user( $u, item => $item, id => $id );
                return error_ml($error) if $error;

                $error = DW::Logic::Importer->set_import_data_options_for_user(
                    $u,
                    import_data_id => $id,
                    %opts
                );
                return error_ml($error) if $error;
            }
            return $r->redirect("$LJ::SITEROOT/tools/importer$authas");
        }

    }
    else {
        if ( scalar keys %{ DW::Logic::Importer->get_import_items_for_user($u) } ) {
            $vars->{widget} = render_status($vars);
        }
        else {
            $vars->{widget} = render_choose_source($vars);
        }

    }

    return DW::Template->render_template( 'tools/importer/index.tt', $vars );
}

sub render_choose_data {
    my $vars    = shift;
    my $options = [
        {
            name         => 'lj_bio',
            display_name => LJ::Lang::ml('widget.importstatus.item.lj_bio'),
            desc         => LJ::Lang::ml('widget.importchoosedata.item.lj_bio.desc'),
            selected     => 0,
            comm_okay    => 1,
        },
        {
            name         => 'lj_friends',
            display_name => LJ::Lang::ml('widget.importstatus.item.lj_friends'),
            desc         => LJ::Lang::ml('widget.importchoosedata.item.lj_friends.desc'),
            selected     => 0,
            comm_okay    => 0,
        },
        {
            name         => 'lj_friendgroups',
            display_name => LJ::Lang::ml('widget.importstatus.item.lj_friendgroups'),
            desc         => LJ::Lang::ml(
                'widget.importchoosedata.item.lj_friendgroups.desc',
                { sitename => $LJ::SITENAMESHORT }
            ),
            selected  => 0,
            comm_okay => 0,
        },
        {
            name         => 'lj_entries',
            display_name => LJ::Lang::ml('widget.importstatus.item.lj_entries'),
            desc         => LJ::Lang::ml('widget.importchoosedata.item.lj_entries.desc'),
            selected     => 0,
            comm_okay    => 1,
        },
        {
            name         => 'lj_comments',
            display_name => LJ::Lang::ml('widget.importstatus.item.lj_comments'),
            desc         => LJ::Lang::ml('widget.importchoosedata.item.lj_comments.desc'),
            selected     => 0,
            comm_okay    => 1,
        },
        {
            name         => 'lj_tags',
            display_name => LJ::Lang::ml('widget.importstatus.item.lj_tags'),
            desc         => LJ::Lang::ml('widget.importchoosedata.item.lj_tags.desc'),
            selected     => 0,
            comm_okay    => 1,
        },
        {
            name         => 'lj_userpics',
            display_name => LJ::Lang::ml('widget.importstatus.item.lj_userpics'),
            desc         => LJ::Lang::ml(
                'widget.importchoosedata.item.lj_userpics.desc',
                { sitename => $LJ::SITENAMESHORT }
            ),
            selected  => 0,
            comm_okay => 1,
        },
    ];

    my $fixup_options = [
        {
            name         => 'lj_entries_remap_icon',
            display_name => LJ::Lang::ml('widget.importstatus.item.lj_entries_remap_icon'),
            desc         => LJ::Lang::ml('widget.importchoosedata.item.lj_entries_remap_icon.desc'),
            selected     => 0,
        },
    ];

    $vars->{options}       = $options;
    $vars->{fixup_options} = $fixup_options;

    return DW::Template->template_string( 'tools/importer/choose_data.tt', $vars );
}

sub render_choose_source {
    my $vars = shift;

    return error_ml('widget.importchoosesource.disabled1')
        unless LJ::is_enabled('importing');

    my @services;

    for my $service (
        (
            {
                name         => 'livejournal',
                url          => 'livejournal.com',
                display_name => 'LiveJournal',
            },
            {
                name         => 'insanejournal',
                url          => 'insanejournal.com',
                display_name => 'InsaneJournal',
            },
            {
                name         => 'dreamwidth',
                url          => 'dreamwidth.org',
                display_name => 'Dreamwidth',
            },
        )
        )
    {
        # only dev servers can import from Dreamwidth for testing
        next if ( $service->{name} eq 'dreamwidth' ) && !$LJ::IS_DEV_SERVER;
        push @services,
            $service
            if LJ::is_enabled( "external_sites",
            { sitename => $service->{display_name}, domain => $service->{url} } );
    }
    $vars->{services} = \@services;

    return DW::Template->template_string( 'tools/importer/choose_source.tt', $vars );

}

sub render_confirm {
    my ( $vars, $opts ) = @_;

    my @items_fields;
    my @items_display;
    foreach my $item ( keys %$opts ) {
        next unless $item =~ /^lj_/ && $opts->{$item};
        push @items_fields, $item unless $item eq 'lj_form_auth';
        push @items_display, LJ::Lang::ml("widget.importstatus.item.$item")
            unless ( $item eq 'lj_verify' )
            or ( $item eq 'lj_form_auth' );
    }
    my $imports = DW::Logic::Importer->get_import_data_for_user( $vars->{u} );

    $vars->{items_fields}  = \@items_fields;
    $vars->{items_display} = \@items_display;
    $vars->{imports}       = $imports;

    return DW::Template->template_string( 'tools/importer/confirm.tt', $vars );
}

sub render_status {
    my $vars             = shift;
    my $items            = DW::Logic::Importer->get_import_items_for_user( $vars->{u} );
    my $item_to_funcname = {
        lj_bio          => 'DW::Worker::ContentImporter::LiveJournal::Bio',
        lj_tags         => 'DW::Worker::ContentImporter::LiveJournal::Tags',
        lj_entries      => 'DW::Worker::ContentImporter::LiveJournal::Entries',
        lj_comments     => 'DW::Worker::ContentImporter::LiveJournal::Comments',
        lj_userpics     => 'DW::Worker::ContentImporter::LiveJournal::Userpics',
        lj_friends      => 'DW::Worker::ContentImporter::LiveJournal::Friends',
        lj_friendgroups => 'DW::Worker::ContentImporter::LiveJournal::FriendGroups',
        lj_verify       => 'DW::Worker::ContentImporter::LiveJournal::Verify',
    };

    my $dbr;
    my $funcmap;
    my $dupect = 0;
    my $color  = {
        init      => '#333',
        ready     => '#33a',
        queued    => '#3a3',
        failed    => '#a33',
        succeeded => '#0f0',
        aborted   => '#f00'
    };
    foreach my $importid ( sort { $b <=> $a } keys %$items ) {
        my $import_item = $items->{$importid};
        foreach my $item ( sort keys %{ $import_item->{items} } ) {
            my $i = $import_item->{items}->{$item};
            $i->{color} = $color->{ $i->{status} };
            $i->{ago}   = $i->{last_touch} ? LJ::diff_ago_text( $i->{last_touch} ) : "";
            if ( $i->{status} eq 'init' ) {
                $i->{status_txt} = LJ::Lang::ml("widget.importstatus.status.$i->{status}.$item");
            }
            else {
                $i->{status_txt} = LJ::Lang::ml("widget.importstatus.status.$i->{status}");

                if ( $i->{status} eq "aborted" ) {
                    unless ($dbr) {

                        # do manual connection
                        my $db = $LJ::THESCHWARTZ_DBS[0];
                        $dbr = DBI->connect( $db->{dsn}, $db->{user}, $db->{pass} );
                    }

                    if ($dbr) {

                        # get the ids for the function map
                        $funcmap ||=
                            $dbr->selectall_hashref( 'SELECT funcid, funcname FROM funcmap',
                            'funcname' );

                        $dupect = $dbr->selectrow_array(
                            q{SELECT COUNT(*) from job
                                    WHERE funcid  = ?
                                      AND uniqkey = ? },
                            undef, $funcmap->{ $item_to_funcname->{$item} }->{funcid},
                            join( "-", ( $item, $vars->{u}->id ) )
                        );
                    }
                }
            }

            $i->{dupe} =
                $dupect ? " " . LJ::Lang::ml("widget.importstatus.processingprevious") : "";
        }
        $vars->{items}    = $items;
        $vars->{time_ago} = \&LJ::diff_ago_text;
    }
    return DW::Template->template_string( 'tools/importer/status.tt', $vars );
}

sub get_queue {
    my $depth = LJ::MemCache::get('importer_queue_depth');
    unless ($depth) {

        # FIXME: don't make this slam the db with people asking the same question, use a lock
        # FIXME: we don't have ddlockd, maybe we should

        # do manual connection
        my $db  = $LJ::THESCHWARTZ_DBS[0];
        my $dbr = DBI->connect( $db->{dsn}, $db->{user}, $db->{pass} )
            or return "Unable to manually connect to TheSchwartz database.";

        # get the ids for the function map
        my $tmpmap = $dbr->selectall_hashref( 'SELECT funcid, funcname FROM funcmap', 'funcname' );

        # get the counts of jobs in queue (active or not)
        my %cts;
        foreach my $map ( keys %$tmpmap ) {
            next unless $map =~ /^DW::Worker::ContentImporter::LiveJournal::/;

            my $ct = $dbr->selectrow_array(
                q{SELECT COUNT(*) FROM job
                  WHERE funcid = ?
                    AND run_after < UNIX_TIMESTAMP()},
                undef, $tmpmap->{$map}->{funcid}
            ) + 0;

            $map =~ s/^.+::(\w+)$/$1/;
            $cts{ lc $map } = $ct;
        }

        LJ::MemCache::set( 'importer_queue_depth', \%cts, 300 );
        $depth = \%cts;
    }
    return $depth;
}

sub erase_handler {
    my ( $ok, $rv ) = controller( authas => 1 );
    return $rv unless $ok;

    my $r = DW::Request->get;
    unless ( $r->did_post ) {

        # No post, return form.
        return DW::Template->render_template(
            'tools/importer/erase.tt',
            {
                authas_html => $rv->{authas_html},
                u           => $rv->{u},
            }
        );
    }

    my $args = $r->post_args;
    die "Invalid form auth.\n"
        unless LJ::check_form_auth( $args->{lj_form_auth} );

    unless ( $args->{confirm} eq 'DELETE' ) {
        return DW::Template->render_template(
            'tools/importer/erase.tt',
            {
                notconfirmed => 1,
                authas_html  => $rv->{authas_html},
                u            => $rv->{u},
            }
        );
    }

    # Confirmed, let's schedule.
    DW::TaskQueue->dispatch(
        DW::Task::ImportEraser->new(
            {
                userid => $rv->{u}->userid
            }
        )
    ) or die "Failed to insert eraser job.\n";

    return DW::Template->render_template(
        'tools/importer/erase.tt',
        {
            u         => $rv->{u},
            confirmed => 1,
        }
    );
}

1;
