package Bric::App::Callback::Desk;

use base qw(Bric::App::Callback);
# Note special name 'desk_asset' so it doesn't conflict
# with 'desk' in Profile/Desk.pm
__PACKAGE__->register_subclass;
use constant CLASS_KEY => 'desk_asset';

use strict;
use Bric::App::Authz qw(:all);
use Bric::App::Session qw(:state :user);
use Bric::App::Event qw(log_event);
use Bric::App::Util qw(:msg :pkg :aref);
use Bric::App::Callback::Publish;
use Bric::App::Callback::Workspace;
use Bric::Biz::Asset::Business::Media;
use Bric::Biz::Asset::Business::Story;
use Bric::Biz::Asset::Template;
use Bric::Biz::Workflow;
use Bric::Biz::Workflow::Parts::Desk;
use Bric::Config qw(:ui :pub);
use Bric::Util::Burner;
use Bric::Util::DBI qw(:junction);
use Bric::Util::Priv::Parts::Const qw(:all);
use Bric::Util::Time qw(strfdate);
use Bric::Util::Fault qw(throw_error);

my $pkgs = {
    story    => 'Bric::Biz::Asset::Business::Story',
    media    => 'Bric::Biz::Asset::Business::Media',
    template => 'Bric::Biz::Asset::Template',
};
my $keys = [ keys %$pkgs ];

my $type      = 'template';

sub checkin : Callback {
    my $self = shift;

    my $a_id    = $self->value;
    my $a_class = $self->params->{$self->class_key.'|asset_class'};
    my $pkg     = get_package_name($a_class);
    my $a_obj   = $pkg->lookup({'id' => $a_id, checkout => 1});
    my $d       = $a_obj->get_current_desk;

    $d->checkin($a_obj);
    $d->save;

    if ($a_class eq 'template') {
        my $sb = Bric::Util::Burner->new({user_id => get_user_id()});
           $sb->undeploy($a_obj);
    }

    log_event("${a_class}_checkin", $a_obj, { Version => $a_obj->get_version });
}

sub checkout : Callback {
    my $self = shift;

    my $a_id    = $self->value;
    my $a_class = $self->params->{$self->class_key.'|asset_class'};
    my $pkg     = get_package_name($a_class);
    my $a_obj   = $pkg->lookup({'id' => $a_id});
    my $d       = $a_obj->get_current_desk;

    $d->checkout($a_obj, get_user_id());
    $d->save;
    $a_obj->save;
    log_event("${a_class}_checkout", $a_obj);

    $a_id = $a_obj->get_id;

    my $profile;
    if ($a_class eq 'template') {
        my $sb = Bric::Util::Burner->new({user_id => get_user_id() });
        $sb->deploy($a_obj);

        $profile = '/workflow/profile/template';
    } elsif ($a_class eq 'media') {
        $profile = '/workflow/profile/media';
    } else {
        $profile = '/workflow/profile/story';
    }

    $self->set_redirect("$profile/$a_id/?checkout=1");
}

