#!/usr/bin/env perl

use strict;
use warnings;

use URI;
use URI::QueryParam;

use HTTP::Daemon;
use HTTP::Response;
use HTTP::Request;
use HTTP::Status;

use LWP::UserAgent;

use JSON qw(decode_json encode_json);

use constant { DEBUG => 1 };

use constant {
    SID   => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
    TOKEN => 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
};

use constant {
    DOMAIN_API_SMS_SERVICE        => 'xxxxxx.pbxmanager.com',
    URI_API_SMS_SERVICE_INBOUND => '/sms/generic_xxxxxx/generic',
    URI_API_SMS_SERVICE_STATUS  => '/sms/generic_xxxxxx/generic/status',
};

my $httpd = HTTP::Daemon->new(
    LocalAddr => '127.0.0.1',
    LocalPort => 65533,
    ReusePort => 1,
) || die;

$| = 1;

printf "Upstream: %s\n", $httpd->url;

=pod

Generic local SMS gateway shim.

This script exposes local HTTP endpoints and translates requests to:
1) Twilio SMS API for outbound messages
2) Your upstream "generic SMS provider" API for inbound + status callbacks

send_sms(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, { From, To, Body, StatusCallback })

result: {
    "sid":"SMa92ab0323423423405901fbd987679d0f52",
    "subresource_uris":{"media":"/2026-04-01/Accounts/ACbe0600asasdasdasdcef71649eaa8ad5cb6a81/Messages/SMa92ab0323423423405901fbd987679d0f52/Media.json"},
    "date_sent":null,
    "status":"queued",
    "date_created":"Thu, 24 Mar 2026 15:37:25 +0000",
    "num_media":"0",
    "price_unit":"USD",
    "body":"Test message",
    "direction":"outbound-api",
    "to":"+18054444444",
    "from":"+14155555555",
    "num_segments":"1",
    "date_updated":"Thu, 24 Mar 2026 15:37:25 +0000",
    "messaging_service_sid":null,
    "error_code":null,
    "uri":"/2026-04-01/Accounts/ACbe0600asasdasdasdcef71649eaa8ad5cb6a81/Messages/SMa92ab0323423423405901fbd987679d0f52.json",
    "api_version":"2026-04-01",
    "error_message":null,
    "account_sid":"ACbe0600asasdasdasdcef71649eaa8ad5cb6a81",
    "price":null
}
=cut

sub send_sms {
    my ( $TWILIO_ACCOUNT_SID, $TWILIO_AUTH_TOKEN, $data ) = @_;

    printf "Sending SMS: %s\n", encode_json($data) if DEBUG;

    my $form = URI->new('http:');
    $form->query_form($data);

    my $req = HTTP::Request->new(
        POST => "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Messages.json",
        [ 'Content-Type' => 'application/x-www-form-urlencoded' ],
        $form->query()
    );

    $req->authorization_basic( $TWILIO_ACCOUNT_SID, $TWILIO_AUTH_TOKEN );

    my $ua = _new_http_client();

    my $res = $ua->request($req);

    if ( $res->is_success ) {
        printf "Sending SMS result: %s\n", $res->decoded_content if DEBUG;

        return decode_json( $res->decoded_content );
    }
    else {
        warn $res->status_line . "\n";
    }

    return undef;
}

=pod

handle_inbound({
+   'MessageSid' => 'SM310bf1afd08af812e8599d567b02bc97',
+   'From' => '+14155555555',
+   'To' => '+18054444444',
+   'Body' => '111',
+   'NumMedia' => '1', // < 0 ? MMS : SMS
*   'MediaContentType0' => 'image/jpeg',
*   'MediaUrl0' => 'https://api.twilio.com/2026-04-01/Accounts/ACbadadad/Messages/MM412331/Media/ME92asdasdd',
    'FromCountry' => 'US',
    'AccountSid' => 'ACbe0600b4a2aasdasdasdaa8ad5cb6a81',
    'FromCity' => '',
    'SmsStatus' => 'received',
    'SmsMessageSid' => 'SM310bf1afd08af812e8599d567b02bc97',
    'ApiVersion' => '2026-04-01',
    'NumSegments' => '1',
    'FromZip' => '',
    'SmsSid' => 'SM310bf1afd08af812e8599d567b02bc97',
    'ToCountry' => 'US',
    'ToState' => 'CA',
    'ToZip' => '',
    'MessagingServiceSid' => 'MG310bf1afd08af812e8599d567b02bc97',
    'ToCity' => '',
    'NumSegments' => '1',
    'ReferralNumMedia' => '0'
})

