#!/usr/bin/env perl

package Amazon::API::Botocore;

use strict;
use warnings;

BEGIN {
  use English qw(-no_match_vars);

  eval { require Log::Log4perl; };

  if ($EVAL_ERROR) {
    no strict qw(refs);  ## no critic (ProhibitNoStrict)

    for (qw(DEBUG INFO WARN ERROR FATAL)) {
      *{ __PACKAGE__ . "::$_" } = sub { };
    }
  }
  else {
    no warnings;         ## no critic (ProhibitNoWarnings)

    require Log::Log4perl::Level;

    Log::Log4perl::Level->import(__PACKAGE__);
    Log::Log4perl->import(qw(:easy));
  }
}

use parent qw( Exporter );

use Amazon::API::Botocore::Shape::Utils qw(require_class create_shape create_module_name);

use Amazon::API::Template qw(:all);
use Amazon::API::Pod::Parser qw(get_pod_section);

use Carp;
use Carp::Always;
use Cwd;
use Data::Dumper;
use English qw( -no_match_vars );
use File::Find;
use File::Path qw(make_path);
use Getopt::Long qw(:config no_ignore_case);
use JSON;
use List::MoreUtils qw( first_index );
use List::Util qw( max );
use Pod::Usage;
use Pod::Text;
use Readonly;
use charnames qw(:full);

# package constants
Readonly::Scalar our $BOTO_PATH_OFFSET       => 3;
Readonly::Scalar our $SHAPE_FILE_TEMPLATE    => '%sAmazon/API/Botocore/Shape/%s/';
Readonly::Scalar our $REQUEST_CLASS_TEMPLATE => 'Amazon::API::Botocore::Shape::%s::%sRequest';
Readonly::Scalar our $STRINGIFY              => 'Dumper';

use Amazon::API::Constants qw( :all );
use Amazon::API::Botocore::Pod qw( pod );

our $VERSION = '2.1.4';  ## no critic (RequireInterpolationOfMetachars)

our %BOTO_SERVICES;

our $TEMPLATE_START = tell DATA;

our @EXPORT_OK = qw(
  %BOTO_SERVICES
  create_module_name
  create_service_shapes
  fetch_boto_services
  get_service_descriptions
  paginator
);

our %EXPORT_TAGS = (
  all => [
    qw(
      create_service_shapes
      fetch_boto_services
      get_service_descriptions
      %BOTO_SERVICES)
  ],
);

caller or __PACKAGE__->main;

########################################################################
sub paginator {
########################################################################
  my (%options) = @_;

  my ( $service, $api, $parameters ) = @options{qw(service api parameters)};

  $parameters //= {};

  croak "no service\n"
    if !$service && ref($service) =~ /^Amazon::API::/xsm;

  croak "no api name\n"
    if !$api;

  my $service_name = ref $service;

  if ( $service_name =~ /^Amazon::API::([^:]+)$/xsm ) {
    $service_name = $1;
  }
  else {
    croak "could not determine service name\n";
  }

  my $request_class = sprintf $REQUEST_CLASS_TEMPLATE, $service_name, $api;

  my $ret = eval { require_class $request_class; };

  croak "unable to find a request class: $request_class\n"
    if !$ret || $EVAL_ERROR;

  my $paginator = $service->get_paginators->{$api};

  my $input_token  = $paginator->{input_token};
  my $more_results = $paginator->{more_results};
  my $limit_key    = $paginator->{limit_key};
  my $output_token = $paginator->{output_token};
  my $result_key   = $paginator->{result_key};

  my $request = $request_class->new($parameters);

  my @response;

  $more_results //= $output_token;

  while ( my $rsp = $service->$api($request) ) {

    push @response, @{ $rsp->{$result_key} };

    last if !$rsp->{$more_results};

    my $limit = $rsp->{$limit_key};

    $request = $request_class->new(
      { $limit ? ( $limit_key => $limit ) : (), $input_token => $rsp->{$output_token} } );
  }

  return \@response;
}