sub move : Callback {
    my $self = shift;

    # Accept one or more assets to be moved to another desk.
    my $next_desk = $self->params->{$self->class_key.'|next_desk'};
    my $assets    = ref $next_desk ? $next_desk : [$next_desk];

    my ($a_id, $a_class, $d_id, $pkg, %wfs);
    foreach my $a (@$assets) {
        my ($a_id, $from_id, $to_id, $a_class, $wfid) = split('-', $a);

        # Do not move assets where the user has not chosen a next desk.
        # And where the desk ID is the same.
        next unless ($to_id and $to_id != $from_id) || $from_id eq 'shelve';

        my $pkg   = get_package_name($a_class);
        my $a_obj = $pkg->lookup({'id' => $a_id});

        unless ($a_obj->is_current) {
            add_msg('Cannot move [_1] asset "[_2]" while it is checked out.',
                    $a_class, $a_obj->get_name);
            next;
        }

        if ($to_id eq 'shelve') {
            # Remove from the current desk and from the workflow.
            log_event("$a_class\_save", $a_obj);
            my $cur_desk = $a_obj->get_current_desk;
            if ($a_obj->get_checked_out) {
                $cur_desk->checkin($a_obj);
                log_event("$a_class\_checkin", $a_obj, {
                    Version => $a_obj->get_version
                });
            }
            $cur_desk->remove_asset($a_obj)->save;
            $a_obj->set_workflow_id(undef);
            $a_obj->save;
            log_event("$a_class\_rem_workflow", $a_obj);
            next;
        }

        my $dpkg = 'Bric::Biz::Workflow::Parts::Desk';

        # Get the current desk and the next desk.
        my $d    = $dpkg->lookup({'id' => $from_id});
        my $next = $dpkg->lookup({'id' => $to_id});

        # Transfer from the current to the next.
        $d->transfer({'to'    => $next,
                      'asset' => $a_obj});

        # Save both desks.
        $d->save;
        $next->save;

        if (ALLOW_WORKFLOW_TRANSFER) {
            if ($wfid != $a_obj->get_workflow_id) {
                # Transfer workflows.
                $a_obj->set_workflow_id($wfid);
                $a_obj->save;
                my $wf = $wfs{$wfid} ||=
                  Bric::Biz::Workflow->lookup({ id => $wfid });
                log_event("${a_class}_add_workflow", $a_obj,
                          { Workflow => $wf->get_name });
            }
        }

        # Log an event
        log_event("${a_class}_moved", $a_obj, { Desk => $next->get_name });
    }
}

