#!/usr/bin/env perl

# Handle compile-time options before loading any modules
# Future::XS must be loaded before Future, so check early
BEGIN {
    my @to_splice;
    for my $i (0 .. $#ARGV) {
        if ($ARGV[$i] eq '--future-xs') {
            $ENV{PAGI_FUTURE_XS} = 1;
            push @to_splice, $i;
        }
        elsif ($ARGV[$i] eq '--http2') {
            $ENV{_PAGI_SERVER_HTTP2} = 1;
            push @to_splice, $i;
        }
    }
    # Remove processed flags in reverse order to preserve indices
    for my $i (reverse @to_splice) {
        splice @ARGV, $i, 1;
    }

    # Capture original args for hot restart re-exec (USR2)
    $ENV{PAGI_ARGV} = join("\0", @ARGV) unless exists $ENV{PAGI_ARGV};
}

use strict;
use warnings;

use Getopt::Long qw(GetOptionsFromArray :config pass_through no_auto_abbrev);
use PAGI::Runner;

# Parse PAGI::Server-specific options before passing to Runner
# This keeps Runner server-agnostic
my %server_opts;

GetOptionsFromArray(
    \@ARGV,
    # Network (Unix socket / multi-listener)
    'socket=s'              => \$server_opts{socket},
    'socket-mode=s'         => \$server_opts{_socket_mode},
    'listen=s@'             => \$server_opts{_listen},

    # Workers/scaling
    'w|workers=i'           => \$server_opts{workers},
    'reuseport'             => \$server_opts{reuseport},
    'max-requests=i'        => \$server_opts{max_requests},
    'max-connections=i'     => \$server_opts{max_connections},

    # TLS
    'ssl-cert=s'            => \$server_opts{_ssl_cert},
    'ssl-key=s'             => \$server_opts{_ssl_key},

    # Timeouts
    'timeout=i'             => \$server_opts{timeout},
    'shutdown-timeout=i'    => \$server_opts{shutdown_timeout},
    'request-timeout=i'     => \$server_opts{request_timeout},
    'ws-idle-timeout=i'     => \$server_opts{ws_idle_timeout},
    'sse-idle-timeout=i'    => \$server_opts{sse_idle_timeout},
    'heartbeat-timeout=i'   => \$server_opts{heartbeat_timeout},

    # Limits
    'max-body-size=i'       => \$server_opts{max_body_size},
    'max-header-size=i'     => \$server_opts{max_header_size},
    'max-header-count=i'    => \$server_opts{max_header_count},
    'max-receive-queue=i'   => \$server_opts{max_receive_queue},
    'max-ws-frame-size=i'   => \$server_opts{max_ws_frame_size},
    'b|listener-backlog=i'  => \$server_opts{listener_backlog},

    # Misc
    'log-level=s'           => \$server_opts{log_level},
    'sync-file-threshold=i' => \$server_opts{sync_file_threshold},
    'access-log-format=s'   => \$server_opts{access_log_format},
);

# Build ssl hash if certs provided
if ($server_opts{_ssl_cert} || $server_opts{_ssl_key}) {
    die "--ssl-cert and --ssl-key must be specified together\n"
        unless $server_opts{_ssl_cert} && $server_opts{_ssl_key};

    die "SSL cert not found: $server_opts{_ssl_cert}\n"
        unless -f $server_opts{_ssl_cert};
    die "SSL key not found: $server_opts{_ssl_key}\n"
        unless -f $server_opts{_ssl_key};

    $server_opts{ssl} = {
        cert_file => delete $server_opts{_ssl_cert},
        key_file  => delete $server_opts{_ssl_key},
    };
}
delete $server_opts{_ssl_cert};
delete $server_opts{_ssl_key};

# Handle --socket-mode (octal string to numeric)
if (defined $server_opts{_socket_mode}) {
    my $mode_str = $server_opts{_socket_mode};
    die "--socket-mode must be an octal value like 0660, got: $mode_str\n"
        unless $mode_str =~ /^0[0-7]{3}$/;
    $server_opts{socket_mode} = oct($mode_str);
}
delete $server_opts{_socket_mode};