########################################################################
sub parse_request_uri {
########################################################################
  my ($uri) = @_;

  my ( $path, $query_string ) = split /\N{QUESTION MARK}/xsm, $uri;

  my @path_parts = split /\N{SOLIDUS}/xsm, $path;

  my @path_args;

  foreach my $idx ( 1 .. $#path_parts + 1 ) {
    if ( $path_parts[$idx] && $path_parts[$idx] =~ /^[{](.*)[}]$/xsm ) {
      push @path_args, ucfirst $1;
      $path_parts[$idx] = '%s';
    }
  }

  my $request_uri = join $SLASH, @path_parts;

  if ($query_string) {
    $request_uri .= $QUESTION_MARK . $query_string;
  }

  return ( $request_uri || $uri, \@path_args );
}

########################################################################
sub fetch_json_file {
########################################################################
  my ($path) = @_;

  my $json;

  open my $fh, '<', $path
    or croak 'could not open ' . $path;

  {
    local $RS = undef;
    $json = JSON->new->utf8->decode(<$fh>);
  }

  close $fh
    or croak 'could not close ' . $path;

  return $json;
}

########################################################################
sub fetch_service_description { goto &fetch_json_file; }
sub fetch_paginators          { goto &fetch_json_file; }
########################################################################

########################################################################
sub create_service_shapes {
########################################################################
  my (%options) = @_;

  my ( $service, $boto_path, $shape_path, $module_name )
    = @options{qw(service botocore-path output-path module-name)};

  fetch_boto_services($boto_path);

  my $service_description
    = get_service_descriptions($service)->[0]->{$service};

  if ( $shape_path && $shape_path !~ /\/\z/xsm ) {
    $shape_path = "$shape_path/";

    $shape_path = sprintf $SHAPE_FILE_TEMPLATE, $shape_path, $module_name;

    if ( !-d $shape_path ) {
      croak "could not create $shape_path"
        if !make_path $shape_path;
    }
  }

  my $shapes = $service_description->{shapes};

  my $count = 0;

  foreach my $shape_name ( keys %{$shapes} ) {
    my $class = create_shape(
      name                => $shape_name,
      service_description => $service_description,
      service             => $module_name
    );

    if ($shape_path) {
      my $module = sprintf '%s%s.pm', $shape_path, $shape_name;

      open my $fh, '>', $module
        or croak "could not open $module for writing";

      binmode $fh, 'encoding(UTF-8)';

      print {$fh} $class;

      close $fh;
    }
    else {
      print {*STDOUT} $class;
    }

    $count++;
  }

  return $count;
}

# File::Find callback - collect paths to most recent service-2.json
# files for all AWS services
########################################################################
sub find_latest_services {
########################################################################
  my $file = $_;
  my $dir  = $File::Find::name;

  return if $dir  !~ qr/botocore\N{SOLIDUS}botocore/xsm;
  return if $file !~ qr/service\N{HYPHEN-MINUS}2\N{FULL STOP}json/xsm;

  my (@path) = split /\N{SOLIDUS}/xsm, $dir;

  my $boto_path = first_index {/botocore/xsm} @path;

  # this should not happen...
  if ( $boto_path < 0 ) {
    croak 'no botocore in path ' . $dir;
  }

  $boto_path += $BOTO_PATH_OFFSET;

  my ( $service, $date ) = @path[ $boto_path, $boto_path + 1, ];

  $BOTO_SERVICES{$service}->{date} = $BOTO_SERVICES{$service}->{date} // $EMPTY;

  if ( $date gt $BOTO_SERVICES{$service}->{date} ) {
    $BOTO_SERVICES{$service} = {
      date => $date,
      path => \@path
    };
  }

  return $file;
}