sub publish : Callback {
    my $self = shift;
    my $param = $self->params;
    my $story_pub = $param->{story_pub} || {};
    my $media_pub = $param->{media_pub} || {};
    my $mpkg = 'Bric::Biz::Asset::Business::Media';
    my $spkg = 'Bric::Biz::Asset::Business::Story';
    my $story = mk_aref($param->{$self->class_key.'|story_pub_ids'});
    my $media = mk_aref($param->{$self->class_key.'|media_pub_ids'});
    my (@rel_story, @rel_media);

    # start with the objects checked for publish
    my @stories = values %$story_pub;
    my @media   = values %$media_pub;
    push @stories, $spkg->list({ version_id => ANY(@$story) }) if @$story;
    push @media,   $mpkg->list({ version_id => ANY(@$media) }) if @$media;

    my %selected = (
        story => { map { $_->get_id => undef } @stories },
        media => { map { $_->get_id => undef } @media   },
    );

    my (@sids, @mids, %desks);

    my (%seen, @messages);

    for ([story => \@stories, $story_pub],
         [media => \@media,   $media_pub]
     ) {
        my ($key, $objs, $pub_ids) = @$_;
        # iterate through objects looking for related and stories
        while (@$objs) {
            my $doc = shift @$objs or next;

            # haven't I seen you someplace before?
            my $vid = $doc->get_version_id;
            my $id =  $doc->get_id;
            next if $seen{"$key$id"};

            unless (chk_authz($doc, PUBLISH, 1)) {
                my $doc_disp_name = lc get_disp_name($key);
                push @messages, [
                    'You do not have permission to publish '
                    . qq{$doc_disp_name "[_1]"},
                    $doc->get_name,
                ];
                next;
            }

            if ($doc->get_checked_out) {
                # Cannot publish checked-out assets.
                my $doc_disp_name = lc get_disp_name($key);
                push @messages,[
                    'Cannot publish $doc_disp_name "[_1]" because it is '
                    . " checked out.",
                    $doc->get_name,
                ];
                delete $pub_ids->{$vid};
                next;
            }

            # Hang on to your hat!
            my $ids = $key eq 'story' ? \@sids : \@mids;
            push @$ids, $vid;

            # Examine all the related objects.
            if (PUBLISH_RELATED_ASSETS) {
                foreach my $rel ($doc->get_related_objects) {
                    # Skip assets that don't need to be published.
                    next unless $rel->needs_publish;
                    # Skip deactivated documents.
                    next unless $rel->is_active;

                    # haven't I seen you someplace before?
                    my $relid  = $rel->get_id;
                    my $relkey = $rel->key_name;
                    next if exists $selected{$relkey}{$relid}
                        || $seen{"$relkey$relid"};
                    my $relvid = $rel->get_version_id;

                    if ($rel->get_checked_out) {
                        # Cannot publish checked-out assets.
                        my $rel_disp_name = lc get_disp_name($rel->key_name);
                        push @messages,[
                            "Cannot auto-publish related $rel_disp_name "
                            . '"[_1]" because it is checked out.',
                            $rel->get_name,
                        ];
                        next;
                    }

                    if ($rel->get_workflow_id) {
                        # It must be on a publish desk.
                        my $did = $rel->get_desk_id;
                        my $desk = $desks{$did}
                            ||= Bric::Biz::Workflow::Parts::Desk->lookup({
                                id => $did,
                            });
                        unless ($desk->can_publish) {
                            my $rel_disp_name = lc get_disp_name($rel->key_name);
                            push @messages,[
                                "Cannot auto-publish related $rel_disp_name "
                                . '"[_1]" because it is not on a publish desk.',
                                $rel->get_name,
                            ];
                            next;
                        }
                    }

                    unless (chk_authz($rel, PUBLISH, 1)) {
                        # Permission denied!
                        my $rel_disp_name = lc get_disp_name($rel->key_name);
                        push @messages,[
                            'You do not have permission to auto-publish '
                            . qq{$rel_disp_name "[_1]"},
                            $rel->get_name,
                        ];
                        next;
                    }

                    # Push onto the appropriate list
                    if ($relkey eq 'story') {
                        push @rel_story, $relvid;
                        push @sids,      $relvid if $pub_ids->{$vid};
                        push @stories,   $rel; # recurse through related stories
                    } else {
                        push @rel_media, $relvid;
                        push @mids,      $relvid if $pub_ids->{$vid};
                    }
                }

                # Publish all aliases, too.
                push @$ids,  map { $_->get_version_id } $doc->list({
                    alias_id          => $doc->get_id,
                    publish_status    => 1,
                    published_version => 1,
                });
            }

            # We've been processed now so make sure we aren't done again
            $seen{"$key$id"}++;
        }

    }

    # By this point we now know if we're going to fail this publish
    # if we set the fail behaviour to fail rather than warn
    if (PUBLISH_RELATED_ASSETS && @messages) {
        if (PUBLISH_RELATED_FAIL_BEHAVIOR eq 'fail') {
            add_msg(@$_) for @messages;
            my $msg = 'Publish aborted due to errors above. Please fix the '
                . ' above problems and try again.';
            throw_error error    => $msg,
                        maketext => $msg;
        } else {
            # we are set to warn, should we add a further warning to the msg ?
            add_msg(@$_) for @messages;
            add_msg('Some of the related assets were not published.');
        }
    } else {
        add_msg(@$_) for @messages;
    }

    # For publishing from a desk, I added two new 'publish'
    # state data: 'rel_story', 'rel_media'. This is to be
    # able to distinguish between related assets and the
    # original stories to be published.
    set_state_data('publish', {
        story => \@sids,
        media => \@mids,
        story_pub => $story_pub,
        media_pub => $media_pub,
        (@rel_story ? (rel_story => \@rel_story) : ()),
        (@rel_media ? (rel_media => \@rel_media) : ())
    });

    if (%$story_pub or %$media_pub) {
        # Instant publish!
        my $pub = Bric::App::Callback::Publish->new(
            cb_request   => $self->cb_request,
            pkg_key      => 'publish',
            apache_req   => $self->apache_req,
            params       => { instant => 1,
                              pub_date => strfdate(),
                          },
        );
        $pub->publish();
    } else {
        $self->set_redirect('/workflow/profile/publish');
    }
}

