package FixMyStreet::App::Controller::Report::Update;

use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }

use utf8;
use Path::Class;
use Utils;

=head1 NAME

FixMyStreet::App::Controller::Report::Update

=head1 DESCRIPTION

Creates an update to a report

=cut

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

    $c->forward('initialize_update');
    $c->forward('load_problem');
    $c->forward('check_form_submitted')
      or $c->go( '/report/display', [ $c->stash->{problem}->id ], [] );

    $c->forward('/auth/check_csrf_token');
    $c->forward('process_update');
    $c->forward('process_user');
    $c->forward('/photo/process_photo');
    $c->forward('check_for_errors')
      or $c->go( '/report/display', [ $c->stash->{problem}->id ], [] );

    $c->forward('save_update');
    $c->forward('redirect_or_confirm_creation');
}

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

    my $display_questionnaire = 0;

    my $update = $c->stash->{update};
    my $problem = $c->stash->{problem} || $update->problem;

    # we may need this if we display the questionnaire
    my $old_state = $problem->state;

    if ( $update->mark_fixed ) {
        $problem->state('fixed - user');

        if ( $update->user->id == $problem->user->id ) {
            $problem->send_questionnaire(0);

            if ( $c->cobrand->ask_ever_reported
                && !$problem->user->answered_ever_reported )
            {
                $display_questionnaire = 1;
            }
        }
    }

    if ( $update->problem_state ) {
        $problem->state( $update->problem_state );
    }

    if ( $update->mark_open && $update->user->id == $problem->user->id ) {
        $problem->state('confirmed');
    }

    if ( $c->cobrand->can_support_problems && $c->user && $c->user->from_body && $c->get_param('external_source_id') ) {
        $problem->interest_count( \'interest_count + 1' );
    }

    $problem->lastupdate( \'current_timestamp' );
    $problem->update;

    $c->stash->{problem_id} = $problem->id;

    if ($display_questionnaire) {
        $c->flash->{old_state} = $old_state;
        $c->detach('/questionnaire/creator_fixed');
    }

    return 1;
}

=head2 process_user

Load user from the database or prepare a new one.

=cut

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

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

    # Extract all the params to a hash to make them easier to work with
    my %params = map { $_ => $c->get_param($_) }
      ( 'name', 'password_register', 'fms_extra_title' );

    if ($c->user_exists) {
        $params{username} = $c->get_param('username');
    } elsif ($c->get_param('submit_sign_in') || $c->get_param('password_sign_in')) {
        $params{username} = $c->get_param('username');
    } else {
        $params{username} = $c->get_param('username_register');
    }
    $params{username} ||= '';
    $c->cobrand->call_hook('disable_login_for_email', $params{username}) unless $c->get_param('oauth_need_email');

    my $anon_button = $c->cobrand->allow_anonymous_updates eq 'button' && $c->get_param('report_anonymously');
    if ($anon_button) {
        my $anon_details = $c->cobrand->anonymous_account;
        my $user = $c->model('DB::User')->find_or_new({ email => $anon_details->{email} });
        $user->name($anon_details->{name});
        $update->user($user);
        $update->name($user->name);
        $c->stash->{contributing_as_anonymous_user} = 1;
        return 1;
    }

    # Extra block to use 'last'
    if ( $c->user_exists ) { {
        my $user = $c->user->obj;

        if ($c->stash->{contributing_as_another_user} = $user->contributing_as('another_user', $c, $c->stash->{problem}->bodies_str_ids)) {
            # Act as if not logged in (and it will be auto-confirmed later on)
            last;
        }

        $user->name( Utils::trim_text( $params{name} ) ) if $params{name};
        $update->name($user->name);
        my $title = Utils::trim_text( $params{fms_extra_title} );
        $user->title( $title ) if $title;
        $update->user( $user );

        # Just in case, make sure the user will have a name
        if ($c->stash->{contributing_as_body} or $c->stash->{contributing_as_anonymous_user}) {
            my $name = $user->moderating_user_name;
            $update->name($name);
            $user->name($name) unless $user->name;
        }

        return 1;
    } }

    my $parsed = FixMyStreet::SMS->parse_username($params{username});
    my $type = $parsed->{type} || 'email';
    $type = 'email' unless $c->cobrand->sms_authentication || $c->stash->{contributing_as_another_user};
    $update->user( $c->model('DB::User')->find_or_new( { $type => $parsed->{username} } ) );

    $c->stash->{phone_may_be_mobile} = $type eq 'phone' && $parsed->{may_be_mobile};

    # The user is trying to sign in. We only care about username from the params.
    if ( $c->get_param('submit_sign_in') || $c->get_param('password_sign_in') ) {
        $c->stash->{tfa_data} = {
            detach_to => '/report/update/report_update',
            login_success => 1,
            oauth_update => { $update->get_inflated_columns }
        };
        unless ( $c->forward( '/auth/sign_in', [ $params{username} ] ) ) {
            $c->stash->{field_errors}->{password} = _('There was a problem with your login information. If you cannot remember your password, or do not have one, please fill in the ‘No’ section of the form.');
            $c->stash->{field_errors}->{password_js} = _('There was a problem with your login information. If you cannot remember your password, or do not have one, please select ‘Fill in your details manually’.');
            return 1;
        }
        my $user = $c->user->obj;
        $update->user( $user );
        $update->name( $user->name );
        $c->stash->{login_success} = 1;
        return 1;
    }

    $update->user->name( Utils::trim_text( $params{name} ) )
        if $params{name};
    $update->name($update->user->name);
    $update->user->title( Utils::trim_text( $params{fms_extra_title} ) )
        if $params{fms_extra_title};

    if ($params{password_register}) {
        $c->forward('/auth/test_password', [ $params{password_register} ]);
        $update->user->password($params{password_register});
    }

    return 1;
}