# Handle --listen (build listen array from repeated --listen flags)
# Detection: contains ':' -> TCP host:port, otherwise -> Unix socket path
if (my $listen_specs = delete $server_opts{_listen}) {
    my @listeners;
    for my $spec (@$listen_specs) {
        if ($spec =~ m{^[./]}) {
            # Starts with / or . — always a Unix socket path
            # (even if it contains colons, e.g. /var/run/app:8080)
            my %entry = (socket => $spec);
            $entry{socket_mode} = $server_opts{socket_mode}
                if defined $server_opts{socket_mode};
            push @listeners, \%entry;
        } elsif ($spec =~ /^\[([^\]]+)\]:(\d+)$/) {
            # IPv6: [::1]:5000
            push @listeners, { host => $1, port => int($2) };
        } elsif ($spec =~ /^(.+):(\d+)$/) {
            # IPv4/hostname: 127.0.0.1:5000
            push @listeners, { host => $1, port => int($2) };
        } else {
            # Bare name without / or . prefix — treat as Unix socket
            my %entry = (socket => $spec);
            $entry{socket_mode} = $server_opts{socket_mode}
                if defined $server_opts{socket_mode};
            push @listeners, \%entry;
        }
    }
    $server_opts{listen} = \@listeners;
    delete $server_opts{socket_mode};  # Applied per-listener above
}

# Handle workers (0 for single-process, >1 for multi-worker)
if (defined $server_opts{workers}) {
    $server_opts{workers} = $server_opts{workers} > 1 ? $server_opts{workers} : 0;
}

# Remove undefined options
%server_opts = map { $_ => $server_opts{$_} } grep { defined $server_opts{$_} } keys %server_opts;

# Pass server options to Runner
PAGI::Runner->run(@ARGV, server_options => \%server_opts);

__END__

=encoding UTF-8

=head1 NAME

pagi-server - PAGI application server

=head1 SYNOPSIS

    pagi-server [options] [app] [key=value ...]

    # Serve current directory (default)
    pagi-server

    # Serve a specific directory
    pagi-server PAGI::App::Directory root=/var/www

    # Run a PAGI app file
    pagi-server ./app.pl

    # With environment modes
    pagi-server -E development app.pl  # Lint middleware enabled
    pagi-server -E production app.pl   # No auto-middleware

    # With options
    pagi-server -p 8080 PAGI::App::Directory root=/var/www
    pagi-server --workers 4 ./myapp.pl

=head1 DESCRIPTION

pagi-server is the command-line interface for running PAGI applications.
It uses L<PAGI::Runner> for application loading and server orchestration,
and L<PAGI::Server> as the default server backend.

=head1 ENVIRONMENT MODES

pagi-server supports environment modes similar to Plack's C<-E> flag:

=over 4

=item B<development>

Auto-enables L<PAGI::Middleware::Lint> with strict mode. This catches
specification violations early and provides helpful error messages.

This is the B<default when running interactively> (TTY detected).

=item B<production>

No middleware is auto-enabled, and access logging is disabled for maximum
performance. Use C<--access-log FILE> to enable logging in production.

This is the B<default when running non-interactively> (no TTY, e.g.,
systemd, docker, cron).

=item B<none>

Explicit opt-out of all auto-middleware, regardless of TTY detection.

=back

Mode is determined by (in order of precedence):

    1. -E / --env command line flag
    2. PAGI_ENV environment variable
    3. Auto-detection: TTY = development, no TTY = production

After determining the mode, C<pagi-server> sets C<PAGI_ENV> to the resolved
value. Your app can check C<$ENV{PAGI_ENV}> to know what mode it's running
in, similar to Plack's C<PLACK_ENV>.

=head1 OPTIONS

=head2 Common Options

These are handled by L<PAGI::Runner> and work with any server backend.

=over 4

=item B<-I>, B<--lib> PATH

Add PATH to C<@INC> before loading the app. Can be specified multiple
times, similar to C<perl -I>.

    pagi-server -I ./lib -I ./vendor/lib ./app.pl

=item B<-a>, B<--app> FILE

Path to .pl/.psgi file returning PAGI app coderef. Legacy option for
backward compatibility; you can also just pass the file as an argument.

=item B<-e> CODE

Inline app code, like C<perl -e>. Must return a PAGI app coderef.

    # Using a PAGI::App module
    pagi-server -MPAGI::App::File -e 'PAGI::App::File->new(root => ".")->to_app'

=item B<-M> MODULE

Load MODULE before evaluating C<-e> code, like C<perl -M>. Can be
specified multiple times. Supports C<Module=import,args> syntax.

    pagi-server -MPAGI::App::File -e 'PAGI::App::File->new(root => ".")->to_app'

=item B<-o>, B<--host> HOST

Bind address (default: 127.0.0.1, localhost only)