sub deploy : Callback {
    my $self = shift;
    my $widget = $self->class_key;
    if (my $a_ids = $self->params->{"$widget|template_pub_ids"}) {
        my $b = Bric::Util::Burner->new;

        $a_ids = ref $a_ids ? $a_ids : [$a_ids];

        if (my $count = @$a_ids) {
            for my $fa (Bric::Biz::Asset::Template->list({
                version_id => ANY(@$a_ids)
            })) {
                my $action = $fa->get_deploy_status ? 'template_redeploy'
                    : 'template_deploy';
                $b->deploy($fa);
                $fa->set_deploy_date(strfdate());
                $fa->set_deploy_status(1);
                $fa->set_published_version($fa->get_current_version);
                $fa->save;
                log_event($action, $fa);

                # Get the current desk and remove the asset from it.
                my $d = $fa->get_current_desk;
                $d->remove_asset($fa);
                $d->save;

                # Clear the workflow ID.
                $fa->set_workflow_id(undef);
                $fa->save;
                log_event("template_rem_workflow", $fa);
                add_msg('Template "[_1]" deployed.', $fa->get_uri)
                    if $count == 1;
            }
            # Sum it up for them
            if ($count > 1) {
                add_msg("[quant,_1,template] deployed.", $count);
            }
        }
    }

    # If there are stories or media to be published, publish them!
    if ($self->params->{"$widget|story_pub_ids"}
          || $self->params->{"$widget|media_pub_ids"}) {
        $self->publish;
    }

}

sub clone : Callback {
    my $self = shift;
    my $aid = $self->value;
    my $param = $self->params;

    # Lookup the story and log that it has been cloned.
    my $story = Bric::Biz::Asset::Business::Story->lookup({ id => $aid });
    log_event('story_clone', $story);

    # Look it up again to avoid the event above being logged on the clone
    # instead of the original story.
    $story = Bric::Biz::Asset::Business::Story->lookup({ id => $aid });

    # Get the current desk.
    my $desk = $story->get_current_desk;

    # Clone the story.
    $story->clone;

    # Merge changes into story
    $self->_merge_properties($story) or return;

    # Save changes
    $story->save;

    # Put the cloned story on the desk.
    $desk->accept({ asset => $story });
    $desk->save;

    # Log events and redirect.
    my $wf = $story->get_workflow_object;
    log_event('story_clone_create', $story);
    log_event('story_add_workflow', $story, { Workflow => $wf->get_name });
    log_event('story_moved', $story, { Desk => $desk->get_name });
    log_event('story_checkout', $story);
    $self->set_redirect('/workflow/profile/story/' . $story->get_id
                        . '/?checkout=1');
}

# This is quite similar to Workspace::delete
sub delete : Callback {
    my $self = shift;
    my $burn = Bric::Util::Burner->new;

    # Deleting assets.
    foreach my $key (@$keys) {
        foreach my $aid (@{ mk_aref($self->params->{"${key}_delete_ids"}) }) {
            my $a = $pkgs->{$key}->lookup({ id => $aid });
            if (chk_authz($a, EDIT, 1)) {
                my $d = $a->get_current_desk;
                $d->remove_asset($a);
                $d->save;
                log_event("${key}_rem_workflow", $a);
                $a->set_workflow_id(undef);
                $a->deactivate;
                $a->save;

                if ($key eq 'template') {
                    $burn->undeploy($a);
                    my $sb = Bric::Util::Burner->new({user_id => get_user_id()});
                    $sb->undeploy($a);
                }
                log_event("${key}_deact", $a);
            } else {
                add_msg('Permission to delete "[_1]" denied.', $a->get_name);
            }
        }
    }
}


### PRIVATE ###

