package Net::OpenVPN::Manager::Handler;

use namespace::autoclean;
use Moose;

use AnyEvent::Handle;
use Net::OpenVPN::Manager::Client;

has 'address' => (
    is => 'ro',
    isa => 'Str',
    required => 1,
);

has 'port' => (
    is => 'ro',
    isa => 'Int',
    required => 1,
);

has 'on_client_connect' => (
    is => 'ro',
    isa => 'CodeRef',
    required => 1,
);

has 'on_client_reauth' => (
    is => 'ro',
    isa => 'CodeRef',
    required => 1,
);

has 'on_client_challenge' => (
    is => 'ro',
    isa => 'CodeRef',
    default => sub { sub {} },
);

has 'on_client_address' => (
    is => 'ro',
    isa => 'CodeRef',
    default => sub { sub {} },
);

has 'on_client_established' => (
    is => 'ro',
    isa => 'CodeRef',
    default => sub { sub {} },
);

has 'on_client_disconnected' => (
    is => 'ro',
    isa => 'CodeRef',
    default => sub { sub {} },
);

has 'on_info' => (
    is => 'ro',
    isa => 'CodeRef',
    default => sub { sub {} },
);

has 'on_status' => (
    is => 'ro',
    isa => 'CodeRef',
    default => sub { sub {} },
);

has 'clients' => (
    traits => ['Hash'],
    is => 'ro',
    isa => 'HashRef[Net::OpenVPN::Manager::Client]',
    default => sub { {} },
    handles => {
        all_clients => 'values',
        set_client => 'set',
        get_client => 'get',
        has_client => 'exists',
        del_client => 'delete',
    },
);

has 'handle' => (
    is => 'ro',
    isa => 'AnyEvent::Handle',
    lazy => 1,
    builder => '_handle_builder',
);

sub _handle_builder {
    my $self = shift;

    return AnyEvent::Handle->new(
        connect => [$self->address => $self->port],
        on_error => sub { print __PACKAGE__." error\n"; },
        on_eof => sub {
            print "eof\n";
            $self->handle->destroy;
        },
    );
}

sub start {
    my $self = shift;

    $self->handle->push_read(line => sub {
        $self->_on_line(@_);
    });
}

sub _on_line {
    my ($self, $handle, $line) = @_;

    # next line
    $handle->push_read(line => sub {
        $self->_on_line(@_);
    });

    if ($line =~ />INFO:(.*)$/) {
        my $info = $1;
        $self->on_info->($self, $info);
    } elsif ($line =~ />CLIENT:CONNECT,(\d+),(\d+)$/) {
        my $client = Net::OpenVPN::Manager::Client->new(cid => $1, kid => $2);
        $handle->unshift_read(line => sub {
            $self->_handle_env($_[0], $_[1], $client);
        });
    } elsif ($line =~ />CLIENT:REAUTH,(\d+),(\d+)$/) {
        my $client = $self->get_client($1);
        return unless($client);
        $client->state('reauthing');
        $client->kid($2);
        $handle->unshift_read(line => sub {
            $self->_handle_env($_[0], $_[1], $client);
        });
    } elsif ($line =~ />CLIENT:CR_RESPONSE,(\d+),(\d+),(.+)$/) {
        my $client = $self->get_client($1);
        return unless($client);
        my $challenge = $3;
        $client->state('challenging');
        #$client->kid($2); # kid looks bad
        $handle->unshift_read(line => sub {
            $self->_handle_env($_[0], $_[1], $client, $challenge);
        });
    } elsif ($line =~ />CLIENT:ESTABLISHED,(\d+)$/) {
        my $client = $self->get_client($1);
        return unless($client);
        $client->state('establishing');
        $handle->unshift_read(line => sub {
            $self->_handle_env($_[0], $_[1], $client);
        });
    } elsif ($line =~ />CLIENT:ADDRESS,(\d+),(.+),(\d+)$/) {
        my $client = $self->get_client($1);
        return unless($client);
        $client->add_address($2);
        $client->state('addressing');
        $self->on_client_address->($self, $client, $2, $3);
    } elsif ($line =~ />CLIENT:DISCONNECT,(\d+)$/) {
        my $client = $self->get_client($1);
        return unless($client);
        $client->state('disconnecting');
        $handle->unshift_read(line => sub {
            $self->_handle_env($_[0], $_[1], $client);
        });
    } elsif ($line =~ /^SUCCESS: client-auth command succeeded$/) {
        print STDERR ("client-auth succeeded\n");
    } elsif ($line =~ /^TITLE,(.*)/) {
        $handle->unshift_read(line => sub {
            $self->_handle_status($_[0], $_[1], { clients => [] });
        });
    } else {
        print STDERR ("unhandled $line\n");
    }
}

sub _handle_env {
    my ($self, $handle, $line, $client, $extra) = @_;

    if ($line =~ /^>CLIENT:ENV,END/) {
        if ($client->state eq "connecting") {
            $self->set_client($client->cid, $client);
            $self->on_client_connect->($self, $client);
        } elsif ($client->state eq "reauthing") {
            $self->on_client_reauth->($self, $client);
        } elsif ($client->state eq "challenging") {
            $self->on_client_challenge->($self, $client, $extra);
        } elsif ($client->state eq "establishing") {
            $self->on_client_established->($self, $client);
        } elsif ($client->state eq "disconnecting") {
            $self->del_client($client->cid);
            $self->on_client_disconnected->($self, $client);
        }
    } elsif ($line =~ /^>CLIENT:ENV,([^=]+)=(.+)$/) {
        $client->set_env($1, $2);
        $handle->unshift_read(line => sub {
            $self->_handle_env($_[0], $_[1], $client, $extra);
        });
    }
}

sub _handle_status {
    my ($self, $handle, $line, $status) = @_;
    if ($line =~ /^END$/) {
        $self->on_status->($self, $status);
    } else {
        if ($line =~ /^CLIENT_LIST,(.*)$/) {
            my @client = split(/,/, $1);
            push(@{$status->{clients}}, \@client);
        }

        $handle->unshift_read(line => sub {
            $self->_handle_status($_[0], $_[1], $status);
        });
    }
}

sub client_auth {
    my ($self, $client) = @_;
    $self->handle->push_write("client-auth ".$client->cid." ".$client->kid."\n");
    map { $self->handle->push_write("$_\n"); } $client->configs;
    $self->handle->push_write("END\n");
}

sub client_auth_nt {
    my ($self, $client) = @_;
    $self->handle->push_write("client-auth-nt ".$client->cid." ".$client->kid."\n");
}

sub client_deny {
    my ($self, $client, $reason) = @_;
    $self->handle->push_write("client-deny ".$client->cid." ".$client->kid." \"$reason\"\n");
}

sub client_pending {
    my ($self, $client, $extra, $timeout) = @_;
    $self->handle->push_write("client-pending-auth ".$client->cid." ".$client->kid." $extra $timeout\n");
}

sub client_pf {
    my ($self, $client, @config) = @_;
    $self->handle->push_write("client-pf ".$client->cid."\n");
    $self->handle->push_write("[CLIENTS DROP]\n");
    $self->handle->push_write("[SUBNETS DROP]\n");
    map { $self->handle->push_write("$_\n"); } $client->pfs;
    $self->handle->push_write("[END]\n");
    $self->handle->push_write("END\n");
}

sub status {
    my $self = shift;

    $self->handle->push_write("status\n");
}

__PACKAGE__->meta->make_immutable;

1;