=head2 oauth_callback

Called when we successfully login via OAuth. Stores the token so we can look up
what we have so far.

=cut

sub oauth_callback : Private {
    my ( $self, $c, $token_code ) = @_;
    my $auth_token = $c->forward('/tokens/load_auth_token',
        [ $token_code, 'update/social' ]);
    $c->stash->{oauth_update} = $auth_token->data;
    $c->detach('report_update');
}

=head2 initialize_update

Create an initial update object, either empty or from stored OAuth data.

=cut

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

    my $update;
    if ($c->stash->{oauth_update}) {
        $update = $c->model("DB::Comment")->new($c->stash->{oauth_update});
    }

    if ($update) {
        $c->stash->{upload_fileid} = $update->get_photoset->data;
    } else {
        $update = $c->model('DB::Comment')->new({
            state => 'unconfirmed',
            cobrand => $c->cobrand->moniker,
            cobrand_data => '',
            lang => $c->stash->{lang_code},
        });
    }

    if ( $c->get_param('first_name') && $c->get_param('last_name') ) {
        my $first_name = $c->get_param('first_name');
        my $last_name = $c->get_param('last_name');
        $c->set_param('name', sprintf( '%s %s', $first_name, $last_name ));

        $c->stash->{first_name} = $first_name;
        $c->stash->{last_name} = $last_name;
    }

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

=head2 load_problem

Our update could be prefilled, or we could be submitting a form containing an
ID. Look up the relevant report either way.

=cut

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

    my $update = $c->stash->{update};
    # Problem ID could come from existing update in token, or from query parameter
    my $problem_id = $update->problem_id || $c->get_param('id');
    $c->forward( '/report/load_problem_or_display_error', [ $problem_id ] );
    $update->problem_id($c->stash->{problem}->id);
}

=head2 check_form_submitted

This makes sure we only proceed to processing if we've had the form submitted
(we may have come here via an OAuth login, for example).

=cut

sub check_form_submitted : Private {
    my ( $self, $c ) = @_;
    return if $c->cobrand->updates_disallowed($c->stash->{problem});
    return $c->get_param('submit_update') || '';
}

=head2 process_update

Take the submitted params and updates our update item. Does not save
anything to the database.

