package FixMyStreet::App::Controller::Admin;
use Moose;
use namespace::autoclean;

BEGIN { extends 'Catalyst::Controller'; }

use POSIX qw(strcoll);
use List::Util 'first';
use FixMyStreet::SMS;

=head1 NAME

FixMyStreet::App::Controller::Admin - Controller for main admin pages

=head1 DESCRIPTION

Admin pages

=head1 METHODS

=cut

sub auto : Private {
    my ( $self, $c ) = @_;

    $c->uri_disposition('relative');

    # User must be logged in to see cobrand, and meet whatever checks the
    # cobrand specifies. Default cobrand just requires superuser flag to be set.
    unless ( $c->user_exists ) {
        $c->detach( '/auth/redirect' );
    }
    unless ( $c->cobrand->admin_allow_user($c->user) ) {
        $c->detach('/page_error_403_access_denied', []);
    }

    if ( $c->cobrand->moniker eq 'zurich' ) {
        $c->cobrand->admin_type();
    }

    $c->forward('check_page_allowed');
}

=head2 summary

Redirect to index page. There to make the allowed pages stuff neater

=cut

sub summary : Path( 'summary' ) : Args(0) {
    my ( $self, $c ) = @_;
    $c->go( 'index' );
}

=head2 index

Displays some summary information for the requests.

=cut

sub index : Path : Args(0) {
    my ( $self, $c ) = @_;

    if ($c->cobrand->moniker eq 'zurich') {
        if ($c->stash->{admin_type} eq 'super') {
            $c->forward('/admin/stats/gather');
            return 1;
        } else {
            return $c->cobrand->admin();
        }
    }

    $c->stash->{edit_body_contacts} = 1
        if grep { $_ eq 'body' } keys %{$c->stash->{allowed_pages}};

    my @unsent = $c->cobrand->problems->search( {
        send_state => ['unprocessed', 'acknowledged'],
        'me.state' => [ FixMyStreet::DB::Result::Problem::open_states() ],
        bodies_str => { '!=', undef },
        # Ignore very recent ones that probably just haven't been sent yet
        confirmed => { '<', \"current_timestamp - '5 minutes'::interval" },
    },
    {
        '+columns' => ['user.email'],
        prefetch => 'contact',
        join => 'user',
        order_by => 'confirmed',
    } )->all;
    $c->stash->{unsent_reports} = \@unsent;

    $c->forward('fetch_all_bodies');

    return 1;
}

=head2 config_page

This admin page displays the overall configuration for the site.

=cut

sub config_page : Path( 'config' ) : Args(0) {
    my ($self, $c) = @_;
    my $dir = FixMyStreet->path_to();
    my $git_version = `cd $dir && git describe --tags`;
    chomp $git_version;
    $c->stash(
        git_version => $git_version,
    );
}

=head2 config_page_cobrand

This displays the COBRAND_FEATURES configuration for the site, grouped by
feature, and provides links to the configuration for each cobrand.

=cut

sub config_page_cobrand : Path( 'config/cobrand_features' ) : Args(0) {
    my ($self, $c) = @_;
    $c->detach('/page_error_403_access_denied', []) unless $c->user->is_superuser;
}

=head2 config_page_cobrand_one

This displays the COBRAND_FEATURES configuration for a particular cobrand given
in the URL.

=cut

sub config_page_cobrand_one : Path( 'config/cobrand_features' ) : Args(1) {
    my ($self, $c, $cobrand) = @_;

    $c->detach('/page_error_403_access_denied', []) unless $c->user->is_superuser;

    $c->stash->{cob} = $cobrand;
    my $features = FixMyStreet->config('COBRAND_FEATURES');
    return unless $features && ref $features eq 'HASH';

    my $config = $c->stash->{config} = {};
    my $fallback = $c->stash->{fallback} = {};
    foreach my $feature (sort keys %$features) {
        next unless $features->{$feature} && ref $features->{$feature} eq 'HASH';
        if (defined $features->{$feature}{$cobrand}) {
            $config->{$feature} = $features->{$feature}{$cobrand};
        } elsif (defined $features->{$feature}{_fallback}) {
            $fallback->{$feature} = $features->{$feature}{_fallback};
        }
    }
}