The default only accepts connections from localhost, which is B<secure by
default> for development. For remote access:

    pagi-server --host 0.0.0.0 ./app.pl    # All IPv4 interfaces

=item B<-p>, B<--port> PORT

Bind port (default: 5000)

=item B<-s>, B<--server> CLASS

Server class to use (default: PAGI::Server). Reserved for future use
with pluggable server backends.

=item B<-E>, B<--env> MODE

Environment mode: development, production, or none.

=item B<-l>, B<--loop> BACKEND

Event loop backend (Poll, EV, Epoll, UV). Default is auto-detect.

=item B<--access-log> FILE

Path to access log file (default: STDERR)

=item B<--no-access-log>

Disable access logging entirely. Improves throughput by 5-15%.

=item B<-D>, B<--daemonize>

Fork to background and run as a daemon.

=item B<--pid> FILE

Write the process ID to FILE.

=item B<--user> USER

After binding, drop privileges to run as USER. Requires root.

=item B<--group> GROUP

After binding, drop privileges to run as GROUP.

=item B<-q>, B<--quiet>

Suppress startup banner and development mode message.

=item B<--no-default-middleware>

Disable auto-middleware even in development mode.

=item B<-v>, B<--version>

Show version info.

=item B<--help>

Show help.

=back

=head2 PAGI::Server Options

These options are passed through to L<PAGI::Server>.

=over 4

=item B<-w>, B<--workers> NUM

Number of worker processes (default: 1, single-process mode)

=item B<--reuseport>

Enable SO_REUSEPORT mode for multi-worker servers.

=item B<--socket> PATH

Unix socket path. Mutually exclusive with C<--host>/C<--port>/C<--listen>.

    pagi-server --socket /tmp/pagi.sock ./app.pl

=item B<--socket-mode> MODE

Octal file permissions for Unix sockets (e.g., C<0660>).

    pagi-server --socket /tmp/pagi.sock --socket-mode 0660 ./app.pl

=item B<--listen> SPEC

Listen endpoint (repeatable). SPEC is C<host:port> for TCP or
C</path/to/socket> for Unix socket. Mutually exclusive with
C<--host>/C<--port>/C<--socket>.

Paths starting with C</> or C<.> are always treated as Unix socket paths,
even if they contain colons. For TCP, use C<host:port> format.

    pagi-server --listen 0.0.0.0:8080 --listen /tmp/pagi.sock ./app.pl

=item B<--ssl-cert> FILE, B<--ssl-key> FILE

Enable HTTPS with the specified certificate and key files.

=item B<--max-requests> NUM

Maximum requests per worker before restart (default: 0, unlimited)

=item B<--max-connections> NUM

Maximum concurrent connections per worker (default: auto-detect)

=item B<--max-body-size> NUM

Maximum request body size in bytes (default: 10MB)

=item B<--timeout> NUM

Connection idle timeout in seconds (default: 60)

=item B<--shutdown-timeout> NUM

Graceful shutdown timeout in seconds (default: 30)

=item B<--log-level> LEVEL

Log verbosity: debug, info, warn, error (default: info)

=item B<-b>, B<--listener-backlog> NUM

Listen queue size (default: 2048)

=item B<--max-receive-queue> NUM

Maximum WebSocket receive queue size (default: 1000)

=item B<--max-ws-frame-size> NUM

Maximum WebSocket frame size in bytes (default: 65536)

=item B<--request-timeout> NUM

Request stall timeout in seconds (default: 0, disabled)

=item B<--ws-idle-timeout> NUM

WebSocket idle timeout in seconds (default: 0, disabled)

=item B<--sse-idle-timeout> NUM

SSE idle timeout in seconds (default: 0, disabled)

=item B<--heartbeat-timeout> NUM

Worker heartbeat timeout in seconds (default: 50, 0 = disabled).
Only active in multi-worker mode. Workers send periodic heartbeats to
the parent via a Unix pipe. If a worker's event loop is blocked for
longer than this timeout (blocking syscall, deadlock, CPU-bound work),
the parent kills it with SIGKILL and respawns a replacement.

This should be larger than the maximum time you expect any single
operation to block the event loop. Async operations using C<await> do
not block the event loop and will never trigger this timeout.

=item B<--sync-file-threshold> NUM

Sync file read threshold in bytes (default: 65536)

=item B<--access-log-format> FORMAT

Access log format string or preset name (default: C<clf>).

Preset names: C<clf>, C<combined>, C<common>, C<tiny>.