=cut

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

    my %params =
      map { $_ => $c->get_param($_) } ( 'update', 'name', 'fixed', 'state', 'reopen' );

    $params{update} =
      Utils::cleanup_text( $params{update}, { allow_multiline => 1 } );

    my $name = Utils::trim_text( $params{name} );

    my $problem = $c->stash->{problem};
    $params{reopen} = 0 unless $c->user && $c->user->id == $problem->user->id;

    my $update = $c->stash->{update};
    $update->text($params{update});

    $update->mark_fixed($params{fixed} ? 1 : 0);
    $update->mark_open($params{reopen} ? 1 : 0);

    $c->stash->{contributing_as_body} = $c->user_exists && $c->user->contributing_as('body', $c, $problem->bodies_str_ids);
    $c->stash->{contributing_as_anonymous_user} = $c->user_exists && $c->user->contributing_as('anonymous_user', $c, $problem->bodies_str_ids);

    # This is also done in process_user, but is needed here for anonymous() just below
    my $anon_button = $c->cobrand->allow_anonymous_updates($problem->category) eq 'button' && $c->get_param('report_anonymously');
    if ($anon_button) {
        $c->stash->{contributing_as_anonymous_user} = 1;
        $c->stash->{contributing_as_body} = undef;
        $c->stash->{contributing_as_another_user} = undef;
    }


    if ($c->stash->{contributing_as_body}) {
        $update->anonymous(0);
    } elsif ($c->stash->{contributing_as_anonymous_user}) {
        $update->anonymous(1);
    } else {
        $update->anonymous($c->get_param('may_show_name') ? 0 : 1);
    }

    if ( $params{state} ) {
        $update->problem_state( $params{state} );
    } else {
        # we do this so we have a record of the state of the problem at this point
        # for use when sending updates to external parties
        if ( $update->mark_fixed ) {
            $update->problem_state( 'fixed - user' );
        } elsif ( $update->mark_open ) {
            $update->problem_state( 'confirmed' );
        # if there is not state param and neither of the above conditions apply
        # then we are not changing the state of the problem so can use the current
        # problem state
        } else {
            $update->problem_state( $problem->state );
        }
    }


    my @extra; # Next function fills this, but we don't need it here.
    # This is just so that the error checking for these extra fields runs.
    # TODO Use extra here as it is used on reports.
    my $body = (values %{$problem->bodies})[0];
    $c->cobrand->process_open311_extras( $c, $body, \@extra );

    if ( $c->get_param('fms_extra_title') ) {
        my %extras = ();
        $extras{title} = $c->get_param('fms_extra_title');
        $extras{email_alerts_requested} = $c->get_param('add_alert');
        $update->extra( \%extras );
    }

    if ( $c->stash->{ first_name } && $c->stash->{ last_name } ) {
        my $extra = $update->extra || {};
        $extra->{first_name} = $c->stash->{ first_name };
        $extra->{last_name} = $c->stash->{ last_name };
        $update->extra( $extra );
    }

    my $private_comments = $c->get_param('private_comments');
    if ( $private_comments ) {
        $update->set_extra_metadata(private_comments => $private_comments);
    }

    $c->stash->{add_alert} = $c->get_param('add_alert');

    return 1;
}


=head2 check_for_errors

Examine the user and the report for errors. If found put them on stash and
return false.

=cut

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

    # they have to be an authority user to update the state
    my $state = $c->get_param('state');
    if ( $state && $state ne $c->stash->{update}->problem->state ) {
        my $error = 0;
        $error = 1 unless $c->user && ($c->user->is_superuser || $c->user->belongs_to_body($c->stash->{update}->problem->bodies_str));
        $error = 1 unless grep { $state eq $_ } FixMyStreet::DB::Result::Problem->visible_states();
        if ( $error ) {
            $c->stash->{errors} ||= [];
            push @{ $c->stash->{errors} }, _('There was a problem with your update. Please try again.');
            return;
        }

    }

    # let the model check for errors
    $c->stash->{field_errors} ||= {};
    my %field_errors = (
        %{ $c->stash->{field_errors} },
        %{ $c->stash->{update}->user->check_for_errors },
        %{ $c->stash->{update}->check_for_errors },
    );

    # if using social login then we don't care about name and email errors
    $c->stash->{is_social_user} = $c->get_param('social_sign_in') ? 1 : 0;
    if ( $c->stash->{is_social_user} ) {
        delete $field_errors{name};
        delete $field_errors{username};
    }

    # if we're contributing as someone else then allow landline numbers
    if ( $field_errors{phone} && $c->stash->{contributing_as_another_user} && !$c->stash->{phone_may_be_mobile}) {
        delete $field_errors{username};
        delete $field_errors{phone};
    }

    if ( my $photo_error  = delete $c->stash->{photo_error} ) {
        $field_errors{photo} = $photo_error;
    }

    # Now assign the username error according to where it came from
    if ($field_errors{username}) {
        if ($c->get_param('submit_sign_in') || $c->get_param('password_sign_in')) {
            $field_errors{username_sign_in} = $field_errors{username};
        } else {
            $field_errors{username_register} = $field_errors{username};
        }
        delete $field_errors{username};
    }

    # all good if no errors
    return 1
      unless ( scalar keys %field_errors
        || $c->stash->{login_success}
        || ( $c->stash->{errors} && scalar @{ $c->stash->{errors} } ) );

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

    $c->stash->{errors} ||= [];
    #push @{ $c->stash->{errors} },
    #  _('There were problems with your update. Please see below.');

    if ( $c->cobrand->allow_anonymous_updates ) {
        my $anon_details = $c->cobrand->anonymous_account;
        my $update = $c->stash->{update};
        $update->user->email(undef) if $update->user->email eq $anon_details->{email};
        $update->name(undef) if $update->name && $update->name eq $anon_details->{name};
    }

    return;
}