sub timeline : Path( 'timeline' ) : Args(0) {
    my ($self, $c) = @_;

    my %time;

    my $probs = $c->cobrand->problems->timeline;

    foreach ($probs->all) {
        push @{$time{$_->created->epoch}}, { type => 'problemCreated', date => $_->created, obj => $_ };
        push @{$time{$_->confirmed->epoch}}, { type => 'problemConfirmed', date => $_->confirmed, obj => $_ } if $_->confirmed;
        push @{$time{$_->whensent->epoch}}, { type => 'problemSent', date => $_->whensent, obj => $_ } if $_->whensent;
    }

    my $questionnaires = $c->model('DB::Questionnaire')->timeline( $c->cobrand->restriction );

    foreach ($questionnaires->all) {
        push @{$time{$_->whensent->epoch}}, { type => 'quesSent', date => $_->whensent, obj => $_ };
        push @{$time{$_->whenanswered->epoch}}, { type => 'quesAnswered', date => $_->whenanswered, obj => $_ } if $_->whenanswered;
    }

    my $updates = $c->cobrand->updates->timeline;

    foreach ($updates->all) {
        push @{$time{$_->created->epoch}}, { type => 'update', date => $_->created, obj => $_} ;
    }

    my $alerts = $c->model('DB::Alert')->timeline_created( $c->cobrand->restriction );

    foreach ($alerts->all) {
        push @{$time{$_->whensubscribed->epoch}}, { type => 'alertSub', date => $_->whensubscribed, obj => $_ };
    }

    $alerts = $c->model('DB::Alert')->timeline_disabled( $c->cobrand->restriction );

    foreach ($alerts->all) {
        push @{$time{$_->whendisabled->epoch}}, { type => 'alertDel', date => $_->whendisabled, obj => $_ };
    }

    $c->stash->{time} = \%time;

    return 1;
}

sub fetch_contacts : Private {
    my ( $self, $c ) = @_;

    my $contacts = $c->stash->{body}->contacts->order_by('category');
    $c->stash->{contacts} = $contacts;
    $c->stash->{live_contacts} = $contacts->not_deleted_admin;
    $c->stash->{any_not_confirmed} = $contacts->search({ state => 'unconfirmed' })->count;

    if ( $c->get_param('text') && $c->get_param('text') eq '1' ) {
        $c->stash->{template} = 'admin/council_contacts.txt';
        $c->res->content_type('text/plain; charset=utf-8');
        return 1;
    }

    return 1;
}

sub stash_contacts_for_template : Private {
    my ( $self, $c, $contacts ) = @_;

    my %active_contacts = map { $_ => 1 } @$contacts;
    my @live_contacts = $c->stash->{live_contacts}->all;
    my @all_contacts = map { {
        id => $_->id,
        category => $_->category,
        active => $active_contacts{$_->id},
        group => $_->groups,
    } } @live_contacts;

    $c->stash->{contacts} = \@all_contacts;
}

sub fetch_languages : Private {
    my ( $self, $c ) = @_;

    my $lang_map = {};
    foreach my $lang (@{$c->cobrand->languages}) {
        my ($id, $name, $code) = split(',', $lang);
        $lang_map->{$id} = { name => $name, code => $code };
    }

    $c->stash->{languages} = $lang_map;

    return 1;
}

sub update_user : Private {
    my ($self, $c, $object) = @_;
    my $parsed = FixMyStreet::SMS->parse_username($c->get_param('username'));
    if ($parsed->{email} || ($parsed->{phone} && $parsed->{may_be_mobile})) {
        my $user = $c->model('DB::User')->find_or_create({ $parsed->{type} => $parsed->{username} });
        if ($user->id && $user->id != $object->user->id) {
            $object->user( $user );
            return 1;
        }
    }
    return 0;
}

