use strict;
use warnings;
use Nei::Mojo::Reactor::Irssi5;
use IO::Socket::SSL;
use Mojo::Base -strict, -signatures;
use Mojo::UserAgent;
use File::Basename;
use Text::ParseWords;
use Encode;
use Irssi;
use Irssi::TextUI;

our $VERSION = '0.3'; # 6c771e1cb85f075
our %IRSSI = (
    authors     => 'Nei',
    name        => 'soju',
    description => 'soju.im helper script',
    license     => 'ISC',
   );
die 'This script requires Irssi 1.3 or later'
    if (Irssi::parse_special('$abiversion')||0) < 35;

use constant CAP_BOUNCER_NETWORKS        => 'soju.im/bouncer-networks';
use constant CAP_BOUNCER_NETWORKS_NOTIFY => 'soju.im/bouncer-networks-notify';
use constant CAP_BATCH                   => 'batch';
use constant IS_FILEHOST                 => 'soju.im/FILEHOST';

use constant BATCH_OPEN_ID   => 0;
use constant BATCH_OPEN_TYPE => 1;

use constant MAX_LAST_UPLOAD => 50;

use Irssi::Irc;

my $utf8;

my %soju_nets;
my %redir_batch;
my %batch_open;

my $ua = make_ua();
my @last_uploads;

sub make_ua {
    my $ua = Mojo::UserAgent->new;
    $ua->transactor->add_generator(
	stream => sub ($transactor, $tx, $path) {
	    $tx->req->content->asset(Mojo::Asset::File->new(path => $path));
	});
    $ua
}

sub run_next ($subname, $arg) {
    Irssi::timeout_add_once(10, $subname, $arg);
}

sub gui_input_get () {
    Irssi::parse_special('$L')
}

sub insert_link ($loc) {
    my $input = gui_input_get();
    my $pos = Irssi::gui_input_get_pos();
    Encode::_utf8_on($input)
	    if $utf8;
    $loc = " $loc"
	if $pos > 0 && (substr $input, $pos - 1, 1) ne ' ';
    (substr $input, $pos, 0) = $loc;
    Irssi::gui_input_set($input);
    Irssi::gui_input_set_pos($pos + length $loc);
}

sub argparse ($data) {
    $data =~ s/^:// ? ($data) : do {
	my @args = split ' :', $data, 2;
	unshift @args, split ' ', shift @args;
	@args
    }
}

sub reconnect_find_chatnet ($chatnet) {
    my @reconnects = grep { $_->{chatnet} eq $chatnet } Irssi::reconnects;
    return $reconnects[0] if @reconnects;
    return;
}

#####  batch handling  #############################################################################

sub redirect_batch ($server, $start, $type) {
    push $redir_batch{$server->{tag}}{$start}->@*, $type;
}

sub batch_redirect_active ($server, $event, $data, $nick, $address) {
    my $r = $batch_open{$server->{tag}}//[];
    my $type;
    $type = $r->[-1][BATCH_OPEN_TYPE] if $r->@*;
    if ($type) {
	Irssi::signal_emit("redir $type $event", $server, $data, $nick, $address);
	return 1;
    }
    return;
}


sub redir_batch_retrieve ($server, $start) {
    return unless $start;
    my $r = $redir_batch{$server->{tag}}//+{};
    my $s = $r->{$start}//[];
    shift $s->@*
}

sub open_batch ($server, $id, $type) {
    push $batch_open{$server->{tag}}->@*, [ $id, $type ];
    if ($type) {
	Irssi::signal_emit("redir $type batch open", $server, $id, '', '');
    }
}