=cut

sub handle_inbound {
    my $data = shift;

    printf "Inbound:%s\n", encode_json($data);

    my $res = _post_form(
        "https://" . DOMAIN_API_SMS_SERVICE . URI_API_SMS_SERVICE_INBOUND,
        $data,
    );

    if ( $res->is_success ) {
        return $res->decoded_content;
    }
    else {
        warn $res->status_line . "\n";
    }

    return undef;
}

=pod

handle_status({
+   'MessageSid' => 'SM310bf1afd08af812e8599d567b02bc97',
+   'From' => '+14155555555',
+   'To' => '+18054444444',
+   'SmsStatus' => 'delivered' | 'undelivered' | 'queued' | 'failed' | 'sent',
    'AccountSid' => 'ACbe0600b4a2acefaad49eaa8ad5cb6a81',
    'MessageStatus' => 'delivered',
    'ApiVersion' => '2026-04-01',
    'RawDlrDoneDate' => '2306021231',
    'SmsSid' => 'SM310bf18aaaddf812e8599d567b02bc97'
})

=cut

sub handle_status {
    my $data = shift;

    printf "Status:%s\n", encode_json($data);

    my $res = _post_form(
        "https://" . DOMAIN_API_SMS_SERVICE . URI_API_SMS_SERVICE_STATUS,
        $data,
    );

    if ( $res->is_success ) {
        return $res->decoded_content;
    }
    else {
        warn $res->status_line . "\n";
    }

    return undef;
}

sub _new_http_client {
    my $ua = LWP::UserAgent->new;
    $ua->ssl_opts( verify_hostname => 0, SSL_verify_mode => 0x00 );
    return $ua;
}

sub _post_form {
    my ( $url, $data ) = @_;

    my $form = URI->new('http:');
    $form->query_form($data);

    my $req = HTTP::Request->new(
        POST => $url,
        [ 'Content-Type' => 'application/x-www-form-urlencoded' ],
        $form->query(),
    );

    return _new_http_client()->request($req);
}

#sub test_sms {
#    return send_sms(
#        SID, TOKEN,
#        {
#            From           => '+14155555555',
#            To             => '+18054444444',
#            Body           => 'Test message',
#            StatusCallback => 'https://' . DOMAIN_API_SMS_SERVICE . '/testsms/generic/status',
#        }
#    );
#}

sub sms {
    my ($data) = @_;
    return send_sms(
        SID,
        TOKEN,
        {
            From           => $data->{from},
            To             => $data->{to},
            Body           => $data->{body},
            ( $data->{mediaUrl} ? ( MediaUrl => $data->{mediaUrl} ) : () ),
            StatusCallback => 'https://' . DOMAIN_API_SMS_SERVICE . '/testsms/generic/status',
        },
    );
}

sub debug_req {
    my $r = shift;
    printf "=" x 80 . "\n";
    printf "Request: (%s) %s\n", $r->method, $r->uri->path;
    printf "Query: %s\n", encode_json( $r->uri->query_form_hash() );

    printf "Headers:\n";
    printf "-" x 80 . "\n";
    printf "\t%s: %s\n", $_, $r->header($_) for keys %{ $r->headers };
    printf "-" x 80 . "\n";

    if ( $r->content ) {
        printf "Body:\n";
        printf "-" x 80 . "\n";

        if ( $r->header('content-type') =~ m{www\-form\-urlencoded} ) {
            my $uri = URI->new('http:');

            $uri->query( $r->content );

            printf "Form:%s\n", encode_json( $uri->query_form_hash() );
        }
        else {
            printf "Raw:%s\n", $r->content;
        }

        printf "-" x 80 . "\n";
    }
}

while ( my $client = $httpd->accept ) {
    while ( my $req = $client->get_request ) {
        debug_req( $req ) if DEBUG;

        my $uri = URI->new('http:');
        $uri->query( $req->content )
          if $req->header('content-type') =~ m{www\-form\-urlencoded};

        my $res = HTTP::Response->new(200);

        if ( $req->uri->path eq '/testsms/generic/send' ) {
            $res->content( encode_json( sms( $uri->query_form_hash() ) ) );
        }
        elsif ( $req->uri->path eq '/testsms/generic' ) {
            handle_inbound( $uri->query_form_hash() );
        }
        elsif ( $req->uri->path eq '/testsms/generic/status' ) {
            handle_status( $uri->query_form_hash() );
        }
        else {
            $res->content('custom sms gateway httpd service');
        }

        $client->send_response( $res );
    }

    $client->close;

    undef $client;
}