########################################################################
sub render_stub {
########################################################################
  my (%args) = @_;

  my %options = %{ $args{options} };

  my $service    = $args{service};
  my $template   = $args{template};
  my $parameters = $args{parameters};

  my $operations = $parameters->{operations};
  my $shapes     = $parameters->{shapes};
  my $metadata   = $parameters->{metadata};

  my $paginators = $parameters->{paginators};

  if ( $parameters->{paginators} ) {
    $paginators = $paginators->{pagination};
  }

  $parameters->{ to_template_var('program_name') }    = $PROGRAM_NAME;
  $parameters->{ to_template_var('program_version') } = $VERSION;
  $parameters->{ to_template_var('timestamp') }       = scalar localtime;
  $parameters->{ to_template_var('end') }             = '__END__';
  $parameters->{ to_template_var('description') }
    = $metadata->{serviceFullName};

  $parameters->{ to_template_var('metadata') }
    = stringify( $parameters->{metadata} );

  my %methods;
  my @errors;

  foreach my $m ( keys %{$operations} ) {
    my %operation = %{ $operations->{$m} };

    my $documentation;

    if ( $options{pod} ) {
      $documentation = html2pod( $operation{documentation} // $EMPTY );
    }

    $methods{$m} = {
      documentation => $documentation,
      input         => $operation{input}->{shape},
      output        => $operation{output}->{shape},
      http          => $operation{http},
      errors        => [ map { $_->{shape} } @{ $operation{errors} } ],
    };

    delete $operations->{$m}->{documentation};
  }

  my @pod;

  foreach my $method ( sort keys %methods ) {
    my $input  = $methods{$method}->{input}  // $EMPTY;
    my $output = $methods{$method}->{output} // $EMPTY;
    my $errors = $methods{$method}->{errors} // $EMPTY;

    if ( $options{pod} ) {
      my $documentation = $EMPTY;

      $documentation = $methods{$method}->{documentation} // $EMPTY;

      $documentation =~ s/\A\n+//xsm;
      $documentation =~ s/\n+\z//xsm;

      if ($input) {
        my $input_shape = $shapes->{$input};
        local $LIST_SEPARATOR = "\n\n";

        my @items
          = map { sprintf '=item %s', $_ } @{ $input_shape->{required} };
        my $required = "@items\n";

        my $members       = $EMPTY;
        my %shape_members = %{ $input_shape->{members} };

        foreach my $m ( sort keys %shape_members ) {
          my $location = $shape_members{$m}->{locationName} || $m;
          $members .= sprintf "\n=item %s\n", $m;

          $members .= html2pod $shape_members{$m}->{documentation};
        }

        $required ||= 'None';

        $input = <<"INPUT";

=over 5

=item $input

=over 5

=item Parameters

=over 5

$members

=back

=item Required

=over 5

$required

=back

=back

=back

INPUT
      }

      if ($output) {
        my $members      = $EMPTY;
        my $output_shape = $shapes->{$output};

        my %shape_members = %{ $output_shape->{members} // {} };

        foreach my $m ( sort keys %shape_members ) {
          $members .= sprintf "\n=item %s\n", $m;
          $members .= html2pod $shape_members{$m}->{documentation};
        }

        $output = <<"OUTPUT";

=over 5

=item $output

=over 5

=item Parameters

=over 5

$members

=back

=back

=back

OUTPUT
      }

      my @error_items;

      if ( @{$errors} ) {
        foreach my $e ( @{$errors} ) {
          my $shape = $shapes->{$e};
          push @error_items, '=over 5';

          push @error_items, sprintf "=item %s\n%s", $e, html2pod $shape->{documentation};

          if ( $shape->{error} ) {
            push @error_items, '=over 5';

            foreach my $k ( sort keys %{ $shape->{error} } ) {
              push @error_items, sprintf "=item %s\n\n%s", $k, $shape->{error}->{$k};
            }

            push @error_items, "=back\n";
          }

          push @error_items, "=back\n";
        }

        local $LIST_SEPARATOR = "\n\n";

        my $error_str = "@error_items";

        $errors = <<"ERRORS";

$error_str

ERRORS
      }
      else {
        $errors = $EMPTY;
      }

      my $none = "=over 5\n\n=item NONE\n\n=back\n\n";

      $output = $output || $none;
      $errors = $errors || $none;
      $input  = $input  || $none;

      my $method_pod = <<'END_OF_POD';

=head2 @method@

@documentation@

=over 5

=item INPUT

@input@

=item OUTPUT

@output@

=item ERRORS

@errors@

=back

END_OF_POD

      my $http = $methods{$method}->{http};

      my $http_method = $EMPTY;
      my $request_uri = $EMPTY;

      if ($http) {
        $http_method = $http->{method}     // $EMPTY;
        $request_uri = $http->{requestUri} // $EMPTY;

        my ( $request_uri_tpl, $args ) = parse_request_uri($request_uri);

        $operations->{$method}->{http}->{parsed_request_uri}
          = { request_uri_tpl => $request_uri_tpl, parameters => $args };

        $method_pod .= <<'END_OF_POD';

=over 5

=item METHOD

@http_method@

=item REQUEST URI

@request_uri@
END_OF_POD
      }

      $method_pod .= <<'END_OF_POD';

=back

END_OF_POD

      my @see_also;

      push @see_also, $methods{$method}->{input}  || ();
      push @see_also, $methods{$method}->{output} || ();
      push @see_also, map { $_->{shape} } @{ $operations->{$method}->{errors} };

      $parameters->{ to_template_var('see_also') } = join "\n",
        map { sprintf 'L<%s::%s>', $parameters->{ to_template_var('package_name') }, $_ } @see_also;

      $parameters->{ to_template_var('method') }      = $method;
      $parameters->{ to_template_var('errors') }      = $errors;
      $parameters->{ to_template_var('input') }       = $input;
      $parameters->{ to_template_var('output') }      = $output;
      $parameters->{ to_template_var('http_method') } = $http_method;
      $parameters->{ to_template_var('request_uri') } = $request_uri;

      $parameters->{ to_template_var('documentation') } = $documentation;

      my $package_name = $parameters->{ to_template_var('package_name') };
      $package_name =~ s/::/\//gxsm;

      push @pod, render_template $method_pod, $parameters;

      my $pod_stub = <<'END_OF_POD';
=pod

=encoding utf8

=head1 NAME

@method@

=head1 SYNOPSIS

 my $service = @package_name@->new;
 my $rsp = $service->@method@($parameters);

=head1 DESCRIPTION

@documentation@

=head1 PARAMETERS

=head2 INPUT

@input@

=head2 OUTPUT

@output@

=head2 ERRORS

@errors@

=head1 NOTES

=over 5

=item * Method: @http_method@

=item * Request URI: @request_uri@

=back

=head1 SEE ALSO

@see_also@

=head1 AUTHOR

Autogenerate by @program_name@ on @timestamp@

=head1 LICENSE AND COPYRIGHT

This module is free software it may be used, redistributed and/or
modified under the same terms as Perl itself.

=cut

1;
END_OF_POD

      # NOTE: this is JUST pod, so if you don't provide an output path,
      # you won't get the method pod
      if ( $options{'output-path'} ) {
        my $method_pod_file = sprintf '%s/%s/%s.pm', $options{'output-path'}, $package_name, $method;

        my $template = render_template( $pod_stub, $parameters );

        if ( $options{'output-path'} ne $DASH ) {
          open my $fh, '>', $method_pod_file
            or croak "could not open $method_pod_file for writing";

          print {$fh} $template;

          close $fh;
        }
        else {
          print {*STDOUT} $template;
        }
      }
    }

    $parameters->{ to_template_var('methods') } = join "\n", @pod;
  }

  $parameters->{ to_template_var('operations') } = stringify($operations);

  $parameters->{ to_template_var('shapes') } = stringify($shapes);

  $parameters->{ to_template_var('paginators') } = stringify($paginators);

  if ( !$options{pod} ) {
    $template =~ s/^\@end.*\z//xsm;
  }

  return render_template( $template, $parameters );
}

########################################################################
sub stringify {
########################################################################
  my ($object) = @_;

  return $object
    if !ref $object;

  local $Data::Dumper::Terse    = $TRUE;
  local $Data::Dumper::Deepcopy = $TRUE;

  return Dumper($object)
    if $STRINGIFY eq 'Dumper';

  return JSON->new->utf8->encode($object);
}

########################################################################
sub get_api_descriptions {
########################################################################
  goto &get_service_descriptions;
}

########################################################################
sub get_service_descriptions {
########################################################################
  my @services = @_;

  my @descriptions;

  if ( !@services ) {
    @services = sort keys %BOTO_SERVICES;
  }

  foreach my $s ( map {lc} @services ) {

    croak "no such service: $s\n"
      if !$BOTO_SERVICES{$s};

    my @path = @{ $BOTO_SERVICES{$s}->{path} };

    my $boto_path = first_index {/botocore/xsm} @path;

    if ( $boto_path < 0 ) {
      croak 'no botocore in path ' . $BOTO_SERVICES{$s}->{path};
    }

    my $service_path = join $SLASH, @path[ 0 .. $boto_path + 2 ], $s, $BOTO_SERVICES{$s}->{date};

    my $service_file    = "$service_path/service-2.json";
    my $paginators_file = "$service_path/paginators-1.json";

    my $paginators = eval { return fetch_paginators($paginators_file); };

    my $service_description = fetch_service_description($service_file);

    if ( $service_description->{operations} ) {
      my $operations = $service_description->{operations};
      my $shapes     = $service_description->{shapes};
      my $metadata   = $service_description->{metadata};

      my $service_name = $metadata->{signingName} || $metadata->{endpointPrefix};

      push @descriptions,
        {
        $s => {
          actions         => [ keys %{$operations} ],
          documentation   => $service_description->{documentation},
          endpoint_prefix => $metadata->{endpointPrefix},
          json_version    => $metadata->{jsonVersion},
          metadata        => $metadata,
          metadata_keys   => [ keys %{$metadata} ],
          operations      => $operations,
          paginators      => $paginators,
          protocol        => $metadata->{protocol},
          service_name    => $service_name,
          shapes          => $shapes,
          target_prefix   => $metadata->{targetPrefix},
          version         => $BOTO_SERVICES{$s}->{date},
        }
        };
    }
  }

  return \@descriptions;
}

########################################################################
sub fetch_boto_services {
########################################################################
  my ($path) = @_;

  if ( !-d "$path/botocore" ) {
    croak <<"END_OF_CROAKING";
!!! No $path/botocore directory found.

In order to create stubs or shapes you must clone the Botocore project
and provide the path to the project using the -b option or by setting
the environment variable BOTOCORE_PATH.

  git clone https://github.com/boto/botocore.git /tmp/botocore
  export BOTOCORE_PATH=/tmp/botocore

END_OF_CROAKING
  }

  find( { wanted => \&find_latest_services, follow => $TRUE }, $path );

  if ( !keys %BOTO_SERVICES ) {
    croak 'no services found in path ' . $path;
  }

  return keys %BOTO_SERVICES;
}

########################################################################
sub extra_args {
########################################################################
  my (%options) = @_;

  return shift @{ $options{'extra-args'} };
}

########################################################################
sub dump_service {
########################################################################
  my (%options) = @_;

  fetch_boto_services( $options{'botocore-path'} );

  my $service = extra_args(%options) // $options{service};

  croak 'no service specified'
    if !$service;

  my @services = $service eq 'all' ? keys %BOTO_SERVICES : $service;

  my $description = get_api_descriptions(@services);

  print JSON->new->pretty->encode( $description->[0] );

  return $TRUE;
}

########################################################################
sub create_stub {
########################################################################
  my (%options) = @_;

  fetch_boto_services( $options{'botocore-path'} );

  my ( $service, $module_name ) = @options{qw( service module-name)};

  $service = lc $service;

  my $package_name = sprintf 'Amazon::API::%s', $module_name;

  croak 'no service specified'
    if !$service;

  my $description = get_api_descriptions($service);

  my $parameters = $description->[0]->{$service};
  $parameters->{package_name} = $package_name;

  my @actions = @{ $parameters->{actions} };

  $parameters->{actions} = $PADDING . join "\n    ", sort @actions;

  if ( $parameters->{protocol} eq 'rest-json' ) {
    foreach (@actions) {
    }
  }

  $parameters->{service}
    = $parameters->{service_name} || $parameters->{endpoint_prefix};

  if ( $parameters->{protocol} eq 'query' ) {
    $parameters->{content_type} = 'application/x-www-form-urlencoded';
  }

  if ( $parameters->{protocol} eq 'json' ) {
    $parameters->{content_type} = 'application/x-amz-json-' . $parameters->{json_version};
  }

  # for rest-json protocol we need a method and and a query uri in
  # addition to the payload
  if ( $parameters->{protocol} eq 'rest-json' ) {
    $parameters->{content_type} = 'application/json';
  }

  my @template_vars = qw(
    actions
    botocore_metadata
    botocore_operation
    content_type
    endpoint_prefix
    package_name
    protocol
    service
    target_prefix
    version
  );

  foreach my $var (@template_vars) {
    $parameters->{ to_template_var($var) } = $parameters->{$var};
  }

  my $module = render_stub(
    service    => $service,
    template   => fetch_template( *DATA, $TEMPLATE_START ),
    parameters => $parameters,
    options    => \%options,
  );

  if ( $options{tidy} && eval { require Perl::Tidy; } ) {

    print {*STDERR} "tidying module...this may take a while\n";

    my $tidy_module = $EMPTY;

    if (
      Perl::Tidy::perltidy(
        argv        => [],
        source      => \$module,
        destination => \$tidy_module,
      )
    ) {
      croak 'could not tidy module!';
    }

    $module = $tidy_module;
  }

  my $file = $options{file};

  if ( !$file && $options{'output-path'} ) {
    my $path = sprintf '%s/Amazon/API', $options{'output-path'};

    if ( !-d $path ) {
      croak "could not create $path"
        if !make_path $path;
    }

    $file = sprintf '%s/%s.pm', $path, $module_name;

    if ( -e $file ) {
      rename $file, "$file.bak";
    }
  }

  my $fh = eval {
    if ($file) {
      open my $handle, '>', $file
        or croak 'could not open ' . $file;

      return $handle;
    }
    else {
      return *STDOUT;
    }
  };

  print {$fh} $module;

  close $fh
    or croak 'could close file';

  return $TRUE;
}

########################################################################
sub format_columns {
########################################################################
  my (%args) = @_;

  my $text = $args{text};

  my $padding      = $args{padding} // 2;
  my $column_width = $args{'column-width'};

  my $indent = $args{indent} ? $SPACE x $args{indent} : $EMPTY;

  my $max_width = max map {length} @{$text};

  my $width = $args{width};  # width of canvas

  # format for screen by default
  if ( !$width ) {
    require Term::ReadKey;

    Term::ReadKey->import('GetTerminalSize');

    ($width) = eval { GetTerminalSize() };
    $width //= 80;
  }

  if ( !$column_width ) {
    $column_width = 2 + $max_width;
  }

  my $columns = int $width / $column_width;

  my @formatted_text = map { sprintf "%-${column_width}s", $_; } @{$text};

  my $output = $EMPTY;

  while (@formatted_text) {

    $output .= sprintf "%s%s\n", $indent, join $EMPTY, grep {defined} @formatted_text[ ( 0 .. $columns - 1 ) ];

    for ( 0 .. $columns - 1 ) {
      shift @formatted_text;
    }
  }

  return $output;
}

########################################################################
sub help {
########################################################################
  my (%options) = @_;

  my (@args) = @ARGV;

  my $token;

  return pod
    if !@args && !$options{service};

  my $service = $options{service};

  my $module;

  if ( @args == 2 ) {
    ( $service, $module ) = @args;
  }
  elsif ( @args == 1 && $service ) {
    $module = $args[0];
  }
  else {
    fetch_boto_services( $options{'botocore-path'} );

    $options{service} //= 'all';

    if ( $options{service} eq 'all' ) {
      return print {*STDOUT} format_columns( text => [ sort keys %BOTO_SERVICES ] );
    }
    else {
      my $description = get_service_descriptions( lc $options{service} );

      my $operations
        = $description->[0]->{ lc $options{service} }->{operations};

      my $documentation
        = $description->[0]->{ lc $options{service} }->{documentation};

      $documentation = html2pod $documentation ;

      my $available_commands
        = format_columns( indent => 1, text => [ sort keys %{$operations} ] );

      my %parameters = (
        to_template_var('service')            => $options{service},
        to_template_var('documentation')      => $documentation,
        to_template_var('available_commands') => $available_commands,
      );

      my $help_text = <<'HELP';
=pod

=encoding utf8

=head1 NAME

@service@

=head1 DESCRIPTION

@documentation@

=head1 AVAILABLE COMMANDS

@available_commands@

=cut
HELP
      my $pod = render_template( $help_text, \%parameters );

      my $pod_parser = Pod::Text->new;

      # yes, this outputs to STDOUT
      return $pod_parser->parse_string_document($pod);
    }
  }

  croak 'no service specified'
    if !$service;

  my $service_name = create_module_name($service);

  my $file;

  # try -s service or
  for ( $service_name, $service ) {
    my $class = sprintf 'Amazon::API::Botocore::Shape::%s::%s', $_, $module;

    $file = require_class($class);

    last if $file;
  }

  if ( !$file || !-e $file ) {
    $file = require_class( sprintf 'Amazon::API::%s::%s', $service_name, $module );
  }

  $module = undef;

  croak "no pod available\n"
    if !$file || !-e $file;

  if ( $options{pager} ) {
    $token = eval {
      require IO::Pager;

      IO::Pager::open( *STDOUT, '|-:utf8', 'Unbuffered' );
    };
  }

  my $pod = get_pod_section $file;

  print {*STDOUT} "$pod\n";

  return $EMPTY;
}

########################################################################
sub main {
########################################################################
  my %options = (
    'botocore-path' => $ENV{BOTOCORE_PATH} || getcwd,
    tidy            => $FALSE,
    pod             => $TRUE,
    pager           => $TRUE,
    'output-path'   => getcwd,
  );

  if ( $ENV{DEBUG} ) {
    print {*STDERR} Dumper( \@ARGV );
  }

  my @option_defs = qw(
    botocore-path|b=s
    help|h
    module-name|m=s
    output-path|o=s
    service|s=s
    tidy|t!
    pod|p!
    pager|P!
  );

  GetOptions( \%options, @option_defs );

  $options{command}      = shift @ARGV;
  $options{'extra-args'} = \@ARGV;

  if ( $options{help} || !$options{command} ) {
    $options{command} = 'help';
  }

  if ( !$options{'module-name'} && $options{service} ) {
    $options{'module-name'} = create_module_name( $options{service} );
  }

  if ( $options{'output-path'} eq $DASH ) {
    delete $options{'output-path'};
  }

  if ( $options{command} ne 'help' ) {
    if ( $options{'output-path'} && $options{'output-path'} =~ /^[.]/xsm ) {
      my $cwd = getcwd;

      $options{'output-path'} =~ s/^[.]/$cwd/xsm;
    }
    elsif ( $options{'output-path'} && $options{'module-name'} ) {
      my $module_path = sprintf '%s/Amazon/API/%s', @options{qw(output-path module-name)};

      if ( !-d $module_path ) {
        croak "could not create path: $module_path\n"
          if !make_path $module_path;
      }
    }
  }

  my %handlers = (
    'dump-service'  => \&dump_service,
    'dump'          => \&dump_service,
    'describe'      => \&dump_service,
    'create-stub'   => \&create_stub,
    'create-stubs'  => \&create_stub,
    'create-shapes' => \&create_service_shapes,
    'create-shape'  => \&create_service_shapes,
    'help'          => \&help,
  );

  croak sprintf 'not a valid command [%s]', $options{command}
    if !$handlers{ $options{command} };

  exit !$handlers{ $options{command} }->(%options);
}

1;

__DATA__

package @package_name@;

# Autogenerated by @program_name@ @program_version@ at @timestamp@

use strict;
use warnings;

use parent qw( Amazon::API );

our @API_METHODS = qw(
@actions@
);

our $VERSION = '2.1.4';

sub new {
  my ( $class, @options ) = @_;
  $class = ref($class) || $class;

  my %options = ref $options[0] ? %{ $options[0] } : @options;

  my $self = $class->SUPER::new(
    { service             => '@service@',
      endpoint_prefix     => '@endpoint_prefix@',
      version             => '@version@',
      target_prefix       => '@target_prefix@',
      api_methods         => \@API_METHODS,
      content_type        => '@content_type@',
      botocore_metadata   => @metadata@,
      botocore_operations => @operations@,
      botocore_shapes     => @shapes@,
      debug               => $ENV{DEBUG} // 0,
      decode_always       => 1,
      paginators          => @paginators@,
      %options
    }
  );

  # global services should be signed with us-east-1 region
  if ( defined $self->get_botocore_metadata->{globalEndpoint} ) {
    $self->set_region('us-east-1');
  }
  
  return $self;
} 

1;

@end@

=pod

=encoding utf8

=head1 NAME

@package_name@

=head1 DESCRIPTION

@description@

=head1 VERSION

Version @program_version@

=head1 METHODS AND SUBROUTINES

@methods@

=head1 NOTES

Autogenerated by @program_name@ at @timestamp@

=head1 LICENSE AND COPYRIGHT

This module is free software it may be used, redistributed and/or
modified under the same terms as Perl itself.

=cut  