sub update_edit : Path('update_edit') : Args(1) {
    my ( $self, $c, $id ) = @_;

    my $update = $c->cobrand->updates->search({ 'me.id' => $id })->first;

    $c->detach( '/page_error_404_not_found', [] )
      unless $update;

    $c->forward('/auth/get_csrf_token');

    $c->stash->{update} = $update;

    if (my $rotate_photo_param = $c->forward('_get_rotate_photo_param')) {
        $c->forward('rotate_photo', [ $update, @$rotate_photo_param ]);
        return 1;
    }

    $c->forward('check_username_for_abuse', [ $update->user ] );

    if ( $c->get_param('resend') ) {
        $c->forward('/auth/check_csrf_token');

        $update->send_state('unprocessed');
        $update->whensent(undef);
        $update->update();
        $c->forward( 'log_edit',
            [ $update->id, 'update', 'resend' ] );

        $c->stash->{status_message} = _('Update will now be resent.');
    }
    elsif ( $c->get_param('mark_sent') ) {
        $c->forward('/auth/check_csrf_token');

        $update->send_state('sent');
        $update->whensent( \'current_timestamp' );
        $update->update();
        $c->forward( 'log_edit',
            [ $update->id, 'update', 'marked sent' ] );

        $c->stash->{status_message} = _('Update has been marked as sent.');
    }
    elsif ( $c->get_param('mark_skip') ) {
        $c->forward('/auth/check_csrf_token');

        $update->send_state('skipped');
        $update->update();
        $c->forward( 'log_edit',
            [ $update->id, 'update', 'mark skipped' ] );

        $c->stash->{status_message} = _('Update has been marked to be skipped from sending.');
    }
    elsif ( $c->get_param('submit') ) {
        $c->forward('/auth/check_csrf_token');

        my $old_state = $update->state;
        my $new_state = $c->get_param('state');

        my $edited = 0;

        # $update->name can be null which makes me unhappy
        my $name = $update->name || '';

        if ( $c->get_param('name') ne $name
          || $c->get_param('anonymous') ne $update->anonymous
          || $c->get_param('text') ne $update->text ) {
              $edited = 1;
        }

        my $remove_photo_param = $c->forward('_get_remove_photo_param');
        if ($remove_photo_param) {
            $c->forward('remove_photo', [$update, $remove_photo_param]);
        }

        $c->stash->{status_message} = _('Updated!');

        # Must call update->hide while it's not hidden (so is_latest works)
        if ($new_state eq 'hidden') {
            my $outcome = $update->hide;
            $c->stash->{status_message} .= _('Problem marked as open.')
                if $outcome->{reopened};
        }

        $update->name( $c->get_param('name') || '' );
        $update->text( $c->get_param('text') );
        $update->anonymous( $c->get_param('anonymous') );
        $update->state( $new_state );

        $edited = 1 if $c->forward('update_user', [ $update ]);

        if ( $new_state eq 'confirmed' and $old_state eq 'unconfirmed' ) {
            $update->confirmed( \'current_timestamp' );
            if ( $update->problem_state && $update->created > $update->problem->lastupdate ) {
                $update->problem->state( $update->problem_state );
                $update->problem->lastupdate( \'current_timestamp' );
                $update->problem->update;
            }
        }

        $update->update;

        if ( $new_state ne $old_state ) {
            $c->forward( 'log_edit',
                [ $update->id, 'update', 'state_change' ] );
        }

        if ($edited) {
            $c->forward( 'log_edit', [ $update->id, 'update', 'edit' ] );
        }

    }
    return 1;
}

sub add_flags : Private {
    my ( $self, $c, $search ) = @_;

    return unless $c->user->is_superuser;

    my $users = $c->stash->{users};
    my %email2user = map { $_->email => $_ } grep { $_->email } @$users;
    my %phone2user = map { $_->phone => $_ } grep { $_->phone } @$users;
    my %username2user = (%email2user, %phone2user);
    my $usernames = $c->model('DB::Abuse')->search($search);

    foreach my $username (map { $_->email } $usernames->all) {
        # Slight abuse of the boolean flagged value
        if ($username2user{$username}) {
            $username2user{$username}->flagged( 2 );
        } else {
            push @{$c->stash->{users}}, { email => $username, flagged => 2 };
        }
    }
}

sub flagged : Path('flagged') : Args(0) {
    my ( $self, $c ) = @_;

    my $problems = $c->cobrand->problems->search(
        { 'me.flagged' => 1 },
        {
            '+columns' => ['user.email'],
            join => 'user',
            prefetch => 'contact',
        }
    );

    # pass in as array ref as using same template as search_reports
    # which has to use an array ref for sql quoting reasons
    $c->stash->{problems} = [ $problems->all ];

    my @users = $c->cobrand->users->search( { flagged => 1 } )->all;
    $c->stash->{users} = [ @users ];

    $c->forward('add_flags', [ {} ]);
    return 1;
}

=head2 set_allowed_pages

Sets up the allowed_pages stash entry for checking if the current page is
available in the current cobrand.

=cut

