package POE::Component::MetaCPAN::Recent;
$POE::Component::MetaCPAN::Recent::VERSION = '1.06';
#ABSTRACT: Obtain uploaded CPAN dists via MetaCPAN.

use strict;
use warnings;
use Carp;
use POE qw[Component::SmokeBox::Recent::HTTP];
use URI;
use HTTP::Request;
use HTTP::Response;
use JSON::PP;
use Time::Piece;
use Digest::SHA qw[sha256_hex];

sub spawn {
  my $package = shift;
  my %opts = @_;
  $opts{lc $_} = delete $opts{$_} for keys %opts;
  croak "$package requires an 'event' argument\n" unless $opts{event};
  $opts{delay} = 180 unless $opts{delay};
  my $options = delete $opts{options};
  my $self = bless \%opts, $package;
  $self->{cache} = {};
  $self->{session_id} = POE::Session->create(
    object_states => [
      $self => {
        shutdown      => '_shutdown',
        http_sockerr  => '_get_connect_error',
        http_timeout  => '_get_connect_error',
        http_response => '_handle_recent',
      },
      $self => [ qw(_start _get_recent _real_shutdown) ],
    ],
    heap => $self,
    ( ref($options) eq 'HASH' ? ( options => $options ) : () ),
  )->ID();
  return $self;
}

sub session_id {
  return $_[0]->{session_id};
}

sub shutdown {
  my $self = shift;
  $poe_kernel->post( $self->{session_id}, 'shutdown' );
  return;
}

sub _shutdown {
  my ($kernel,$self) = @_[KERNEL,OBJECT];
  $self->{_shutdown} = 1;
  return if $self->{_http_requests};
  $kernel->yield( '_real_shutdown' );
  return;
}

sub _real_shutdown {
  my ($kernel,$self) = @_[KERNEL,OBJECT];
  $kernel->alarm_remove_all();
  $kernel->alias_remove( $_ ) for $kernel->alias_list();
  $kernel->refcount_decrement( $self->{sender_id}, __PACKAGE__ );
  return;
}

sub _start {
  my ($kernel,$session,$sender,$self) = @_[KERNEL,SESSION,SENDER,OBJECT];
  $self->{session_id} = $session->ID();
  if ( $kernel == $sender and !$self->{session} ) {
    croak "Not called from another POE session and 'session' wasn't set\n";
  }
  my $sender_id;
  if ( $self->{session} ) {
    if ( my $ref = $kernel->alias_resolve( $self->{session} ) ) {
        $sender_id = $ref->ID();
    }
    else {
        croak "Could not resolve 'session' to a valid POE session\n";
    }
  }
  else {
    $sender_id = $sender->ID();
  }
  $kernel->refcount_increment( $sender_id, __PACKAGE__ );
  $self->{sender_id} = $sender_id;
  $self->{timestamp} = 0;
  # Start requesting
  $kernel->yield('_get_recent');
  return;
}

sub _get_recent {
  my ($kernel,$self) = @_[KERNEL,OBJECT];
  $kernel->delay('_get_recent');
  if ( $self->{shutdown} ) {
    $kernel->yield('_real_shutdown');
    return;
  }
  POE::Component::SmokeBox::Recent::HTTP->spawn(
     uri => URI->new( 'http://fastapi.metacpan.org/release/recent?type=l&page=1&page_size=100' ),
  );
  my $cachetime = time() - ( 60 * 60 * 2 );
  foreach my $digest ( keys %{ $self->{cache} } ) {
    delete $self->{cache}->{$digest} if $self->{cache}->{$digest} < $cachetime;
  }
  $self->{_http_requests}++;
  return;
}

sub _handle_recent {
  my ($kernel,$self,$http_resp) = @_[KERNEL,OBJECT,ARG0];
  $self->{_http_requests}--;
  if ( $http_resp and $http_resp->code() == 200 ) {
    my $recents = eval { decode_json( $http_resp->content() ) };
    SWITCH: {
      last SWITCH unless $recents;
      last SWITCH unless $recents->{releases};
      last SWITCH unless ref $recents->{releases} eq 'ARRAY';
      my @uploads;
      RELEASES: foreach my $recent ( @{ $recents->{releases} } ) {
        my $ts = Time::Piece->strptime($recent->{date},"%Y-%m-%dT%H:%M:%S")->epoch;
        $self->{timestamp} = $ts unless $self->{timestamp};
        last RELEASES if $ts <= $self->{timestamp};
        $recent->{ts} = $ts;
        my $digest = sha256_hex( join '/', $recent->{author}, $recent->{name} );
        push @uploads, $recent unless $self->{cache}->{$digest};
        $self->{cache}->{$digest} = $ts;
      }
      foreach my $upload ( reverse @uploads ) {
        $self->{timestamp} = delete $upload->{ts};
        $kernel->post( $self->{sender_id}, $self->{event}, $upload );
      }
    }
  }
  $kernel->yield('_real_shutdown') if $self->{shutdown};
  $kernel->delay('_get_recent', $self->{delay}) unless $self->{shutdown};
  return;
}

sub _get_connect_error {
  my ($kernel,$self) = @_[KERNEL,OBJECT];
  $self->{_http_requests}--;
  $kernel->delay('_get_recent', $self->{delay});
  return;
}

"Fooby Dooby Foo Bar";

__END__

=pod

=encoding UTF-8

=head1 NAME

POE::Component::MetaCPAN::Recent - Obtain uploaded CPAN dists via MetaCPAN.

=head1 VERSION

version 1.06

=head1 SYNOPSIS

  use strict;
  use POE qw(Component::MetaCPAN::Recent);

  $|=1;

  POE::Session->create(
        package_states => [
          'main' => [qw(_start upload)],
        ],
  );

  $poe_kernel->run();
  exit 0;

  sub _start {
    POE::Component::MetaCPAN::Recent->spawn(
        event => 'upload',
    );
    return;
  }

  sub upload {
    use Data::Dumper;
    print Dumper( $_[ARG0] ), "\n";
    return;
  }

=head1 DESCRIPTION

POE::Component::MetaCPAN::Recent is a L<POE> component that alerts newly uploaded CPAN
distributions. It obtains this information from polling L<http://fastapi.metacpan.org/release/recent>.

=head1 CONSTRUCTOR

=over

=item C<spawn>

Takes a number of parameters:

  'event', the event handler in your session where each new upload alert should be sent, mandatory;
  'session', optional if the poco is spawned from within another session;

The 'session' parameter is only required if you wish the output event to go to a different
session than the calling session, or if you have spawned the poco outside of a session.

Returns an object.

=back

=head1 METHODS

=over

=item C<session_id>

Returns the POE::Session ID of the component.

=item C<shutdown>

Terminates the component.

=back

=head1 INPUT EVENTS

=over

=item C<shutdown>

Terminates the component.

=back

=head1 OUTPUT EVENTS

An event will be triggered for each new CPAN upload. The event will have ARG0 set to the C<hashref> of the
upload

=head1 SEE ALSO

L<POE>

L<POE::Component::SmokeBox::Recent::HTTP>

L<http://fastapi.metacpan.org/release/recent>

=head1 AUTHOR

Chris Williams <chris@bingosnet.co.uk>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Chris Williams.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
