use strict;
use warnings;    ## required because I can't work out how to get percritic to use my modern config

package WebService::Google::Client::Discovery;
$WebService::Google::Client::Discovery::VERSION = '0.06';

# ABSTRACT: Google API discovery service


use Moo;
use Carp;
use WebService::Google::Client::UserAgent;
use List::Util qw/uniq/;
use Hash::Slice qw/slice/;
use Data::Dumper;
use CHI;    # Caching .. NB Consider reviewing https://metacpan.org/pod/Mojo::UserAgent::Role::Cache
use strict;
use warnings;

has 'ua' => ( is => 'rw', default => sub { WebService::Google::Client::UserAgent->new }, lazy => 1 );    ## typically shared with parent instance of Client which sets on new
has 'debug' => ( is => 'rw', default => 0, lazy => 1 );
has 'chi' => ( is => 'rw', default => sub { CHI->new( driver => 'File', namespace => __PACKAGE__ ) }, lazy => 1 );


sub get_rest
{
  my ( $self, $params ) = @_;
  ## TODO: validate that reset endpoint exists in discovery_data
  ## TODO: warn if user doesn't have the necessary scope
  ## TODO: consolidate the http method calls to a single function - ie - discover_all - simplistic quick fix -  assume that if no param then endpoint is as per discover_all
  croak( "get_rest called with api param undefined" . Dumper $params)     unless defined $params->{ api };
  croak( "get_rest called with version param undefined" . Dumper $params) unless defined $params->{ version };

  croak( "get_rest called with empty api param defined" . Dumper $params)     if $params->{ api } eq '';
  croak( "get_rest called with empty version param defined" . Dumper $params) if $params->{ version } eq '';


  my $key = "$params->{api}/$params->{version}/rest";
  carp "get_rest - $key" if $self->debug;
  if ( my $dat = $self->chi->get( $key ) )    ## clobbers some of the attempted thinking further on .. just return it for now if it's there
  {
    #carp Dumper $dat;
    return $dat;
  }

  if ( my $expires_at = $self->chi->get_expires_at( $key ) )    ## maybe this isn't th ebest way to check if get available.
  {
    carp "CHI '$key' cached data with root = " . $self->chi->root_dir . "expires  in ", scalar( $expires_at ) - time(), " seconds\n" if $self->debug;

    #carp "Value = " . Dumper $self->chi->get( $key ) if  $self->debug ;
    return $self->chi->get( $key );

  }
  else
  {
    carp "'$key' not in cache - fetching it from https://www.googleapis.com/discovery/v1/apis/$key" if $self->debug;
    ## TODO: better handle failed response - if 403 then consider adding in the auth headers and trying again.
    my $ret = $self->ua->get( "https://www.googleapis.com/discovery/v1/apis/$key" )->result;
    if ( $ret->is_success )
    {
      my $dat = $ret->json || croak( "failed to convert https://www.googleapis.com/discovery/v1/apis/$key return data in json" );

      #carp("dat1 = " . Dumper $dat);
      $self->chi->set( "$key", $dat, '30d' );
      return $dat;

      #my $ret_data = $self->chi->get( $key );
      #carp ("ret_data = " . Dumper $ret_data) unless ref($ret_data) eq 'HASH';
      #return $ret_data;# if ref($ret_data) eq 'HASH';
      #croak();
      #$self->chi->remove( $key ) unless eval '${^TAINT}'; ## if not hashref then assume is corrupt so delete it
    }
    else
    {
      ## TODO - handle auth required error and resubmit request with OAUTH headers
      croak( "$ret->message" );    ## should probably croak
    }
  }
  croak( "something went wrong in get_rest key = $key - try again to see if data corruption has been flushed for " . Dumper $params);

  #return $self->chi->get( $key );
  #croak('never gets here');
}


