#!/usr/local/bin/perl
BEGIN
{
	use strict;
	use lib './lib';
	# use lib '/Users/jack/iseerm/src/perl/Net-API-Stripe/lib';
	use LWP::UserAgent;
	use File::Basename;
	use Devel::Confess;
	use TryCatch;
	use HTML::TreeBuilder;
	use Data::Dumper::Concise;
	use JSON;
	use JSON::Relaxed ();
	use IO::File;
	use URI;
	use Net::API::Stripe;
	our( $VERSION ) = '0.1';
};

{
	our $out = IO::File->new();
	our $err = IO::File->new();
	$out->fdopen( fileno( STDOUT ), 'w' );
	$out->binmode( ":utf8" );
	$out->autoflush( 1 );
	$err->autoflush( 1 );
	$err->fdopen( fileno( STDERR ), 'w' );
	$err->binmode( ":utf8" );
	
	our $stripe_class_to_package_name = $Net::API::Stripe::TYPE2CLASS;
# 	{
# 	ach_credit_transfer		=> 'Net::API::Stripe::Payment::Source::ACHCreditTransfer',
# 	ach_debit				=> 'Net::API::Stripe::Payment::Source::ACHDebit',
# 	address					=> 'Net::API::Stripe::Address',
# 	bank_account			=> 'Net::API::Stripe::Connect::ExternalAccount::Bank',
# 	billing_address			=> 'Net::API::Stripe::Address',
# 	billing_details			=> 'Net::API::Stripe::Billing::Details',
# 	card					=> 'Net::API::Stripe::Connect::ExternalAccount::Card',
# 	charges					=> 'Net::API::Stripe::List',
# 	generated_from			=> 'Net::API::Stripe::Payment::GeneratedFrom',
# 	last_payment_error		=> 'Net::API::Stripe::Error',
# 	last_setup_error		=> 'Net::API::Stripe::Error',
# 	next_action				=> 'Net::API::Stripe::Payment::Intent::NextAction',
# 	payment_intent			=> 'Net::API::Stripe::Payment::Intent',
# 	payment_method			=> 'Net::API::Stripe::Payment::Method',
# 	payment_method_details	=> 'Net::API::Stripe::Payment::Method::Details',
# 	plan					=> 'Net::API::Stripe::Payment::Plan',
# 	setup_intent			=> 'Net::API::Stripe::Payment::Intent::Setup',
# 	shipping				=> 'Net::API::Stripe::Shipping',
# 	shipping_address		=> 'Net::API::Stripe::Address',
# 	source					=> 'Net::API::Stripe::Payment::Source',
# 	transfer_data			=> 'Net::API::Stripe::Payment::Intent::TransferData',
# 	};
	our $stripe_hash_to_ignore =
	[
	'alipay',
	'amex_express_checkout',
	'apple_pay',
	'au_becs_debit',
	'available_plans',
	'bancontact',
	'capabilities',
	'card_present',
	'checks',
	'current_phase',
	'custom',
	'customer_acceptance',
	'decline_on',
	'default_settings',
	## Dynamic virtual module
	'delivery_estimate',
	## Virtual module
	'dob',
	'eps',
	'fraud_details',
	'fpx',
	'giropay',
	'google_pay',
	'ideal',
	'installments',
	'klarna',
	'masterpass',
	'multi_use',
	'multibanco',
	'offline',
	'online',
	'p24',
	'parameters',
	'pause_collection',
	'pending_request',
	'payment_method_options',
	'pending_update',
	## Virtual module used in Net::API::Stripe::Issuing::Card
	'pin',
	'receipt',
	'redirect_to_url',
	'samsung_pay',
	'schedule',
	'sepa_debit',
	'shipping_address_collection',
	'single_use',
	'sofort',
	'source_types',
	'stripe_account',
	'three_d_secure',
	'three_d_secure_usage',
	'visa_checkout',
	'wallet',
	'wechat',
	];
	
	## <script nonce="XLa05PAIlBkIKnht9fBNxQ==">        APIDocs.load({
	## flattenedAPISections
	our $basedir = File::Basename::dirname( $0 );
	our $ua = LWP::UserAgent->new;
	$ua->timeout( 5 );
	$ua->agent( "Angels, Inc Legal Tech/$VERSION" );
	$ua->cookie_jar({ file => "$basedir/cookies.txt" });
	mkdir( "$basedir/cache" ) if( !-e( "$basedir/cache" ) );
	our $cache_dir = "$basedir/cache";
	our $example_json_dir = "$basedir/sample_json";
	my $base_url = 'https://stripe.com/docs/api';
	my $base_uri = URI->new( $base_url );
	my $stripe_ref = &fetch( $base_url ) || die( "Cannot fetch url $base_url\n" );
	my $section_ref = $stripe_ref->{apiSections} || die( "Cannot find the key apiSections in json data at url $base_url\n", Data::Dumper::Concise::Dumper( $stripe_ref ), "\n" );
	my $topics;
	if( ref( $section_ref ) eq 'HASH' && exists( $section_ref->{topics} ) )
	{
		$topics = $section_ref->{topics} || die( "Cannot find topics in apiSections json at url $base_url\n" );
	}
	elsif( ref( $section_ref ) eq 'ARRAY' )
	{
		if( scalar( @$section_ref ) > 0 && ref( $section_ref->[0] ) eq 'HASH' && exists( $section_ref->[0]->{sections} ) )
		{
			$topics = $section_ref;
		}
		else
		{
			die( "Found topics in apiSections, but the array is either empty of does not contain hash references.\n" );
		}
	}
	else
	{
		die( "Unable to find the topics sections anywhere in the json data.\n" );
	}
	my $urls = {};
	for( my $i = 1; $i < scalar( @$topics ); $i++ )
	{
		my $sections = $topics->[$i]->{sections};
		foreach my $section ( @$sections )
		{
			# $out->print( "Found class name '$section->{anchor}', path '$section->{path}' and title '$section->{title}'\n" );
			my $uri = $base_uri->clone;
			$uri->path( join( '', $base_uri->path, $section->{path} ) );
			my $this = 
			{
			name => $section->{anchor},
			uri => $uri,
			title => $section->{title},
			};
			$urls->{ $this->{name} } = $this;
		}
	}
	$out->printf( "Found %d urls:\n", scalar( keys( %$urls ) ) );
	
	## To contains the package name that contain missing methods
	my $errors = {};
	my $ok = {};
	my $classes = {};
	foreach my $class ( sort( keys( %$urls ) ) )
	{
		my $this = $urls->{ $class };
		$out->print( "$class ($this->{title}) => $this->{uri}\n" );
		my $url = $this->{uri};
		my $ref = &fetch( $url ) || die( "Cannot fetch url $url\n" );
		## $out->print( Dumper( $ref ), "\n" );
		## Also search for the api code sample in apiSections->{customer_bank_account_object}->{data}->{snippets}->[0]
		if( $ref->{flattenedAPISections} && ref( $ref->{flattenedAPISections} ) eq 'HASH' )
		{
			foreach my $section ( keys( %{$ref->{flattenedAPISections}} ) )
			{
				if( $ref->{flattenedAPISections}->{ $section }->{data}->{snippets} &&
					ref( $ref->{flattenedAPISections}->{ $section }->{data}->{snippets} ) eq 'ARRAY' )
				{
					my $sec_ref = $ref->{flattenedAPISections}->{ $section };
					die( "snippets property for section $section at url $url is not an array reference!\n" ) if( ref( $sec_ref->{data}->{snippets}->[0] ) ne 'HASH' );
					die( "snippets property's first entry for section $section at url $url is not an hash reference reference!\n" ) if( ref( $sec_ref->{data}->{snippets} ) ne 'ARRAY' );
					my $code_str = $sec_ref->{data}->{snippets}->[0]->{code};
					my $name = $sec_ref->{data}->{object_type} || $sec_ref->{data}->{doc_object_type} || die( "Cannot find an object type for section $section at url $url\n" );
					$name =~ s/[^a-zA-Z0-9\_]+//g;
					$out->print( "Found section code for $section. Saving it to $example_json_dir/$name.json\n" );
					## Now save it
					my $fh = IO::File->new( ">$example_json_dir/${name}.json" ) || die( "Unable to create json file \"$example_json_dir/$name.json at url $url: $!\n" );
					# my $json = JSON->new->pretty->encode( $code_ref )
					$fh->binmode( ':utf8' );
					$fh->print( $code_str, "\n" );
					$fh->close;
				}
			}
		}
	
		$out->print( "Now searching for Stripe api classes specifications\n" );
		my $api_ref = $ref->{flattenedAPISections};
		$out->printf( "Stripe API hash found contains %d keys at top level\n", scalar( keys( %$api_ref ) ) );
		## We search for specs and subspec recursively
		## Those properties must be accompanied at the same level by a name and object_type properties
		local $crawl = sub
		{
			my $hash = shift( @_ );
			my $level = shift( @_ );
			foreach my $k ( sort( keys( %$hash ) ) )
			{
				my $pref = ( '.' x $level );
				## $out->printf( "${pref}Checking for hash $k\n" ) if( ref( $hash->{ $k } ) eq 'HASH' );
				if( ref( $hash->{ $k } ) eq 'HASH' && !$hash->{ $k }->{_looping} )
				{
					my $this = $hash->{ $k };
					# if( $this->{name} && $this->{object_type} && ( $this->{specs} || $this->{subspec} ) )
					if( $this->{check} eq 'hash' && $this->{object_type} && ( $this->{specs} || $this->{subspec} ) )
					{
						$this->{name} ||= $this->{object_type};
						my $this_ref = ( $this->{specs} || $this->{subspec} );
						$this_ref->{_class_doc} = $this->{documentation}->{html};
						$this_ref->{_base_url} = $url;
						## Found an existing entry
						if( exists( $classes->{ $this->{name} } ) && scalar( keys( %{$classes->{ $this->{name} }} ) ) )
						{
							## Previous entry has smaller number of keys than current one, so we use this one
							if( scalar( keys( %{$classes->{ $this->{name} }} ) ) < scalar( keys( %$this_ref ) ) )
							{
								$classes->{ $this->{name} } = $this_ref;
							}
						}
						## Set a fresh declaration of Stripe class
						else
						{
							$classes->{ $this->{name} } = $this_ref;
						}
					}
					## Make sure we are not looping
					$this->{_looping}++;
					$crawl->( $this, $level * 1 );
				}	
			}
		};
		$crawl->( $api_ref, 0 );
	}

	$out->printf( "Found %d Stripe classes\n", scalar( keys( %$classes ) ) );
	my $stripe_classes = {};
	STRIPE_CLASS: foreach my $class ( sort( keys( %$classes ) ) )
	{
		$out->print( "$class\n" );
		my $this = $classes->{ $class };
		$out->print( "\t", join( "\t", split( /\n/, $this->{_class_doc} ) ), "\n" );
		my $max = 0;
		foreach my $k ( keys( %$this ) )
		{
			$max = length( $k ) if( length( $k ) > $max );
		}
		$max++;
		foreach my $k ( sort( keys( %$this ) ) )
		{
			## Skip private properties
			next if( substr( $k, 0, 1 ) eq '_' );
			my $def = $this->{ $k };
			my $data_type = $def->{check};
			my $object_type = $def->{object_type};
			my $desc;
			if( $data_type eq $object_type )
			{
				$desc = $data_type;
			}
			elsif( $data_type eq 'array' )
			{
				$desc = "array of ${object_type}";
			}
			elsif( $data_type eq 'hash' && $data_type ne $object_type )
			{
				$desc = "${object_type} object";
			}
			elsif( $data_type eq 'hash' )
			{
				$desc = $data_type;
			}
			elsif( $object_type eq $k )
			{
				$desc = "$data_type";
			}
			## expandable objects
			elsif( $data_type eq 'string' )
			{
				$desc = "expandable ${object_type} object";
			}
			else
			{
				$desc = "type = '$data_type', object = '$object_type'";
			}
			$def->{desc} = $desc;
			$out->print( "$k", ( '.' x ( $max - length( $k ) ) ), ": [${desc}] ", $def->{documentation}->{html}, "\n" );
		}
		if( scalar( grep( /^$class$/, @$stripe_hash_to_ignore ) ) )
		{
			$out->print( "Instructed to ignore this hash $class\n" );
			next;
		}
		if( !exists( $stripe_class_to_package_name->{ $class } ) )
		{
			if( exists( $this->{id} ) && exists( $this->{object} ) )
			{
				$out->print( "** Unknown Stripe class $class\n" );
				$errors->{ $class } = { unknown => $this, base_url => $this->{_base_url} };
			}
			else
			{
				$out->print( "** This class \"$class\" is just a hash that Stripe use as an object, but lacks an id and object property. It should be added to the \@stripe_hash_to_ignore array\n" );
				$errors->{ $class } = { to_ignore => $this, base_url => $this->{_base_url} };
			}
			next;
		}
		my $package = $stripe_class_to_package_name->{ $class };
		$stripe_classes->{ $package } = { name => $class, base_url => $def->{_base_url} };
		$out->print( "Loading module $package...\n" );
		my $module = $package;
		$module =~ s/\:{2}/\//g;
		$module .= '.pm';
		try
		{
			## https://stackoverflow.com/questions/32608504/how-to-check-if-perl-module-is-available#comment53081298_32608860
			#$out->print( "Module $package already loaded? ", defined( *{"${package}::"} ) ? 'yes' : 'no', "\n" );
			#require "$module" unless( defined( *{"${package}::"} ) );
			require "$module";
		}
		catch( $e )
		{
			$out->print( "** Error attempting to load module $package for Stripe class $class: $e\n" );
			$errors->{ $package } = { loading => $e, base_url => $this->{_base_url} };
			next STRIPE_CLASS;
		}
		$out->print( "Checking fields in Stripe class $class against methods in our module $package\n" );
		my $not_found = 0;
		my $n = 0;
		$stripe_classes->{ $package }->{fields} = [];
		foreach my $prop ( sort( keys( %$this ) ) )
		{
			# next if( $prop eq '_looping' || $prop eq '_class_doc' );
			next if( substr( $prop, 0, 1 ) eq '_' );
			$n++;
			push( @{$stripe_classes->{ $package }->{fields}}, $prop );
			if( !$package->can( $prop ) )
			{
				$not_found++;
				$errors->{ $package } = { missing => [], base_url => $this->{_base_url} } if( !exists( $errors->{ $package }->{missing} ) );
				push( @{$errors->{ $package }->{missing}}, $prop );
			}
			$out->print( "Is method $prop implemented? ", $package->can( $prop ) ? 'yes' : 'no', "\n" );
		}
		if( $not_found )
		{
			$errors->{ $package }->{total} = $n;
			$out->printf( "** Found %d missing method(s) for package $package\n", $not_found );
		}
		else
		{
			$out->print( "All is Ok\n" );
			$ok->{ $package } = $class;
		}
		$out->print( "_" x 20, "\n" );
	}
	
	$out->print( "\n" );
	$out->print( "_" x 42, "\n" );
	$out->printf( "Found %d packages with problems\n", scalar( keys( %$errors ) ) );
	if( scalar( keys( %$errors ) ) )
	{
		STRIPE_API_ERROR: foreach my $pkg ( sort( keys( %$errors ) ) )
		{
			my $this = $errors->{ $pkg };
			$out->print( "$pkg ($this->{base_url}) :\n" );
			if( $this->{unknown} )
			{
				$out->print( "\tThis Stripe class $pkg is unknown to us.\n" );
				my $max = 0;
				my $ref = $this->{unknown};
				foreach my $k ( keys( %$ref ) )
				{
					next if( substr( $k, 0, 1 ) eq '_' );
					$max = length( $k ) if( length( $k ) > $max );
				}
				$max++;
				foreach my $k ( sort( keys( %$ref ) ) )
				{
					next if( substr( $k, 0, 1 ) eq '_' );
					my $def = $ref->{ $k };
					$out->print( "$k ", ( '.' x ( $max - length( $k ) ) ), ": [", $def->{desc}, "] $def->{documentation}->{html}\n" );
				}
				next STRIPE_API_ERROR;
			}
			elsif( $this->{to_ignore} )
			{
				my $ref = $this->{to_ignore};
				$out->print( "\tThis is a fake Stripe class, and just a hash that should be added to the \@stripe_hash_to_ignore\n" );
				next STRIPE_API_ERROR;
			}
			if( $this->{loading} )
			{
				$out->print( "\tError loading: ", $this->{loading}, "\n" );
			}
			if( $this->{missing} )
			{
				$out->printf( "\t%d method(s) missing out of %d: %s\n", scalar( @{$this->{missing}} ), $this->{total}, join( ', ', @{$this->{missing}} ) );
				$out->print( "Existing methods found are: ", join( ', ', @{$stripe_classes->{ $pkg }->{fields}} ), "\n" );
			}
		}
	}
	$out->printf( "%d ok classes found\n", scalar( keys( %$ok ) ) );
	foreach my $pkg ( sort( keys( %$ok ) ) )
	{
		$out->print( "$pkg => ", $ok->{ $pkg }, "\n" );
	}
	exit( 0 );
}