sub _merge_properties {
    my ($self, $story) = @_;
    my $param = $self->params;
    my $widget = $self->class_key;

    my $err = 0;

    # Title
    $story->set_title($param->{title});

    # Slug - account for sluglessness
    $param->{slug} = '' unless exists($param->{slug}) && defined($param->{slug});
    if (ALLOW_SLUGLESS_NONFIXED || $story->is_fixed || $param->{slug} =~ /\S/) {
        $story->set_slug($param->{slug});
    } else {
        add_msg('Slug required for non-fixed (non-cover) story type.');
        $err++;
    }

    # Category
    my $cid = $param->{"$widget|new_category_id"};
    unless (defined $cid && $cid ne '') {
        add_msg('Please select a primary category.');
        $err++;
    } else {
        # Delete all the current categories first,
        # then add the new category and make it primary.
        # It's a little more complicated to avoid
        # deleting the new category if it's already there.
        my %todelete_cats = map { $_->get_id => $_ } $story->get_categories;

        my $was_there = delete($todelete_cats{$cid}) || 0;
        $story->add_categories([$cid]) unless $was_there;
        if (my $seconds = $param->{"$widget|secondary_category_id"}) {
            # Add secondary categories.
            my @add;
            for my $catid (@{mk_aref($seconds)}) {
                # Leave existing seconary categories and the primary category.
                next if delete $todelete_cats{$catid} || $catid == $cid;
                push @add, Bric::Biz::Category->lookup({ id => $catid });
            }
            $story->add_categories(\@add) if @add;
        }
        $story->delete_categories([ values(%todelete_cats) ]) if %todelete_cats;
        $story->set_primary_category($cid);
    }

    # Cover date
    if ($param->{'cover_date-partial'}) {
        add_msg('Cover Date incomplete.');
        $err++;
    } else {
        $story->set_cover_date($param->{cover_date});
    }

    # Output Channel
    my $ocid = $param->{"$widget|new_oc_id"};
    unless (defined $ocid && $ocid ne '') {
        add_msg('Please select a primary output channel.');
        $err++;
    } else {
        # Delete all the current output channels first,
        # then add the new output channel and make it primary.
        # It's a little more complicated to avoid
        # deleting the new output channel if it's already there.

        # Note the current output channels.
        my %todelete_ocs =  map { $_->get_id => $_ }
          $story->get_output_channels;

        unless (delete $todelete_ocs{$ocid}) {
            $story->add_output_channels(
                Bric::Biz::OutputChannel->lookup({ id => $ocid })
            );
        }
        $story->set_primary_oc_id($ocid);

        # Add any secondary output channels.
        my (%seen, @add);
        if (my $ocs2 = $param->{"$widget|secondary_oc_id"}) {
            for my $oc2id (@{mk_aref($ocs2)}) {
                # Leave existing seconary OCs and the primary OC.
                next if delete $todelete_ocs{$oc2id} || $oc2id == $ocid;
                push @add, Bric::Biz::OutputChannel->lookup({ id => $oc2id })
                  unless $seen{$oc2id}++;
            }
        }

        # Add any secondary output channels from an associated OC group.
        if (my $grp_id = $param->{"$widget|oc_grp_id"}) {
            my %allowed = map { $_->get_id => 1 }
              $story->get_element_type->get_output_channels;
            push @add,
              map { $_->[1] }
              grep {
                  $allowed{$_->[0]}
                  && !$seen{$_->[0]}++
                  && ! delete $todelete_ocs{$_->[0]}
              }
              map { [ $_->get_id => $_ ] }
              Bric::Biz::OutputChannel->list({ grp_id => $grp_id });
        }

        # Now add them.
        $story->add_output_channels(@add) if @add;

        # Delete any leftovers.
        $story->del_output_channels(values %todelete_ocs) if %todelete_ocs;
    }

    return 1 unless $err;
    return;
}

1;
