package OpenID::Lite::RelyingParty::IDResHandler::Verifier;
use Any::Moose;

use URI;
use List::MoreUtils qw(any);
use OpenID::Lite::Nonce;
use OpenID::Lite::Util::URI;
use OpenID::Lite::Util::XRI;
use OpenID::Lite::SignatureMethods;
use OpenID::Lite::Identifier;
use OpenID::Lite::RelyingParty::DirectCommunication;
use OpenID::Lite::RelyingParty::Discover;
use OpenID::Lite::RelyingParty::Discover::Service;
use OpenID::Lite::Constants::Namespace
    qw(SPEC_1_0 SPEC_2_0 SIGNON_1_1 SIGNON_1_0 SIGNON_2_0 SERVER_2_0);
use OpenID::Lite::Constants::ModeType qw(CHECK_AUTHENTICATION);

has 'params' => (
    is       => 'ro',
    isa      => 'OpenID::Lite::Message',
    required => 1,
);

has 'current_url' => (
    is       => 'ro',
    isa      => 'Str',
    required => 1,
);

has 'service' => (
    is        => 'rw',
    isa       => 'OpenID::Lite::RelyingParty::Discover::Service',
    predicate => 'has_service',
);

has 'store' => (
    is => 'ro',

    #does => 'OpenID::Lite::Role::Storable',
);

has '_discoverer' => (
    is         => 'ro',
    lazy_build => 1,
);

has '_direct_communication' => (
    is         => 'ro',
    isa        => 'OpenID::Lite::RelyingParty::DirectCommunication',
    lazy_build => 1,
);

with 'OpenID::Lite::Role::AgentHandler';
with 'OpenID::Lite::Role::ErrorHandler';

my @OP1_FIELDS     = qw(return_to assoc_handle sig signed identity);
my @OP1_SIG_FIELDS = qw(return_to identity);

my @OP2_FIELDS     = qw(return_to assoc_handle sig signed op_endpoint);
my @OP2_SIG_FIELDS = qw(return_to response_nonce assoc_handle);

sub verify {
    my $self = shift;
    $self->_check_for_fields() or return;
    $self->_verify_return_to() or return;

    $self->_verify_discovery_results() or return;
    $self->_check_nonce()     or return;
    $self->_check_signature() or return;

    return 1;
}

sub _check_for_fields {
    my $self = shift;

    my ( @fields, @signed_fields );
    if ( $self->params->is_openid1 ) {
        @fields        = @OP1_FIELDS;
        @signed_fields = @OP1_SIG_FIELDS;
    }
    else {
        @fields        = @OP2_FIELDS;
        @signed_fields = @OP2_SIG_FIELDS;
    }

    my $signed = $self->params->get('signed')
        or $self->ERROR(q{signed key not found.});
    my @signed = split /,/, $signed;

    for my $field (@fields) {
        unless ( $self->params->has_key($field) ) {
            return $self->ERROR( sprintf q{"%s" key not found}, $field );
        }
    }
    for my $field (@signed_fields) {
        unless ( $self->params->has_key($field)
            && ( any { $field eq $_ } @signed ) )
        {
            return $self->ERROR( sprintf q{"%s" key not found}, $field );
        }
    }
    return 1;
}

sub _verify_return_to {
    my $self = shift;

    my $url = $self->params->get('return_to');
    return unless $url;
    $url = OpenID::Lite::Util::URI->normalize($url);
    return unless $url;
    return unless OpenID::Lite::Util::URI->is_uri($url);

    $self->_verify_return_to_args($url)
        or return;
    $self->_verify_return_to_base($url)
        or return;
    return 1;
}

sub _verify_return_to_args {
    my ( $self, $return_to ) = @_;
    $return_to = URI->new($return_to);
    my %parsed = $return_to->query_form();

    for my $key ( keys %parsed ) {
        my $msg_val = $self->params->get_extra($key);
        return $self->ERROR(
            sprintf q{Message missing return_to argument, "%s"}, $key )
            unless $msg_val;
        return $self->ERROR(
            sprintf
                q{Parameter [%s]s value [%s] doesn't match return_to's value [%s]},
            $key, $msg_val, $parsed{$key} )
            if ( $msg_val ne $parsed{$key} );
    }

    for my $key ( @{ $self->params->get_extra_keys() } ) {
        my $msg_val = $self->params->get_extra($key);
        return $self->ERROR(
            q{Unexpected parameter (not on return_to), [%s = %s]},
            $key, $msg_val )
            if not exists $parsed{$key};
        if ( $msg_val ne $parsed{$key} ) {
            return $self->ERROR(
                sprintf
                    q{Parameter [%s]s value [%s] doesn't match return_to's value [%s]},
                $key, $msg_val, $parsed{$key} );
        }
    }

    return 1;
}