sub discover_all
{

  my ( $self ) = @_;
  if ( my $expires_at = $self->chi->get_expires_at( 'discovery_data' ) )
  {
    carp "discovery_data cached data expires  in ", scalar( $expires_at ) - time(), " seconds\n" if $self->debug;
    return $self->chi->get( 'discovery_data' );
  }
  else
  {
    my $ret = $self->ua->get( 'https://www.googleapis.com/discovery/v1/apis' )->result;
    if ( $ret->is_success )
    {
      my $all = $ret->json;
      $self->chi->set( 'discovery_data', $all, '30d' );
      return $self->chi->get( 'discovery_data' );
    }
    else
    {
      ## TODO - handle auth required error and resubmit request with OAUTH headers
      carp( "$ret->message" );    ## should probably croak
      return {};
    }
  }
  return {};
}


sub available_APIs
{
  my ( $self ) = @_;
  my $all = $self->discover_all()->{ items };

  #print Dumper $all;
  for my $i ( @$all )
  {
    $i = { map { $_ => $i->{ $_ } } grep { exists $i->{ $_ } } qw/name version documentationLink/ };
  }
  my @subset = uniq map { $_->{ name } } @$all;    ## unique names
                                                   # carp scalar @$all;
                                                   # carp scalar @subset;
                                                   # carp Dumper \@subset;
                                                   # my @a = map { $_->{name} } @$all;

  my @arr;
  for my $s ( @subset )
  {
#print Dumper $s;
    my @v        = map      { $_->{ version } } grep           { $_->{ name } eq $s } @$all;
    my @doclinks = uniq map { $_->{ documentationLink } } grep { $_->{ name } eq $s } @$all;

    # carp "Match! :".Dumper \@v;
    # my $versions = grep
    push @arr, { name => $s, versions => \@v, doclinks => \@doclinks };
  }

  #carp Dumper \@arr;
  #exit;
  return \@arr;

  # return \@a;
}


sub service_exists
{
  my ( $self, $api ) = @_;
  return 0 unless $api;
  my $apis_all = $self->available_APIs();
  return grep { $_->{ name } eq $api } @$apis_all;    ## 1 iff an equality is found with keyed name
}


sub supported_as_text
{
  my ( $self ) = @_;
  my $ret = '';
  for my $api ( @{ $self->available_APIs() } )
  {
    croak( 'doclinks key defined but is not the expected arrayref' ) unless ref $api->{ doclinks } eq 'ARRAY';
    croak( 'array of apis provided by available_APIs includes one without a defined name' ) unless defined $api->{ name };

    my @clean_doclinks = grep { defined $_ } @{ $api->{ doclinks } };    ## was seeing undef in doclinks array - eg 'surveys'causing warnings in join

    ## unique doclinks using idiom from https://www.oreilly.com/library/view/perl-cookbook/1565922433/ch04s07.html
    my %seen = ();
    my $doclinks = join( ',', ( grep { !$seen{ $_ }++ } @clean_doclinks ) ) || '';    ## unique doclinks as string

    $ret .= $api->{ name } . ' : ' . join( ',', @{ $api->{ versions } } ) . ' : ' . $doclinks . "\n";
  }
  return $ret;
}


sub available_versions
{
  my ( $self, $api ) = @_;
  return [] unless $api;
  my @api_target = grep { $_->{ name } eq $api } @{ $self->available_APIs() };
  return [] if scalar( @api_target ) == 0;
  return $api_target[0]->{ versions };
}


sub latest_stable_version
{
  my ( $self, $api ) = @_;
  return '' unless $api;
  return '' unless $self->available_versions( $api );
  return '' unless @{ $self->available_versions( $api ) } > 0;
  my $versions = $self->available_versions( $api );    # arrayref
  if ( $versions->[-1] =~ /beta/ )
  {
    return $versions->[0];
  }
  else
  {
    return $versions->[-1];
  }
}


sub find_APIs_with_diff_vers
{
  my ( $self ) = @_;
  my $all = $self->available_APIs();
  return grep { scalar @{ $_->{ versions } } > 1 } @$all;
}


sub search_in_services
{
  my ( $self, $string ) = @_;

  #carp Dumper $self->available_APIs();
  my @res = grep { $_->{ name } eq lc $string } @{ $self->available_APIs };

  #carp " search_in_servicesResult: ".Dumper \@res;
  #carp " search_in_servicesResult: ".Dumper $res[0];
  croak( 'search_in_services has nothing to return ' ) unless defined $res[0];
  return $res[0];
}