# Store changes in token for when token is validated.
sub tokenize_user : Private {
    my ($self, $c, $update) = @_;
    $c->stash->{token_data} = {
        name => $update->user->name,
        password => $update->user->password,
    };
    $c->forward('/auth/set_oauth_token_data', [ $c->stash->{token_data} ])
        if $c->get_param('oauth_need_email');
}

=head2 save_update

Save the update and the user as appropriate.

=cut

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

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

    # If there was a photo add that too
    if ( my $fileid = $c->stash->{upload_fileid} ) {
        $update->photo($fileid);
    }

    if ( $update->is_from_abuser ) {
        $c->stash->{template} = 'tokens/abuse.html';
        $c->detach;
    }

    if ( $c->stash->{is_social_user} ) {
        my $token = $c->model("DB::Token")->create( {
            scope => 'update/social',
            data => { $update->get_inflated_columns },
        } );

        $c->stash->{detach_to} = '/report/update/oauth_callback';
        $c->stash->{detach_args} = [$token->token];

        $c->forward('/auth/social/handle_sign_in') if $c->get_param('social_sign_in');
    }

    if ($c->user_exists) {
        if ($c->user->is_superuser || $c->user->from_body) {
            $update->set_extra_metadata( contributed_by => $c->user->id );
        }
    }

    if ( $c->cobrand->never_confirm_updates ) {
        $update->user->update_or_insert;
        $update->confirm();
    # If created on behalf of someone else, we automatically confirm it,
    # but we don't want to update the user account
    } elsif ($c->stash->{contributing_as_another_user}) {
        $update->set_extra_metadata( contributed_as => 'another_user');
        $update->confirm();
    } elsif ($c->stash->{contributing_as_body}) {
        $update->set_extra_metadata( contributed_as => 'body' );
        $update->confirm();
    } elsif ($c->stash->{contributing_as_anonymous_user}) {
        $update->set_extra_metadata( contributed_as => 'anonymous_user' );
        $update->confirm();
    } elsif ( !$update->user->in_storage ) {
        # User does not exist.
        $c->forward('tokenize_user', [ $update ]);
        $update->user->name( undef );
        $update->user->password( '', 1 );
        $update->user->insert;
    }
    elsif ( $c->user && $c->user->id == $update->user->id ) {
        # Logged in and same user, so can confirm update straight away
        $update->user->update;
        $update->confirm;
    } else {
        # User exists and we are not logged in as them.
        $c->forward('tokenize_user', [ $update ]);
        $update->user->discard_changes();
    }

    $update->update_or_insert;

    return 1;
}

=head2 redirect_or_confirm_creation

Now that the update has been created either redirect the user to problem page if it
has been confirmed or email them a token if it has not been.

=cut

sub redirect_or_confirm_creation : Private {
    my ( $self, $c ) = @_;
    my $update = $c->stash->{update};

    # If confirmed send the user straight there.
    if ( $update->confirmed ) {
        $c->forward( 'update_problem' );
        $c->forward( 'signup_for_alerts' );
        if ($c->stash->{contributing_as_another_user} && $update->user->email) {
            $c->send_email( 'other-updated.txt', {
                to => [ [ $update->user->email, $update->name ] ],
            } );
        }
        $c->stash->{template} = 'tokens/confirm_update.html';
        return 1;
    }

    # People using 2FA can not log in by code
    $c->detach( '/page_error_403_access_denied', [] ) if $update->user->has_2fa;

    my $data = $c->stash->{token_data};
    $data->{id} = $update->id;
    $data->{add_alert} = $c->get_param('add_alert') ? 1 : 0;

    if ($update->user->email_verified) {
        $c->forward('send_confirmation_email');
        # tell user that they've been sent an email
        $c->stash->{non_public} = $update->problem->non_public;
        $c->stash->{template}   = 'email_sent.html';
        $c->stash->{email_type} = 'update';
    } elsif ($update->user->phone_verified) {
        $c->forward('send_confirmation_text');
    } else {
        warn "Reached update confirmation with no username verification";
    }

    return 1;
}

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

    my $update = $c->stash->{update};
    my $token = $c->model("DB::Token")->create( {
        scope => 'comment',
        data  => $c->stash->{token_data},
    } );
    my $template = 'update-confirm.txt';
    $c->stash->{token_url} = $c->uri_for_email( '/C', $token->token );
    $c->send_email( $template, {
        to => [ $update->name ? [ $update->user->email, $update->name ] : $update->user->email ],
    } );
}