sub set_allowed_pages : Private {
    my ( $self, $c ) = @_;

    my $pages = $c->cobrand->admin_pages;

    my @allowed_links = sort {$pages->{$a}[1] <=> $pages->{$b}[1]}  grep {$pages->{$_}->[0] } keys %$pages;

    $c->stash->{allowed_pages} = $pages;
    $c->stash->{allowed_links} = \@allowed_links;

    return 1;
}

sub get_user : Private {
    my ( $self, $c ) = @_;

    my $user = ($c->user && $c->user->name);
    $user ||= $c->req->remote_user();
    $user ||= '';

    return $user;
}

=item log_edit

    $c->forward( 'log_edit', [ $object_id, $object_type, $action_performed ] );

Adds an entry into the admin_log table using the current user.

=cut

sub log_edit : Private {
    my ( $self, $c, $id, $object_type, $action, $time_spent ) = @_;

    $time_spent //= 0;
    $time_spent = 0 if $time_spent < 0;

    my $user_object = do {
        my $auth_user = $c->user;
        $auth_user ? $auth_user->get_object : undef;
    };

    $c->model('DB::AdminLog')->create(
        {
            admin_user => $c->forward('get_user'),
            $user_object ? ( user => $user_object ) : (), # as (rel => undef) doesn't work
            object_type => $object_type,
            action => $action,
            object_id => $id,
            time_spent => $time_spent,
        }
    )->insert();
}

=head2 check_username_for_abuse

    $c->forward('check_username_for_abuse', [ $user ] );

Checks if $user is in the abuse table and sets username_in_abuse accordingly.

=cut

sub check_username_for_abuse : Private {
    my ( $self, $c, $user ) = @_;

    my $is_abuse = $c->model('DB::Abuse')->find({ email => [ $user->phone, $user->email ] });

    $c->stash->{username_in_abuse} = 1 if $is_abuse;
}

=head2 rotate_photo

Rotate a photo 90 degrees left or right

=cut

# returns index of photo to rotate, if any
sub _get_rotate_photo_param : Private {
    my ($self, $c) = @_;
    my $key = first { /^rotate_photo/ } keys %{ $c->req->params } or return;
    my ($index) = $key =~ /(\d+)$/;
    my $direction = $c->get_param($key);
    return [ $index || 0, $direction ];
}

sub rotate_photo : Private {
    my ( $self, $c, $object, $index, $direction ) = @_;

    return unless $direction eq _('Rotate Left') or $direction eq _('Rotate Right');

    my $fileid = $object->get_photoset->rotate_image(
        $index,
        $direction eq _('Rotate Left') ? -90 : 90
    ) or return;

    $object->update({ photo => $fileid });

    return 1;
}

=head2 remove_photo

Remove a photo from a report

=cut

# Returns index of photo(s) to remove, if any
sub _get_remove_photo_param : Private {
    my ($self, $c) = @_;

    return 'ALL' if $c->get_param('remove_photo');

    my @keys = map { /(\d+)$/ } grep { /^remove_photo_/ } keys %{ $c->req->params } or return;
    return \@keys;
}

sub remove_photo : Private {
    my ($self, $c, $object, $keys) = @_;
    if ($keys eq 'ALL') {
        $object->get_photoset->delete_cached;
        $object->photo(undef);
    } else {
        my $fileids = $object->get_photoset->remove_images($keys);
        $object->photo($fileids);
    }
    return 1;
}

=head2 check_page_allowed

Checks if the current catalyst action is in the list of allowed pages and
if not then redirects to 404 error page.

=cut

sub check_page_allowed : Private {
    my ( $self, $c ) = @_;

    $c->forward('set_allowed_pages');

    (my $page = $c->req->path) =~ s#admin/?##;
    $page =~ s#/.*##;

    $page ||= 'summary';

    if ( !grep { $_ eq $page } keys %{ $c->stash->{allowed_pages} } ) {
        $c->detach( '/page_error_404_not_found', [] );
    }

    return 1;
}

sub fetch_all_bodies : Private {
    my ($self, $c ) = @_;

    my @bodies = $c->cobrand->call_hook('admin_fetch_all_bodies');
    if (!@bodies) {
        my $bodies = $c->model('DB::Body')->search(undef, {
            columns => [ "id", "name", "deleted", "parent" ],
        })->with_parent_name;
        $bodies = $bodies->with_defect_type_count if $c->stash->{with_defect_type_count};
        @bodies = $bodies->translated->all_sorted;
    }

    $c->stash->{bodies} = \@bodies;

    return 1;
}