sub close_batch ($server, $id) {
    while (($batch_open{$server->{tag}}//[])->@*) {
	my $r = pop $batch_open{$server->{tag}}->@*;
	my ($rid, $type) = $r->@*;
	if ($type) {
	    Irssi::signal_emit("redir $type batch close", $server, $rid, '', '');
	}
	last if $id eq $rid;
    }
}

sub event_batch ($server, $data, $nick, $address) {
    my ($batchid, $start, @rest) = argparse($data);
    if ($batchid =~ s/^[+]//) {
	my $type = redir_batch_retrieve($server, $start);
	open_batch($server, $batchid, $type);
    } elsif ($batchid =~ s/^-//) {
	close_batch($server, $batchid);
    }
}

####################################################################################################

sub sync_bnc ($server) {
    return unless exists $server->{cap_supported}{+CAP_BOUNCER_NETWORKS};

    if (defined $server->isupport('bouncer_netid')) {
	# we're in a singlenet connection
    } else {
	my @missing_caps = grep { my $c = $_; not grep { $c eq $_ } $server->{cap_active}->@* }
	    (CAP_BOUNCER_NETWORKS_NOTIFY, CAP_BOUNCER_NETWORKS, CAP_BATCH);
	$server->send_raw_now("CAP REQ :@missing_caps")
	    if @missing_caps;
	redirect_batch($server, +CAP_BOUNCER_NETWORKS, 'auto');
	$server->send_raw_now('BOUNCER LISTNETWORKS');
    }
}

sub event_bouncer ($server, $data, $nick, $address) {
    return if batch_redirect_active($server, 'bouncer', $data, $nick, $address);

    if ($data =~ s/(\S+)(?:\s|$)//) {
	Irssi::signal_emit("event bouncer \L$1", $server, $data, $nick, $address);
    } else {
	$server->print(undef, MSGLEVEL_CRAP, "BOUNCER $data");
    }
}

sub bnc_update_config ($server, $netid, $newconfig) {
    my $config = $soju_nets{$server->{tag}}{$netid} //= +{};
    %$config = (%$config, %$newconfig);
    $config
}

sub bnc_autoconfig ($server, $netid, $newconfig) {
    my $config = bnc_update_config($server, $netid, $newconfig);

    my $client = Irssi::settings_get_str('soju_client_id');
    my $netname = "$server->{tag}|$config->{name}";
    $netname =~ s/[^a-zA-Z0-9|_-]+/_/g;
    my $username = "$server->{username}/$config->{name}\@$client";
    my $net = Irssi::chatnet_find($netname);
    unless ($net && $net->{username} eq $username) {
	Irssi::command("^network add "
		       ."-user $username "
		       ."$netname");
    }
    if ($config->{state} eq 'connected') {
	unless (Irssi::server_find_chatnet($netname)) {
	    my $reconnect = reconnect_find_chatnet($netname);
	    if ($reconnect) {
		Irssi::command("reconnect $reconnect->{tag}");
	    } else {
		Irssi::command("^server add "
			       ."-network $netname "
			       ."-".($server->{use_tls} ? "" : "no")."tls "
			       ."$server->{address} "
			       ."$server->{port} "
			       ."$server->{password}");
		Irssi::command("connect $netname");
	    }
	}
    } elsif ($config->{state} eq 'disconnected') {
	my $s = Irssi::server_find_chatnet($netname);
	if ($s) {
	    $s->command('disconnect');
	} else {
	    my $r = reconnect_find_chatnet($netname);
	    Irssi::command("disconnect $r->{tag}")
		    if $r;
	}
    } elsif ($config->{state} eq 'connecting') {
	$server->printformat('', MSGLEVEL_CRAP, 'soju_connecting', $config->{name}, $netid, $config->{address}, $config->{port});
    }
    if ($config->{error}) {
	$server->printformat('', MSGLEVEL_CRAP, 'soju_connection_error', $config->{name}, $config->{error},
			     $netid // '(unknown netid)', $config->{address} // '(unknown address)', $config->{port} // '(unknown port)');
    }
}

sub event_bouncer_network ($server, $data, $, $) {
    my ($netid, $configstr) = argparse($data);
    my $config = Irssi::Irc::parse_message_tags($configstr);
    bnc_autoconfig($server, $netid, $config);
}

sub redir_auto_bouncer ($server, $data, $nick, $address) {
    if ($data =~ s/(\S+)(?:\s|$)//) {
	Irssi::signal_emit("redir auto bouncer \L$1", $server, $data, $nick, $address);
    } else {
	# unknown, ignore
    }
}

sub redir_auto_bouncer_network ($server, $data, $, $) {
    my ($netid, $configstr) = argparse($data);
    my $config = Irssi::Irc::parse_message_tags($configstr);
    bnc_autoconfig($server, $netid, $config);
}

# sub redir_auto_batch_open ($server, $data, $, $) {
# }

# sub redir_auto_batch_close ($server, $data, $, $) {
# }

sub redir_listnet_bouncer ($server, $data, $nick, $address) {
    if ($data =~ s/(\S+)(?:\s|$)//) {
	Irssi::signal_emit("redir listnet bouncer \L$1", $server, $data, $nick, $address);
    } else {
	# unknown, ignore
    }
}

sub redir_listnet_bouncer_network ($server, $data, $, $) {
    my ($netid, $configstr) = argparse($data);
    my $newconfig = Irssi::Irc::parse_message_tags($configstr);
    my $config = bnc_update_config($server, $netid, $newconfig);
    my %config_opts = %$config;
    delete @config_opts{qw(host port name tls state)};
    my $settings = join ', ', $config->{state}, ($config->{tls} ? 'tls' : ()),
	map { "$_: $config_opts{$_}" } sort keys %config_opts;
    $server->printformat('', MSGLEVEL_CRAP, 'soju_server_line',
			 $netid, $config->{host}, $config->{port} // '', $config->{name} // '', $settings // '');
}

sub redir_listnet_batch_open ($server, $data, $, $) {
    $server->printformat('', MSGLEVEL_CRAP, 'soju_server_header');
}

sub redir_listnet_batch_close ($server, $data, $, $) {
    $server->printformat('', MSGLEVEL_CRAP, 'soju_server_footer');
}

sub bnc_disconnect_all ($tag) {
    return if Irssi::server_find_tag($tag);
    for my $s (Irssi::servers) {
	if ($s->{chatnet} =~ /^\Q$tag\E\|/) {
	    $s->command('disconnect');
	}
    }
}

sub server_disconnected ($server) {
    if (exists $server->{cap_supported}{+CAP_BOUNCER_NETWORKS}) {

	if (defined $server->isupport('bouncer_netid')) {
	    # we're in a singlenet connection
	} else {
	    run_next('bnc_disconnect_all', $server->{tag});
	}
    }

    delete $_->{$server->{tag}} for
	(\(%soju_nets, %redir_batch, %batch_open));
}

sub is_soju_master ($server) {
    $server->isa('Irssi::Irc::Server')
	&& exists $server->{cap_supported}{+CAP_BOUNCER_NETWORKS}
	&& !defined $server->isupport('bouncer_netid')
}

sub is_soju_parent ($server, $subserver) {
    is_soju_master($server)
	&& $server->{address} eq $subserver->{address}
	&& $subserver->{chatnet} =~ /^\Q$server->{tag}\E\|/
}

sub find_soju ($server, $complain) {
    if (exists $server->{cap_supported}{+CAP_BOUNCER_NETWORKS}) {
	if (defined $server->isupport('bouncer_netid')) {
	    my @sojus = grep { is_soju_parent($_, $server) } Irssi::servers;
	    return $sojus[0] if @sojus == 1;
	} else {
	    return $server;
	}
    } else {
	my @sojus = grep { is_soju_master($_) } Irssi::servers;
	return $sojus[0] if @sojus == 1;
    }
    if ($complain) {
	print CLIENTERROR 'Active server is not a soju connection';
    }
    return;
}

sub cmd_soju ($data, $server, $witem) {
    if ($data =~ /^-/) {
	my @tags = map { $_->{tag} } Irssi::servers;
	my $tag_re = join '|', map { "\Q$_" } sort { length $b <=> length $a } @tags;
	if ($data =~ s/^-($tag_re)(?:\s+|$)//i) {
	    $server = Irssi::server_find_tag($1);
	}
    }
    Irssi::command_runsub('soju', $data, $server, $witem);
}

sub cmd_soju_network ($data, $server, $witem) {
    $server = find_soju($server, 1)
	or return;
    redirect_batch($server, +CAP_BOUNCER_NETWORKS, 'listnet');
    $server->send_raw_now('BOUNCER LISTNETWORKS');
}

sub complete_command_soju_upload ($list, $window, $word, $line, $want_space) {
    Irssi::signal_emit('complete command cat', $list, $window, $word, $line, $want_space);
}

sub cmd_soju_upload ($data, $server, $witem) {
    use Data::Dumper;
    $Data::Dumper::Sortkeys = 1;
    print Dumper +{
	data => $data,
	server => $server,
	witem => $witem,
	window => Irssi::active_win,
       };
    upload_common($data, $server, $witem, 0, 1);
}

sub upload_common ($data, $server, $witem, $silent, $is_file_upload, $name = undef) {
    unless ($server) {
	Irssi::UI::Window::format_create_dest(undef, MSGLEVEL_CLIENTERROR)->printformat_module('fe-common/core', 'not_connected')
		unless $silent;
	return;
    }
    my $filehost = $server->isupport(+IS_FILEHOST);
    unless ($filehost) {
	print CLIENTERROR 'Could not find FILEHOST support on active server connection. Please check your soju config.'
	    unless $silent;
	return;
    }

    $server = find_soju($server, !$silent)
	or return;

    my @stream_data;
    my $content_message;
    my $filename;

    if ($is_file_upload) {
	my ($file) = shellwords($data);

	unless (-f $file) {
	    print CLIENTERROR '[soju upload] file not found "'.$file.'"';
	    return;
	}
	@stream_data = (stream => $file);
	$content_message = "'$file'";
	$filename = basename($file);
    } else {
	return unless length $data;
	@stream_data = ($data);
	$content_message = 'clipboard';
	$filename = "$name-paste.txt";
    }

    my $win = Irssi::active_win;
    my $oll = $win->view->{buffer}{cur_line};
    $win->print("[soju upload] Uploading $content_message to '$filehost'...", MSGLEVEL_NEVER);
    my $ll = $win->view->{buffer}{cur_line};
    $win->view->set_bookmark_bottom('soju_upload')
	if $ll->{_irssi} ne $oll->{_irssi};
    my $url = Mojo::URL->new($filehost)
	->userinfo($server->{username} . ':' . $server->{password});
    $ua->post_p($url, {
	'Content-Disposition' => "attachment; filename=\"$filename\""
    }
		=> @stream_data)
	->then(sub ($tx) {
		   if ($tx->res->is_error) {
		       my $err = $tx->res->body;
		       $err =~ s/[\r\n]*$//;
		       Irssi::active_win->print("[soju upload] upload failed: '$err'", MSGLEVEL_NEVER);
		   } else {
		       my $loc = Mojo::URL->new($tx->res->headers->location);
		       unless ($loc->is_abs) {
			   $loc = $loc->to_abs(Mojo::URL->new($filehost));
		       }
		       my $win = Irssi::active_win;
		       my $line = $win->view->get_bookmark('soju_upload');
		       $win->view->remove_line($line) if $line;
		       my $info = "Uploaded $content_message to '$loc'";
		       push @last_uploads, (sprintf "[%02d:%02d] ", (localtime)[2,1]) . $info;
		       shift @last_uploads if @last_uploads > MAX_LAST_UPLOAD;
		       $win->print("[soju upload] $info", MSGLEVEL_NEVER)
			   unless Irssi::settings_get_bool('soju_upload_silent');
		       $win->view->redraw if $line;
		       run_next('insert_link', "$loc")
			   if Irssi::settings_get_bool('soju_upload_autoinsert') || !$is_file_upload;
		   }
	       })
	->catch(sub ($err) {
		    Irssi::active_win->print("[soju upload] upload failed: '$err'", MSGLEVEL_NEVER);
		});
}

sub paste_event ($paste, $arg) {
    return unless Irssi::settings_get_bool('soju_upload_paste');
    my $win = Irssi::active_win;
    my $name = $win->{active_server} ?
	$win->{active_server}->parse_special('$N') :
	Irssi::parse_special('$nick');

    upload_common($paste, $win->{active_server}, $win->{active}, 1, 0, $name);
    Irssi::signal_stop;
}

sub cmd_soju_lastuploads ($data, $server, $witem) {
    my $win = Irssi::active_win;
    $win->print('[soju upload] last uploads:', MSGLEVEL_NEVER);
    $win->print($_) for @last_uploads;
}

sub event_motd_start ($server, $, $, $) {
    sync_bnc($server);
}

sub init {
    setup_changed();
    for my $s (Irssi::servers) {
	next unless $s->isa('Irssi::Irc::Server');
	sync_bnc($s);
    }
}

sub setup_changed {
    $utf8 = lc Irssi::settings_get_str('term_charset') eq 'utf-8';
}

Irssi::settings_add_str('soju', 'soju_client_id', 'irssi');
Irssi::settings_add_bool('soju', 'soju_upload_paste', 1);
Irssi::settings_add_bool('soju', 'soju_upload_autoinsert', 1);
Irssi::settings_add_bool('soju', 'soju_upload_silent', 0);

Irssi::signal_add({
    # at this point, 005 is finished
    'event 375'                     => 'event_motd_start',
    'event 422'                     => 'event_motd_start', # no motd
    'server disconnected'           => 'server_disconnected',
    'event batch'                   => 'event_batch',
    'event bouncer'                 => 'event_bouncer',
    'event bouncer network'         => 'event_bouncer_network',
    # 'redir auto batch open'       => 'redir_auto_batch_open',
    'redir auto bouncer'            => 'redir_auto_bouncer',
    'redir auto bouncer network'    => 'redir_auto_bouncer_network',
    # 'redir auto batch close'      => 'redir_auto_batch_close',
    'redir listnet batch open'      => 'redir_listnet_batch_open',
    'redir listnet bouncer'         => 'redir_listnet_bouncer',
    'redir listnet bouncer network' => 'redir_listnet_bouncer_network',
    'redir listnet batch close'     => 'redir_listnet_batch_close',
    'setup changed'                 => 'setup_changed',
    'paste event'                   => 'paste_event',
});

Irssi::theme_register([
    'soju_connecting'       => 'Connecting to {server $0}',
    'soju_connection_error' => 'Error in connection {server $0} {reason $1}',
    'soju_server_header'    => '%##  Soju Server          Port  Network    Settings',
    'soju_server_line'      => '%#%|$[!2]0 $[!20]1 $[5]2 $[10]3 %|$4',
    'soju_server_footer'    => '',
   ]);

Irssi::command_bind({
    'soju'             => 'cmd_soju',
    'soju network'     => 'cmd_soju_network',
    'soju upload'      => 'cmd_soju_upload',
    'soju lastuploads' => 'cmd_soju_lastuploads',
});

Irssi::signal_register({
    'complete command ' => [qw[glistptr_char* Irssi::UI::Window string string intptr]],
});

Irssi::signal_add({
    'complete command soju upload' => 'complete_command_soju_upload',
});

init();