sub get_method_meta
{
  my ( $self, $caller ) = @_;

  # $caller = 'WebService::Google::Client::Calendar::CalendarList::delete';
  my @a = split( /::/x, $caller );

  # carp Dumper \@a;
  my $method       = pop @a;                                   # delete
  my $resource     = lcfirst pop @a;                           # CalendarList
  my $service      = lc pop @a;                                # Calendar
  my $service_data = $self->search_in_services( $service );    # was string, become hash
  carp "getResourcesMeta:get_method_meta : " if ( $self->debug );    # . Dumper $service_data

  my $all           = $self->get_rest( { api => $service_data->{ name }, version => $service_data->{ versions }[0] } );
  my $baseUrl       = $all->{ baseUrl };
  my $resource_data = $all->{ resources }{ $resource };                                                                   # return just a list of all methods
  my $method_data   = $resource_data->{ methods }{ $method };                                                             # need method
  $method_data->{ path } = $baseUrl . $method_data->{ path };
  my $res = slice $method_data, qw/httpMethod path id/;    ## refactored without undertsanding - my $res = slice $method_data, qw/httpMethod path id/;
  return $res;
}


sub get_resource_meta
{
  my ( $self, $package ) = @_;

  # $package = 'WebService::Google::Client::Calendar::Events';

  my @a = split( /::/x, $package );
  croak( 'Must be at least 2 levels beneath WebService::Google::Client' ) unless $package =~ /^WebService::Google::Client::[^;]*::/xm;
  my $resource     = lcfirst pop @a;                                                                                     # CalendarList
  my $service      = lc pop @a;                                                                                          # Calendar
                                                                                                                         #carp qq{
                                                                                                                         #  $package
                                                                                                                         #  $service;
                                                                                                                         #};
  my $service_data = $self->search_in_services( $service );                                                              # was string, become hash
                                                                                                                         #carp Dumper $service_data;
  my $all          = $self->get_rest( { api => $service_data->{ name }, version => $service_data->{ versions }[0] } );

  #croak Dumper $all;
  croak( "GET REST SPEC FOR  $service_data->{ name } failed to return hashref with version $service_data->{ versions }[0]  " . Dumper $all) unless ref( $all ) eq 'HASH';
  if ( defined $all->{ resources } )
  {
    return $all->{ resources }{ $resource } if ( defined $all->{ resources }{ $resource } );
  }
  else
  {
    croak( "resources is not a defined key" );
  }
  return $all->{ resources }{ $resource };    # return just a list of all methods
}


sub list_of_methods
{
  my ( $self, $package ) = @_;
  my $r         = $self->get_resource_meta( $package );
  my @meth_keys = keys %{ $r->{ methods } };
  return \@meth_keys;
}