sub fetch_body_areas : Private {
    my ($self, $c, $body ) = @_;

    my $children = $body->area_children;
    unless ($children) {
        # Body doesn't have any areas defined.
        delete $c->stash->{areas};
        delete $c->stash->{fetched_areas_body_id};
        return;
    }

    $c->stash->{areas} = [ sort { strcoll($a->{name}, $b->{name}) } values %$children ];
    # Keep track of the areas we've fetched to prevent a duplicate fetch later on
    $c->stash->{fetched_areas_body_id} = $body->id;
}

sub update_extra_fields : Private {
    my ($self, $c, $object) = @_;
    my @indices = grep { /^metadata\[\d+\]\.code/ } keys %{ $c->req->params };
    @indices = sort map { /(\d+)/ } @indices;
    my @extra_fields;
    foreach my $i (@indices) {
        my $meta = {};
        $meta->{code} = $c->get_param("metadata[$i].code");
        next unless $meta->{code};

        $meta->{order} = int $c->get_param("metadata[$i].order");
        $meta->{protected} = $c->get_param("metadata[$i].protected") ? 'true' : 'false';

        my $behaviour = $c->get_param("metadata[$i].behaviour") || 'question';
        if ($behaviour eq 'question') {
            $meta->{required} = $c->get_param("metadata[$i].required") ? 'true' : 'false';
            $meta->{variable} = 'true';
            my $desc = $c->get_param("metadata[$i].description");
            $meta->{description} = FixMyStreet::Template::sanitize($desc, 1);
            $meta->{datatype} = $c->get_param("metadata[$i].datatype");

            if ( $meta->{datatype} eq "singlevaluelist" || $meta->{datatype} eq "multivaluelist" ) {
                $meta->{values} = [];
                my $re = qr{^metadata\[$i\]\.values\[\d+\]\.key};
                my @vindices = grep { /$re/ } keys %{ $c->req->params };
                @vindices = sort { $a <=> $b } map { /values\[(\d+)\]/ } @vindices;
                foreach my $j (@vindices) {
                    my $name = $c->get_param("metadata[$i].values[$j].name");
                    my $key = $c->get_param("metadata[$i].values[$j].key");
                    my $disable = $c->get_param("metadata[$i].values[$j].disable");
                    my $disable_message = $c->get_param("metadata[$i].values[$j].disable_message");
                    push(@{$meta->{values}}, {
                        name => $name,
                        key => $key,
                        $disable ? (disable => 1, disable_message => $disable_message) : (),
                    }) if $name;
                }
            }
        } elsif ($behaviour eq 'notice') {
            $meta->{variable} = 'false';
            my $desc = $c->get_param("metadata[$i].description");
            $meta->{description} = FixMyStreet::Template::sanitize($desc, 1);
            $meta->{disable_form} = $c->get_param("metadata[$i].disable_form") ? 'true' : 'false';
        } elsif ($behaviour eq 'hidden') {
            $meta->{automated} = 'hidden_field';
        } elsif ($behaviour eq 'server') {
            $meta->{automated} = 'server_set';
        }

        push @extra_fields, $meta;
    }
    @extra_fields = sort { $a->{order} <=> $b->{order} } @extra_fields;
    $object->set_extra_fields(@extra_fields);
}

sub body_specific_page : Private {
    my ( $self, $c, $load_all_action, $view_action ) = @_;

    my $user = $c->user;

    if ($user->is_superuser) {
        $c->forward($load_all_action);
    } elsif ( $user->from_body ) {
        my $body_id = $user->from_body->id;
        $body_id = $c->cobrand->call_hook(permission_body_override => [ $body_id ]) || [ $body_id ];
        if (@$body_id > 1) {
            $c->forward($load_all_action);
            my %bodies = map { $_ => 1 } @$body_id;
            $c->stash->{bodies} = [ grep { $bodies{$_->id} } @{$c->stash->{bodies}} ];
        } else {
            $c->res->redirect( $c->uri_for_action($view_action, $body_id) );
        }
    } else {
        $c->detach( '/page_error_404_not_found' );
    }
}

sub trim {
    my $self = shift;
    my $e = shift;
    $e =~ s/^\s+//;
    $e =~ s/\s+$//;
    return $e;
}

=head1 AUTHOR

Struan Donald

=head1 LICENSE

This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.

=cut

__PACKAGE__->meta->make_immutable;

1;