Custom format strings use Apache-style atoms:

    --access-log-format combined
    --access-log-format '%h %s %Dms'

See L<PAGI::Server/ACCESS LOG FORMAT> for the full atoms table.

=item B<--future-xs>

Enable L<Future::XS> for improved Future performance.
Must be installed separately: C<cpanm Future::XS>

Can also be enabled via environment variable: C<PAGI_FUTURE_XS=1>

=item B<--http2>

Enable HTTP/2 support. Requires L<Net::HTTP2::nghttp2>.

When combined with C<--ssl-cert>/C<--ssl-key>, uses ALPN negotiation
(h2/http/1.1). Without TLS, enables h2c (cleartext HTTP/2) mode.

See L<PAGI::Server> for HTTP/2 protocol tuning options
(C<h2_max_concurrent_streams>, C<h2_initial_window_size>, etc.)
which can be passed via the constructor API.

=back

=head1 APP SPECIFICATION

=head2 Module Name

If the argument contains C<::>, it's treated as a Perl module name:

    pagi-server PAGI::App::Directory root=/var/www

The module must have C<new()> and C<to_app()> methods.

=head2 File Path

If the argument contains C</> or ends with C<.pl>/C<.psgi>:

    pagi-server ./app.pl

The file must return a PAGI app coderef.

=head2 Default

If no app is specified, serves the current directory:

    pagi-server
    # equivalent to: pagi-server PAGI::App::Directory root=.

=head1 SIGNAL HANDLING

=over 4

=item B<TERM>, B<INT>

Graceful shutdown.

=item B<HUP>

Graceful restart (multi-worker mode only).

=item B<TTIN>, B<TTOU>

Increase/decrease worker count (multi-worker mode only).

=item B<USR2>

Hot restart (multi-worker mode only). Fork and exec a new master process
that inherits listening sockets via C<PAGI_REUSE>. The new master spawns
workers and, once they are healthy, sends SIGTERM to the old master.
Zero-downtime deploy — both old and new masters serve requests during
the transition.

    # Deploy new code, then:
    kill -USR2 $(cat /var/run/myapp/pagi.pid)

Ignored in single-worker mode (logs a warning).

=back

=head1 EXAMPLES

    # Development (auto-detected from TTY)
    pagi-server ./app.pl

    # Explicit production mode
    pagi-server -E production ./app.pl
    PAGI_ENV=production pagi-server ./app.pl

    # Multi-worker production
    pagi-server --host 0.0.0.0 -p 80 --workers 4 -E production ./app.pl

    # With HTTPS
    pagi-server --ssl-cert cert.pem --ssl-key key.pem ./app.pl

    # Maximum performance
    pagi-server --no-access-log --workers 8 -E production ./app.pl

    # Using EV event loop
    pagi-server --loop EV ./app.pl

    # Unix socket for nginx
    pagi-server --socket /tmp/pagi.sock ./app.pl

    # Multi-listener: TCP + Unix socket
    pagi-server --listen 0.0.0.0:8080 --listen /tmp/pagi.sock ./app.pl

    # Multi-listener with workers
    pagi-server --listen 0.0.0.0:8080 --listen /tmp/pagi.sock -w 4 ./app.pl

=head1 ENVIRONMENT

=over 4

=item B<PAGI_ENV>

Default environment mode if C<-E> is not specified and no TTY detected.

=item B<IO_ASYNC_LOOP>

Preferred IO::Async loop backend.

=back

=head1 FUTURE::IO INTEGRATION

C<pagi-server> automatically configures L<Future::IO> to use the IO::Async
backend if L<Future::IO::Impl::IOAsync> is installed. This enables seamless
use of Future::IO-based libraries in your PAGI applications:

    # These just work under pagi-server:
    use Async::Redis;
    use Some::FutureIO::Database;

    # PAGI::SSE->every() also works:
    $sse->every(1, sub { $sse->send(data => time) });

In non-production mode, pagi-server will report when Future::IO is configured:

    Future::IO configured for IO::Async

If you're using L<PAGI::Server> programmatically (not via pagi-server), you
must configure Future::IO yourself. See L<PAGI::Server/"LOOP INTEROPERABILITY">
for details.

=head1 SEE ALSO

L<PAGI::Runner>, L<PAGI::Server>, L<PAGI::Middleware::Lint>, L<Future::IO>

=head1 AUTHOR

John Napiorkowski E<lt>jjnapiork@cpan.orgE<gt>

=head1 LICENSE

This software is licensed under the same terms as Perl itself.

=cut