sub meta_for_API
{
  my ( $self, $params ) = @_;
  my $full = $self->discover_all;
  my @api;

  if ( defined $params->{ api } )
  {
    @api = grep { $_->{ name } eq $params->{ api } } @{ $full->{ items } };
  }
  else
  {
    croak "meta_for_API() : No api specified!";
  }

  if ( defined $params->{ version } )
  {
    @api = grep { $_->{ version } eq $params->{ version } } @api;
  }

  return $api[0];
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

WebService::Google::Client::Discovery - Google API discovery service

=head1 VERSION

version 0.06

=head2 MORE INFORMATION

L<https://developers.google.com/discovery/v1/reference/>

=head2 SEE ALSO

Not using Swagger but it is interesting - 
L<https://github.com/APIs-guru/openapi-directory/tree/master/APIs/googleapis.com> for Swagger Specs.

L<Google::API::Client> - contains code for parsing discovery structures 

includes a chi property that is an instance of CHI using File Driver to cache discovery resources for 30 days

say $client-dicovery->chi->root_dir(); ## provides full file path to temp storage location used for caching

=head2 TODO

* deal with case of service names - either make it case insensitive or lock in a consistent approach - currently smells like case changes on context
* handle 403 ( Daily Limit for Unauthenticated Use Exceeded.) errors when reqeusting a disdovery resource for a resvice 
  but do we have access to authenticated reqeusts?
* consider refactoring this entire module into UserAgent .. NB - this is also included as property of  Services.pm which is the factory for dynamic classes

=head1 METHODS

=head2 C<get_rest()>

Retrieve the description of a particular version of an API

  my $d = WebService::Google::Client::Discovery->new;
  $d->get_rest({ api=> 'calendar', version => 'v3' });

Return result like

  $VAR1 = {
          'ownerDomain' => 'google.com',
          'version' => 'v2.4',
          'protocol' => 'rest',
          'icons' => 'HASH(0x29760c0)',
          'discoveryVersion' => 'v1',
          'id' => 'analytics:v2.4',
          'parameters' => 'HASH(0x29709c0)',
          'basePath' => '/analytics/v2.4/',
          'revision' => '20170321',
          'description' => 'Views and manages your Google Analytics data.',
          'servicePath' => 'analytics/v2.4/',
          'title' => 'Google Analytics API',
          'kind' => 'discovery#restDescription',
          'rootUrl' => 'https://www.googleapis.com/',
          'etag' => '"YWOzh2SDasdU84ArJnpYek-OMdg/uF7o_i10s0Ir7WGM7zLi8NwSHXI"',
          'ownerName' => 'Google',
          'auth' => 'HASH(0x2948880)',
          'resources' => 'HASH(0x296b218)',
          'batchPath' => 'batch',
          'name' => 'analytics',
          'documentationLink' => 'https://developers.google.com/analytics/',
          'baseUrl' => 'https://www.googleapis.com/analytics/v2.4/'
        };

=head2 C<discover_all>

  Return details about all APIs

    my $d = WebService::Google::Client::Discovery->new;
    print Dumper $d

=head2 C<available_APIs>

Return arrayref of all available API's (services)

    {
      'name' => 'youtube',
      'versions' => [ 'v3' ]
    },

Useful when printing list of supported API's in documentation

=head2 C<service_exists>

Return 1 if service is supported by Google API discovery. Otherwise return 0

  carp $d->service_exists('calendar');  # 1
  carp $d->service_exists('someapi');  # 0

=head2 C<print_supported>

  Print list of supported APIs in human-readible format 
 
  NB This is not currently called by anywhere and probably better refactoring to return a string rather than print

=head2 C<available_versions>

  Show available versions of particular API

  $d->available_versions('calendar');  # ['v3']
  $d->available_versions('youtubeAnalytics');  # ['v1','v1beta1']

  Returns arrayref

=head2 C<latest_stable_version>

return latest stable verion of API

  $d->available_versions('calendar');  # ['v3']
  $d->latest_stable_version('calendar');  # 'v3'

  $d->available_versions('tagmanager');  # ['v1','v2']
  $d->latest_stable_version('tagmanager');  # ['v2']

  $d->available_versions('storage');  # ['v1','v1beta1', 'v1beta2']
  $d->latest_stable_version('storage');  # ['v1']

=head2 C<find_APIs_with_diff_vers>

Return only APIs with multiple versions available

=head2 C<search_in_services>

  Search in services in "I'm lucky way" ? wtf?

  Must process case-insensitive way:
  e.g. Class is called CalendarList but resources is called calendarList in discovery

=head2 C<get_method_meta>

Download metadata from Google API discovery for particular class method

  $discovery->get_resource_meta('WebService::Google::Client::Calendar::CalendarList::delete')

=head2 C<get_resource_meta>

Download metadata from Google API discovery for particular resource

  $discovery->get_resource_meta('WebService::Google::Client::Calendar::Events')


  returns a hasref with key methods 

=head2 C<list_of_methods>

Return array of methods that are available for particular resource

  $discovery->list_of_methods('WebService::Google::Client::Calendar::Events')

=head2 C<meta_for_API>

  Same as get_rest method but faster. #!PS then why not replace it?
  

  meta_for_API({ api => 'calendar', version => 'v3' });

=head1 AUTHORS

=over 4

=item *

Pavel Serikov <pavelsr@cpan.org>

=item *

Peter Scott <peter@localshop.com.au>

=back

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017-2018 by Pavel Serikov, Peter Scott and others.

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
