package Astro::ADS::Search;
# ABSTRACT: Queries the ADS Search endpoint and collects the results
$Astro::ADS::Search::VERSION = '1.90';
use Moo;
extends 'Astro::ADS';
with 'Astro::ADS::Role::ResultMapper';

use Carp;
use Data::Dumper::Concise;
use Mojo::Base -strict; # do we want -signatures
use Mojo::DOM;
use Mojo::File qw( path );
use Mojo::URL;
use Mojo::Util qw( quote );
use PerlX::Maybe;
use Types::Standard qw( Int Str ArrayRef HashRef ); # InstanceOf ConsumerOf

no warnings 'experimental'; # suppress warning for native perl 5.36 try/catch

has [qw/q fq fl sort/] => (
    is       => 'rw',
);
has [qw/start rows/] => (
    is       => 'rw',
    isa      => Int->where( '$_ >= 0' ),
);
# TODO: add year and other fields
has [qw/authors objects bibcode/] => (
    is       => 'rw',
    isa      => ArrayRef[Str],
    default  => sub { return [] },
);
has [qw/author_logic object_logic/] => (
    is       => 'rw',
    isa      => HashRef[],
    default  => sub { return {} },
);

before sort => sub {
    my $orig = shift;
    my $self = shift;
    my ($field, $direction) = @_;

    return unless $field;

    my $sort_field_re = qr/(?:id|author_count|bibcode|citation_count|citation_count_norm|classic_factor|first_author|date|entry_date|read_count|score)/;
    if ($field =~ /^$sort_field_re\+(?:asc|desc)$/) {
        $orig->($self, $field);
    }
    if ($field !~ /^$sort_field_re$/) {
        carp 'Invalid sort field: ', $field;
        return;
    }
    if ($direction eq 'asc' || $direction eq 'desc') {
        carp 'Invalid sort direction: ', $direction;
        return;
    }
    $orig->($self, join('+', $field, $direction) );
};

sub query {
    my ($self, $terms) = @_;
    my $url = $self->base_url->clone->path('search/query');
    my $search_terms = $self->gather_search_terms( $terms ) or return;

    $url->query( $search_terms );
    my $response = $self->get_response( $url );
    if ( $response->is_error ) {
        carp $response->message;
        return Astro::ADS::Result->new( {error => $response->message} );
    }

    my $json = $response->json;
    return $self->parse_response( $json );
}

sub query_tree {
    my ($self, $terms) = @_;
    carp "Not implemented yet"; return;

    my $url = $self->base_url->path('search/qtree');
    $url->query( { q => $self->q, fl => $self->fl } );
    return $self->get_result( $url );
}

sub bigquery {
    my ($self, $terms) = @_;
    carp "Not implemented yet"; return;

    my $url = $self->base_url->path('search/bigquery');
    $url->query( { q => $self->q, fl => $self->fl } );
    #return $self->post_result( $url );
}

sub gather_search_terms {
    my ($self, $terms) = @_;

    my @query = ();
    if ( $terms && $terms->{q} ) {
        push @query, delete $terms->{q};
    }
    else {
        push @query, $self->q if $self->q;
        push @query, delete $terms->{'+q'} if exists $terms->{'+q'};

        if ( @{$self->authors} ) {
            my $tag = 'author:';
            substr($tag, 0, 0) = '=' if $self->author_logic->{exact};
            if ( @{$self->authors} > 1 ) {
                my $logic = $self->author_logic->{OR} ? q{ OR } : q{ };
                push @query, "$tag(" . join( $logic, map { quote $_ } @{$self->authors}) . ')';
            }
            else {
                push @query, $tag . quote $self->authors->[0];
            }
        }

        if ( @{$self->objects} ) {
            my $tag = 'object:';
            if ( @{$self->objects} > 1 ) {
                my $logic = $self->object_logic->{OR} ? q{ OR } : q{ };
                push @query, "$tag(" . join( $logic, map { quote $_ } @{$self->objects}) . ')';
            }
            else {
                push @query, $tag . quote $self->objects->[0];
            }
        }

        # need to remember which attributes take multiple values
        push @query, @{$self->bibcode} if @{$self->bibcode};
    }
                
    unless ( @query ) {
        carp 'No search terms provided for query';
        return;
    }

    my $search_terms = {
        q => join(q{ }, @query),
        maybe fq => $self->fq,
        maybe fl => $self->fl,
        maybe start => $self->start,
        maybe rows  => $self->rows,
        maybe sort  => $self->sort,
        %$terms
    };

    return $search_terms;
}

sub add_authors {
    my ($self, @authors) = @_;
    push @{$self->authors}, @authors;
}

sub add_objects {
    my ($self, @objects) = @_;
    push @{$self->objects}, @objects;
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Astro::ADS::Search - Queries the ADS Search endpoint and collects the results

=head1 VERSION

version 1.90

=head1 SYNOPSIS

    my $search = Astro::ADS::Search->new({
        q  => '...', # initial search query
        fl => '...', # return list of attributes
    });

    my $result = $search->query();
    my @papers = $result->papers();

    while ( my $t = $result->next_query() ) {
        $result = $search->query( $t );
        push @papers, $result->get_papers();
    }

    while ( my $t = $result->next_query() ) {
        push @papers, $result->more_papers( $t );
    }

    while ( push @papers, $result->next_query()->more_papers() ) {
    }

=head1 DESCRIPTION

Search for papers in the Harvard ADS 

You can put base terms in the creation of the object and use the
query method to add new terms to that query only

=head1 Methods

=head2 query

Adding a field key C<+q> to the query method B<adds> the query
term to the existing query terms,
whereas specifying a value for C<q> in the query method
overwrites the query terms and neglects gathering other search attributes,
such as authors or objects.

=head2 add_authors

Add a list of authors to a search query. Authors added here will not be
deleted if the query attribute is updated.

=head2 add_objects

Add a list of objects to a search query. Objects added here will not be
deleted if the query attribute is updated.

=head2 query_tree

B<Not implemented yet>

Will return the L<Abstract Syntax Tree|https://ui.adsabs.harvard.edu/help/api/api-docs.html#get-/search/qtree> for the query.

=head2 bigquery

B<Not implemented yet>

Accepts a L<list of many IDs|https://ui.adsabs.harvard.edu/help/api/api-docs.html#post-/search/bigquery> and supports paging.

=head2 Notes

From the ADS API, the "=" sign turns off the synonym expansion feature
available with the author and title fields

=head1 See Also

=over 4

=item *L<Astro::ADS>

=item *L<Astro::ADS::Result>

=item *L<ADS API|https://ui.adsabs.harvard.edu/help/api/>

=item *L<Search Syntax|https://ui.adsabs.harvard.edu/help/search/search-syntax>

=back

=head1 AUTHOR

Boyd Duffee <duffee@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2025 by Boyd Duffee.

This is free software, licensed under:

  The MIT (X11) License

=cut