sub fetch
{
	my $url = shift( @_ );
	my $uri = URI->new( $url );
	# my $cache_file = 'stripe_api_doc_cache.html';
	my $cache_file = $uri->path;
	$cache_file =~ s,^/,,;
	## There should not be, but just in case
	$cache_file =~ s/\.html$//;
	$cache_file =~ s/\//_/g;
	$cache_file =~ s/[^a-zA-Z0-9]+/_/g;
	$cache_file =~ s/\_{2,}/_/g;
	my $cache_json = "${cache_dir}/${cache_file}.json";
	$cache_file .= '.html';
	$cache_file = "$cache_dir/$cache_file";
	my $ttl = 86400;
	my $html;
	$out->print( "Cache file $cache_file exists? ", -e( $cache_file ) ? 'yes' : 'no', "\n" );
	$out->print( "Cache file $cache_file is empty? ", -z( $cache_file ) ? 'yes' : 'no', "\n" );
	$out->print( "Cache file $cache_file ttl of $ttl has expired? ", ( ( ( stat( $cache_file ) )[9] + $ttl ) - time() <= 0 ) ? 'yes' : 'no', "\n" );
	if( -e( $cache_file ) && !-z( $cache_file ) && ( ( ( stat( $cache_file ) )[9] + $ttl ) - time() > 0 ) )
	{
		$out->print( "Fetching data from cache file $cache_file\n" );
		my $io = IO::File->new( "<$cache_file" ) || die( "$cache_file: $!\n" );
		$io->binmode( ':utf8' );
		$html = join( '', $io->getlines );
		$io->close;
	}
	else
	{
		$out->print( "Fetching url $url\n" );
		my $resp;
		try
		{
			$resp = $ua->get( $url );
		}
		catch( $e )
		{
			die( "An Error occured while accessing url $url: $e\n" );
		}
		die( "$url: ", $resp->status_line ) if( !$resp->is_success );
		$out->print( "Retrieving content of page retrieved\n" );
		$html = $resp->decoded_content || die( "No content found for url $url\n" );
		$out->print( "Saving data to cache file $cache_file\n" );
		my $io = IO::File->new( ">$cache_file" ) || die( "$!\n" );
		$io->binmode( ':utf8' );
		$io->print( $html ) || die( $! );
		$io->close;
	}
	my $tree = HTML::TreeBuilder->new;
	$tree->ignore_unknown( 0 );
	$tree->store_comments( 1 );
	$tree->parse( $html );
	$tree->eof();
	$out->print( "Checking script sections...\n" );
	my $n = 0;
	my @scripts = $tree->look_down( _tag => 'script' );
	my $the_sc;
	foreach my $sc ( @scripts )
	{
		# $out->printf( "Checking No %d\n", ++$n );
		if( $sc->as_HTML( '' ) =~ /APIDocs\.load/ )
		{
			# $out->print( "Found it !\n" );
			$the_sc = $sc;
			last;
		}
	}
	my $ct = $the_sc->content_array_ref;
	# $out->print( Dumper( $ct ), "\n" );
	my $data = $ct->[0];
	$out->printf( "Found %d bytes of embedded json data.\n", length( $data ) );
	$data =~ s/^[[:blank:]\r\n]*APIDocs\.load\(//i;
	$data =~ s/\)\;[[:blank:]\r\n]*$//i;
	$data =~ s/\,([[:blank:]\r\n]*)\}$/$1\}/;
	$data =~ s/\b(lang|version|apiSections|flattenedAPISections|serviceWorkerPath|fuseWebWorkerPath|cacheControlVersion|showAllGatedContent|accountSwitcherData)\:/"$1":/g;
	$out->print( "Decoding json...\n" );
	my $j = JSON->new;
	## $j->filter_json_object(sub{ print( "Decoding:", @_, "\m" ); });
	my $jr = JSON::Relaxed::Parser->new;
	my $ref;
	try
	{
		## my $ref = $j->relaxed->decode( $data );
		$ref = $jr->parse( $data );
		$out->print( "Saving json to $cache_json\n" );
		my $fh = IO::File->new( ">$cache_json" ) || die( "$cache_json: $!\n" );
		$fh->binmode( ':utf8' );
		my $json_data = JSON->new->canonical->pretty->encode( $ref );
		$fh->print( $json_data ) || die( "$cache_json: $!\n" );
		$fh->close;
	}
	catch( $e )
	{
		$err->print( "An error occurred while decoding data: $e\nOriginal data is:\n$data\n" );
		if( $e =~ /at character offset (\d+)/ )
		{
			my $po = $1;
			$err->print( "Error is located around this:\n", substr( $data, $pos - 30, 20 ), "\n" );
		}
		exit( 1 );
	}
	$out->printf( "Decoded json contains %d keys at top level\n", scalar( keys( %$ref ) ) );
	if( exists( $ref->{apiSections} ) )
	{
		$out->print( "Found the apiSections section in json data" );
# 		if( ref( $ref->{apiSections} ) eq 'HASH' )
# 		{
# 			$out->printf( " and it is a hash reference with %d keys.\n", scalar( keys( %{$ref->{apiSections}} ) ) );
# 		}
# 		else
# 		{
# 			$out->print( " however, it is not a hash reference. Here is the data: ", Dumper( $ref ), "\n" );
# 			exit( 1 );
# 		}
	}
	return( $ref );
}

__END__

