#!perl
use strict;
use warnings;
use 5.020;
use experimental 'postderef';
use Net::Twitter;
use Path::Tiny;
use JSON;
use Try::Tiny;
use Safe::Isa;
use open ':std',':locale';

my $WRITING=0;

use Data::Printer;

sub get_twitter {
    my $config_file = $0 =~ s{\.[^.]+$}{.json}r;
    my $conf = decode_json(path($config_file)->slurp_raw);
    return Net::Twitter->new(traits=>[
        'API::RESTv1_1',
        'AutoCursor',
        'AutoCursor' => {
            max_calls => 15,
            force_cursor => 1,
            array_accessor => 'users',
            methods => [qw(friends followers)],
        },
    ],%$conf);
}

sub fetch_lists_info {
    my ($tw) = @_;

    my $lists = $tw->list_ownerships({
        count => 200,
    });
    my %lists_info = map {
        $_->{id} => {
            name => $_->{name},
        },
    } $lists->{lists}->@*;

    for my $list_id (sort keys %lists_info) {
        my $members = $tw->list_members({
            list_id => $list_id,
            count => 2000,
            skip_status => 1,
            include_entities => 0,
        });
        $lists_info{$list_id}->{members}={
            map { $_->{id} => 1 } $members->{users}->@*,
        };
    }
    return \%lists_info;
}

sub fetch_friends_info {
    my ($tw) = @_;

    my $friends = $tw->friends({
        count => 200,
        skip_status => 1,
        include_user_entities => 0,
    });

    my %friends_info = map {
        $_->{id} => { $_->%{qw(name screen_name)} }
    } $friends->@*;

    return \%friends_info;
}

sub cache_file { $0 =~ s{\.[^.]+$}{-cache.json}r };

sub load_info {
    return try { decode_json(path(cache_file)->slurp_raw) };
}

sub save_info {
    path(cache_file)->spew_raw(encode_json({
        lists=>$_[0],
        friends=>$_[1],
    }));
    return;
}

sub to_list {
    sort {
        $a->{name} cmp $b->{name}
    } map {
        { id => $_, $_[0]->{$_}->%* }
    } keys $_[0]->%*
}

sub set_display_name {
    my $max=0;
    for my $e ($_[0]->@*) {
        $e->{dn} = sprintf '%s (%d)',$e->@{qw(name id)};
        my $l = length($e->{dn});
        $max = $l if $max<$l;
    }
    return $max;
}

sub print_friends_lists_matrix {
    my ($li,$fi) = @_;

    my @lists = to_list($li);
    set_display_name(\@lists);
    my @friends = to_list($fi);
    my $friend_width = set_display_name(\@friends);

    for my $f (@friends) {
        printf '%-*s : ',$friend_width,$f->{dn};
        print join ', ',
            map { $_->{dn} }
            grep { $_->{members}->{$f->{id}} }
            @lists;
        print "\n";
    }
}

sub parse_friends_lists_matrix {
    my ($fn) = @_;

    my @lines = path($fn)->lines_utf8;

    my %lists;
    for my $l (@lines) {
        $l =~ s{\A.*?\((\d+)\)\s+:}{} or next;
        my $friend_id = $1;
        my @lists = map { s{\A\s*(.*?)\s*\z}{$1}r } split ',',$l;
        for my $list (@lists) {
            my ($list_name,$list_id) = ($list =~ m{\A(.*?)(?:\s+\((\d+)\))?\z});
            next unless $list_name;
            my $list_data = $lists{$list_name}||={};
            if ($list_id && $list_data->{id}) {
                if ($list_data->{id} != $list_id) {
                    warn "List $list_name id conflict for friend $friend_id";
                }
            }
            else {
                $list_data->{id} ||= $list_id;
            }
            $list_data->{members}->{$friend_id} = 1;
        }
    }
    return \%lists;
}

sub make_it_so {
    my ($tw,$current_lists_by_id,$wanted_lists) = @_;

    my %current_lists_by_name = map {
        $current_lists_by_id->{$_}{name} => {
            id => $_,
            $current_lists_by_id->{$_}->%*,
        };
    } keys $current_lists_by_id->%*;

    my @operations;
    # first, lists to create
    for my $list (keys $wanted_lists->%*) {
        if ($current_lists_by_name{$list}) {
            $wanted_lists->{$list}{id} ||= $current_lists_by_name{$list}{id};
            if ($wanted_lists->{$list}{id} != $current_lists_by_name{$list}{id}) {
                warn "list $list has conflicting ids!\n";
            }
        }
        else {
            warn "creating $list\n";
            my $list_data;
            if ($WRITING) {
                $list_data = $tw->create_list({name=>$list,mode=>'private'});
            }
            else {
                $list_data = { id => int(rand(10000)) };
            }
            $current_lists_by_name{$list} = {
                $list_data->%{id},
                members => {},
            };
        }
    }
    # then, set members
    for my $list (keys $wanted_lists->%*) {
        warn "operating on $list\n";
        my %current_members = $current_lists_by_name{$list}->{members}->%*;
        my %wanted_members = $wanted_lists->{$list}{members}->%*;
        my %to_add = %wanted_members; delete @to_add{keys %current_members};
        my %to_remove = %current_members; delete @to_remove{keys %wanted_members};
        my @to_remove=keys %to_remove;
        while (my @these = splice @to_remove,0,100) {
            warn "Removing @these from $list\n";
            if ($WRITING) {
                $tw->remove_list_members({
                    list_id => $current_lists_by_name{$list}{id},
                    user_id => \@these,
                });
            }
        }
        my @to_add=keys %to_add;
        while (my @these = splice @to_add,0,100) {
            warn "Adding @these to $list\n";
            if ($WRITING) {
                $tw->add_list_members({
                    list_id => $current_lists_by_name{$list}{id},
                    user_id => \@these,
                });
            }
        }
    }
}

my $tw = get_twitter;
try {
    my $lists_info;
    my $friends_info;
    my $info = load_info();
    if ($info) {
        ($lists_info,$friends_info) = $info->@{qw(lists friends)},
    }
    else {
        $lists_info = fetch_lists_info($tw);
        $friends_info = fetch_friends_info($tw);
        save_info($lists_info,$friends_info);
    }
    if (@ARGV) {
        my $wanted_lists = parse_friends_lists_matrix($ARGV[0]);
        make_it_so($tw,$lists_info,$wanted_lists);
    }
    else {
        print_friends_lists_matrix($lists_info,$friends_info);
    }
} catch {
    if ($_->$_isa('Net::Twitter::Error')) {
        my $limit = $tw->rate_limit_status;
        p $limit;
        p $_;
    }
    else { local $@=$_;die }
};