sub _verify_return_to_base {
    my ( $self, $return_to ) = @_;
    $return_to = URI->new($return_to);
    my $current_url = URI->new( $self->current_url );
    for my $meth (qw(scheme host port path)) {
        return $self->ERROR(
            sprintf q{"%s" in response parameter wasn't match to "%s"},
            $return_to->as_string, $current_url->as_string )
            unless $current_url->$meth eq $return_to->$meth;
    }
    return 1;
}

sub _verify_discovery_single {
    my ( $self, $endpoint, $to_match ) = @_;
    for my $type_uri ( @{ $to_match->types } ) {
        return $self->ERROR(q{Type uri mismatch.})
            unless $endpoint->has_type($type_uri);
    }

    my $defragged_claimed_id;
    my $scheme = OpenID::Lite::Util::XRI->identifier_scheme(
        $to_match->claimed_identifier );
    if ( $scheme eq 'xri' ) {
        $defragged_claimed_id = $to_match->claimed_identifier;
    }
    elsif ( $scheme eq 'uri' ) {
        if (OpenID::Lite::Util::URI->is_uri( $to_match->claimed_identifier ) )
        {
            my $parsed = URI->new( $to_match->claimed_identifier );
            $parsed->fragment(undef);
            $defragged_claimed_id = $parsed->as_string;
        }
        else {
            $defragged_claimed_id = $to_match->claimed_identifier;
        }
    }
    else {
        return $self->ERROR(
            sprintf q{Invalid claimed_id, "%s"},
            $to_match->claimed_identifier || ''
        );
    }

    if ( $defragged_claimed_id ne $endpoint->claimed_identifier ) {
        return $self->ERROR(
            sprintf q{Claimed IDs don't match, "%s" and "%s" },
            $defragged_claimed_id, $endpoint->claimed_identifier );
    }

    if ($endpoint->find_local_identifier ne $to_match->find_local_identifier )
    {
        return $self->ERROR(
            sprintf q{Local IDs don't match, "%s" and "%s" },
            $to_match->find_local_identifier,
            $endpoint->find_local_identifier
        );
    }

    if ( !$to_match->url ) {
        if ( $to_match->preferred_namespace ne SPEC_1_0 ) {
            return $self->ERROR(
                q{ensure that endpoint is openid2.0 without openid.op_endpoint?}
            );
        }
    }
    elsif ( $endpoint->url ne $to_match->url ) {
        return $self->ERROR( sprintf q{OP endpoint mismatch, "%s" and "%s"},
            $endpoint->url, $to_match->url );
    }
    return 1;
}

sub _verify_discovered_services {
    my ( $self, $claimed_id, $services, $to_match_endpoints ) = @_;

    my @failure_messages;
    for my $service ( @$services ) {
        for my $to_match ( @$to_match_endpoints ) {
            if ( $self->_verify_discovery_single($service, $to_match) ) {
                $self->service($service);
                return 1;
            } else {
                push(@failure_messages, $self->errstr);
                $self->ERROR(undef);
            }
        }
    }
    return $self->ERROR(
        sprintf q{No matching endpoint found for claimed_id "%s".}, $claimed_id);
}

sub _discover_and_verify {
    my ( $self, $claimed_id, $to_match_endpoints ) = @_;
    $claimed_id = OpenID::Lite::Identifier->normalize($claimed_id);
    my $services = $self->_discoverer->discover($claimed_id)
        or return $self->ERROR( $self->_discoverer->errstr );
    unless ( @$services > 0 ) {
        return $self->ERROR( sprintf q{No OpenID information found at %s},
            $claimed_id );
    }
    return $self->_verify_discovered_services( $claimed_id, $services,
        $to_match_endpoints );
}

sub _verify_discovery_results_openid1 {
    my $self       = shift;

    my $claimed_id = $self->params->get_extra('openid1_claimed_id');

    unless ($claimed_id) {
        if ( $self->has_service && $self->service->claimed_identifier ) {
            $claimed_id = $self->service->claimed_identifier;
        }
        else {
            return $self->ERROR(q{When using OpenID 1, the claimed id must be supplied,
                either by passing it through as a return_to parameter or by using a
                session, and supplied to the IDResHandler when it is constructed.});
        }
    }

    my $to_match = OpenID::Lite::RelyingParty::Discover::Service->new;
    $to_match->op_local_identifier( $self->params->get('identity') );
    $to_match->claimed_identifier($claimed_id);

    my $to_match_1_0 = $to_match->copy();
    $to_match->add_type(SIGNON_1_1);
    $to_match_1_0->add_type(SIGNON_1_0);

    if ( $self->has_service ) {
        my $verified = $self->_verify_discovery_single( $self->service, $to_match )
                    || $self->_verify_discovery_single( $self->service, $to_match_1_0 );
        return 1 if $verified;
    }

    return $self->_discover_and_verify(
        $to_match->claimed_identifier,
        [$to_match, $to_match_1_0]
    );
}

sub _verify_discovery_results_openid2 {
    my $self = shift;
    my $to_match = OpenID::Lite::RelyingParty::Discover::Service->new;
    $to_match->add_type(SIGNON_2_0);
    $to_match->add_uri( $self->params->get('op_endpoint') );

    # claimed_id && identity both or none?
    my $claimed_id = $self->params->get('claimed_id');
    my $identity   = $self->params->get('identity');

    $to_match->claimed_identifier($claimed_id);
    $to_match->op_local_identifier($identity);

    if ( $claimed_id && !$identity ) {
        return $self->ERROR(
            q{openid.claimed_id is present without openid.identity});
    }
    elsif ( !$claimed_id && $identity ) {
        return $self->ERROR(
            q{openid.identity is present without openid.claimed_id});
    }
    elsif ( !$claimed_id ) {

        my $service = OpenID::Lite::RelyingParty::Discover::Service->new;
        $service->add_type(SERVER_2_0);
        $service->add_uri( $self->params->get('op_endpoint') );
        $self->service($service);
        return 1;
    }

    if ( $self->has_service ) {
        unless ( $self->_verify_discovery_single( $self->service, $to_match ) ) {
            return unless $claimed_id;
            return unless $self->_discover_and_verify( $claimed_id, [$to_match] )
        }
    }
    else {
        $self->_discover_and_verify( $claimed_id, [$to_match] )
            or return;
    }

    if ( $self->service->claimed_identifier ne $to_match->claimed_identifier )
    {
        my $copied = $self->service->copy();
        $copied->claimed_identifier( $to_match->claimed_identifier );
        $self->service($copied);
    }

    return 1;
}

sub _verify_discovery_results {
    my $self = shift;
    if ( $self->params->is_openid1 ) {
        $self->_verify_discovery_results_openid1()
            or return;
    }
    elsif ( $self->params->is_openid2 ) {
        $self->_verify_discovery_results_openid2()
            or return;
    }
    else {
        return $self->ERROR(q{No reached});
    }
}

sub _check_nonce {
    my $self = shift;
    my ( $nonce, $server_url );
    if ( $self->params->is_openid1 ) {
        $nonce
            = $self->params->get_extra('rp_nonce');   # TODO: get extra params
        $server_url = '';
    }
    elsif ( $self->params->is_openid2 ) {
        $nonce = $self->params->get('response_nonce');
        $server_url = $self->has_service ? $self->service->url : undef;
    }
    else {
        return $self->ERROR(q{IdRes Response doesn't have proper ns value.});
    }

    unless ($nonce) {
        return $self->ERROR(q{No proper nonce found.});
    }

    my ( $timestamp, $unique ) = OpenID::Lite::Nonce->split_nonce($nonce)
        or
        return $self->ERROR( sprintf q{Invalid response_nonce format. "%s"},
        $nonce );

    if ( $self->store
        && !$self->store->use_nonce( $server_url, $timestamp, $unique ) )
    {
        return $self->ERROR(
            sprintf q{Nonce already used or out of range: "%s"}, $nonce );
    }

    return 1;
}

sub _check_signature {

    my $self = shift;

    my $assoc;
    my $server_url = $self->service->url;

    if ( $self->store ) {
        my $assoc_handle = $self->params->get('assoc_handle');
        $assoc = $self->store->get_association( $server_url, $assoc_handle );
    }

    if ( !$assoc ) {
        return $self->_check_auth();
    }
    else {
        if ( $assoc->is_expired ) {
            return $self->ERROR(q{Association expired});
        }
        else {
            my $secret     = $assoc->secret;
            my $assoc_type = $assoc->type;
            my $method
                = OpenID::Lite::SignatureMethods->select_method($assoc_type);
            return $self->ERROR( sprintf q{Bad signature in response from %s},
                $server_url )
                unless $method->verify( $secret, $self->params );
        }
        return 1;
    }
}

sub _check_auth {
    my $self   = shift;
    my $params = $self->_create_check_auth_request()
        or return;
    my $server_url = $self->service->url;
    my $res_params
        = $self->_direct_communication->send_request( $server_url, $params )
            or return $self->ERROR(q{Failed direct-communication, 'checkauth' request});
    return $self->_process_check_auth_response($res_params);
}

sub _process_check_auth_response {
    my ( $self, $res_params ) = @_;
    my $is_valid          = $res_params->get('is_valid') || 'false';
    my $invalidate_handle = $res_params->get('invalidate_handle');
    my $server_url        = $self->service->url;
    if ($invalidate_handle) {
        if ( $self->store ) {
            $self->store->remove_association( $server_url,
                $invalidate_handle );
        }
    }

    if ( $is_valid ne 'true' ) {
        return $self->ERROR(
            sprintf
                q{Server %s responds that check_authentication call is not valid.},
            $server_url,
        );
    }
    return 1;
}

sub _create_check_auth_request {
    my $self       = shift;
    my $ca_message = $self->params->copy();
    $ca_message->set( mode => CHECK_AUTHENTICATION );
    return $ca_message;

}

sub _build__discoverer {
    my $self = shift;
    return OpenID::Lite::RelyingParty::Discover->new(
        agent => $self->agent, );
}

sub _build__direct_communication {
    my $self = shift;
    return OpenID::Lite::RelyingParty::DirectCommunication->new(
        agent => $self->agent, );
}

no Any::Moose;
__PACKAGE__->meta->make_immutable;
1;