sub send_confirmation_text : Private {
    my ( $self, $c ) = @_;
    my $update = $c->stash->{update};
    $c->forward('/auth/phone/send_token', [ $c->stash->{token_data}, 'comment', $update->user->phone ]);
    my $error = $c->render_fragment( 'auth/_username_error.html', { default => 'phone' });
    if ($error) {
        $c->stash->{field_errors}{username_register} = $error;
        $c->go( '/report/display', [ $c->stash->{problem}->id ], [] );
    }
    $c->stash->{submit_url} = '/report/update/text';
}

sub confirm_by_text : Path('text') {
    my ( $self, $c ) = @_;

    $c->stash->{submit_url} = '/report/update/text';
    $c->forward('/auth/phone/code', [ 'comment', '/report/update/process_confirmation' ]);
}

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

    $c->stash->{template} = 'tokens/confirm_update.html';
    my $data = $c->stash->{token_data};
    my $token_redeem_cooldown_seconds = $c->stash->{token_redeem_cooldown_seconds};
    my $token = $c->stash->{token_object};

    unless ($c->stash->{update}) {
        $c->stash->{update} = $c->model('DB::Comment')->find({ id => $data->{id} }) || return;
    }
    my $comment = $c->stash->{update};

    # check that this email or domain are not the cause of abuse. If so hide it.
    if ( $comment->is_from_abuser ) {
        $c->stash->{template} = 'tokens/abuse.html';
        return;
    }

    if ($comment->state ne 'unconfirmed' && (!$token_redeem_cooldown_seconds ||
        (!$token->redeemed || $token->redeemed < time() - $token_redeem_cooldown_seconds))) {

        # Don't log the user in on the confirmation link unless it was redeemed within
        # some specified cooldown period.

        my $report_uri = $c->cobrand->base_url_for_report( $comment->problem ) . $comment->problem->url;
        $c->res->redirect($report_uri);
        return;
    }

    if ($comment->state eq 'unconfirmed') {
        if ($token_redeem_cooldown_seconds) {
            # Mark when the token was redeemed if there is a cooldown set so
            # we can track if new uses are within the window.
            $token->mark_redeemed(time());
        }

        if ( $data->{name} || $data->{password} ) {
            for (qw(name facebook_id twitter_id)) {
                $comment->user->$_( $data->{$_} ) if $data->{$_};
            }
            $comment->user->add_oidc_id($data->{oidc_id}) if $data->{oidc_id};
            $comment->user->extra({
                %{ $comment->user->get_extra() },
                %{ $data->{extra} }
            }) if $data->{extra};
            $c->cobrand->call_hook(roles_from_oidc => $comment->user, $data->{roles});
            $comment->user->password( $data->{password}, 1 ) if $data->{password};
            $comment->user->update;
            # Make sure extra oauth state is restored, if applicable
            foreach (qw/logout_redirect_uri change_password_uri/) {
                if ($data->{$_}) {
                    $c->session->{oauth} ||= ();
                    $c->session->{oauth}{$_} = $data->{$_};
                }
            }
        }
    }

    if ($comment->user->email_verified) {
        $c->authenticate( { email => $comment->user->email, email_verified => 1 }, 'no_password' );
    } elsif ($comment->user->phone_verified) {
        $c->authenticate( { phone => $comment->user->phone, phone_verified => 1 }, 'no_password' );
    } else {
        warn "Reached user authentication with no username verification";
    }
    $c->set_session_cookie_expire(0);

    if ($comment->state eq 'unconfirmed') {
        $c->stash->{update}->confirm;
        $c->stash->{update}->update;
        $c->forward('update_problem');
        $c->stash->{add_alert} = $data->{add_alert};
        $c->forward('signup_for_alerts');
    }

    return 1;
}

=head2 signup_for_alerts

If the user has selected to be signed up for alerts then create a
new_updates alert. Or if they're logged in and they've unticked the
box, disable their alert.

NB: this does not check if they are a registered user so that should
happen before calling this.

=cut

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

    my $update = $c->stash->{update};
    my $user = $update->user;
    my $problem_id = $update->problem_id;

    if ( $c->stash->{add_alert} ) {
        my $options = {
            cobrand => $update->cobrand,
            cobrand_data => $update->cobrand_data,
            lang => $update->lang,
        };
        $user->create_alert($problem_id, $options);
    } elsif ( my $alert = $user->alert_for_problem($problem_id) ) {
        $alert->disable();
    }

    $c->cobrand->call_hook(update_email_shortlisted_user => $update);

    return 1;
}

__PACKAGE__->meta->make_immutable;

1;
