package Bif::Sync;
use strict;
use warnings;
use Bif::DB::Plugin::ChangeUUIDv1;
use Bif::Mo;
use Coro;
use DBIx::ThinSQL qw/qv/;
use Log::Any '$log';
use JSON;

our $VERSION = '0.1.5_5';

has changes_dup => (
    is      => 'rw',
    default => 0
);

has changes_sent => (
    is      => 'rw',
    default => 0
);

has changes_torecv => (
    is      => 'rw',
    default => 0
);

has changes_tosend => (
    is      => 'rw',
    default => 0
);

has changes_recv => (
    is      => 'rw',
    default => 0
);

has debug => (
    is      => 'rw',
    default => 0,
);

has db => (
    is       => 'ro',
    required => 1,
);

has hub_id => ( is => 'rw', );

has on_error => ( is => 'ro', required => 1 );

has on_update => (
    is      => 'rw',
    default => sub { },
);

has rh => ( is => 'rw', );

has wh => ( is => 'rw', );

has json => (
    is      => 'rw',
    default => sub { JSON->new->utf8 },
);

has temp_table => (
    is       => 'rw',
    init_arg => undef,
);

sub new_temp_table {
    my $self = shift;
    my $tmp = 'sync_' . $$ . sprintf( "%08x", rand(0xFFFFFFFF) );

    $self->db->do( 'CREATE TEMPORARY TABLE '
          . $tmp . '('
          . 'id INTEGER NOT NULL UNIQUE ON CONFLICT IGNORE,'
          . 'ucount INTEGER'
          . ')' );

    $self->temp_table($tmp);
    return $tmp;
}

sub read {
    my $self = shift;

    my $json = $self->rh->readline("\n");

    if ( !defined $json ) {
        $self->on_error->('connection close/timeout');
        $self->write('EOF/Timeout');
        return 'EOF';
    }

    my $msg = eval { $self->json->decode($json) };

    if ($@) {
        $self->on_error->($@);
        $self->write('InvalidEncoding');
        return 'INVALID';
    }
    elsif ( !defined $msg ) {
        $self->on_error->('no message received');
        $self->write('NoMessage');
        return 'INVALID';
    }

    $log->debugf( 'r: %s', $msg );

    return @$msg;
}

sub write {
    my $self = shift;

    $log->debugf( 'w: %s', \@_ );

    return $self->wh->print( $self->json->encode( \@_ ) . "\n" );
}

# Let sub classes override if necessary
sub trigger_on_update { }

sub real_sync {
    my $self      = shift;
    my $kind      = shift || die 'real_sync($KIND,$id,$prefix,$change_id)';
    my $id        = shift;
    my $prefix    = shift // '';
    my $change_id = shift;

    if ( not defined $id ) {
        $self->write( 'ProtocolError', 'real_sync($id)' );
        return 'ProtocolError';
    }

    my $db      = $self->db;
    my $tmp     = $self->temp_table;
    my $hub_id  = $self->hub_id;
    my $prefix2 = $prefix . '_';

    $self->trigger_on_update( 'matching: ' . $prefix2 );

    my @refs;

    # Case 2b (see below): this end is a leaf node so fake the prefix stuff
    if ($change_id) {
        @refs = $db->xhashrefs(
            select => [
                'SUBSTR(c.uuid, 1, LENGTH(Xrc.prefix)+1) AS prefix',
                'Xrc.sha1 AS sha1',
                'Xrc.change_id AS change_id'
            ],
            from       => "${kind}_related_changes Xrc",
            inner_join => 'changes c',
            on         => 'c.id = Xrc.change_id',
            where      => {
                "Xrc.${kind}_id" => $id,
                'Xrc.change_id'  => $change_id,
                'Xrc.prefix'     => $prefix,
            },
        );
    }

    # Branch node comparison
    else {
        @refs = $db->xhashrefs(
            select => [
                'Xrc.prefix AS prefix',
                'Xrc.sha1 AS sha1',
                'Xrc.change_id AS change_id'
            ],
            from  => "${kind}_related_changes Xrc",
            where => {
                "Xrc.${kind}_id"  => $id,
                'Xrc.prefix LIKE' => $prefix2,
            },
            order_by => 'Xrc.prefix',
        );
    }

    $self->write(
        'MATCH', $prefix2,
        {
            map { $_->{prefix} => [ $_->{sha1}, $_->{change_id} ? 1 : 0 ] }
              @refs
        }
    );

    my ( $action, $mprefix, $there ) = $self->read;

    return "expected MATCH $prefix2 {} (not $action $mprefix ...)"
      unless $action eq 'MATCH'
      and $mprefix eq $prefix2
      and ref $there eq 'HASH';

    my @next;
    my @prefix;

    foreach my $ref (@refs) {

        # Case 1: Prefix does not exist at other end
        if ( !exists $there->{ $ref->{prefix} } ) {
            push( @prefix, ' OR ' ) if @prefix;
            push( @prefix,
                "Xrc.prefix LIKE ",
                qv( $ref->{prefix} . '%' ),
                ' AND Xrc.change_id IS NOT NULL' );
        }

        # Case 2: Prefix hash is not the same
        elsif ( $there->{ $ref->{prefix} }->[0] ne $ref->{sha1} ) {

            # Case 2a: both ends are leaf nodes
            if ( $there->{ $ref->{prefix} }->[1] and $ref->{change_id} ) {
                push( @prefix, ' OR ' ) if @prefix;
                push( @prefix, "Xrc.change_id = ", qv( $ref->{change_id} ) );
            }

            # Case 2b: this end is a leaf node
            elsif ( $ref->{change_id} ) {
                push( @next, [ $ref->{prefix}, $ref->{change_id} ] );
            }

            # Case 2c: this end, and *maybe* other end are branch nodes
            else {
                push( @next, [ $ref->{prefix} ] );
            }
        }
    }

    if (@prefix) {
        $self->db->xdo(
            insert_into => "$tmp(id)",
            select      => 'Xrc.change_id',
            from        => "${kind}_related_changes Xrc",
            where => [ "Xrc.${kind}_id = ", qv($id), ' AND (', @prefix, ')' ],
        );
    }

    if (@next) {
        foreach my $next ( sort { $a->[0] cmp $b->[0] } @next ) {
            $self->real_sync( $kind, $id, @$next );
        }
    }

    return ucfirst($kind) . 'Sync' . $prefix;
}

sub real_send_changesets {
    my $self       = shift;
    my $total      = shift;
    my $statements = shift;

    my $sent = 0;
    $self->changes_tosend($total);
    $self->changes_sent($sent);

    my $db  = $self->db;
    my $sth = $db->xprepare(@$statements);
    $sth->execute;

    while ( my $id = $sth->val ) {
        my $changeset = $db->uchangeset_v1($id);
        return 'SendFailure' unless $self->write( 'CHANGESET', $changeset );

        $self->changes_sent( ++$sent );
        $self->trigger_on_update;
    }

    return 'ChangesetCountMismatch' unless $sent == $total;
    return 'SendChangesets';
}

sub send_changesets {
    my $self       = shift;
    my $total      = shift;
    my $statements = shift;

    $self->write( 'TOTAL', $total );
    my @r = $self->real_send_changesets( $total, $statements );
    return @r unless $r[0] eq 'SendChangesets';

    @r = $self->read;
    return 'SendChangesets' if $r[0] eq 'Recv' and $r[1] == $total;
    return @r;
}

sub real_recv_changesets {
    my $self  = shift;
    my $total = shift;
    my $db    = $self->db;
    my $got   = 0;
    my $dup   = 0;

    $self->changes_torecv($total);
    $self->changes_recv($got);
    $self->changes_dup($dup);
    $self->trigger_on_update;

    while ( $got < $total ) {
        my ( $action, $changeset ) = $self->read;

        unless ( $action eq 'CHANGESET' ) {
            $self->write( 'ProtocolError', "expected CHANGSET not: $action" );
            return "expected CHANGSET not: $action";
        }

        $self->changes_recv( ++$got );
        my ( $res, $uuid ) = $db->save_uchangeset_v1($changeset);

        if ( 0 == $res ) {
            $self->changes_dup( ++$dup );
        }
        elsif ( $res < 0 ) {
            $self->write( 'UUIDMismatch', $uuid );
            return ( 'UUIDMismatch', $uuid );
        }

        $self->trigger_on_update;
    }

    return 'RecvChangesets';
}

sub recv_changesets {
    my $self = shift;
    my ( $action, $total ) = $self->read;
    $total //= '*undef*';

    return "expected TOTAL <int> (not $action $total)"
      if $action ne 'TOTAL' or $total !~ m/^\d+$/;

    my @res = $self->real_recv_changesets($total);
    return @res unless $res[0] eq 'RecvChangesets';

    if ( $self->changes_recv != $self->changes_torecv ) {
        $self->write( 'RecvCountMismatch', $self->changes_recv );
        return (
            'RecvCountMismatch',
            sprintf(
                'expected %d received %d',
                $self->changes_torecv, $self->changes_recv
            )
        );
    }

    $self->db->xdo(
        insert_into => 'func_merge_changes',
        values      => { merge => 1 },
    );

    $self->write( 'Recv', $self->changes_recv );
    return 'RecvChangesets';
}

sub exchange_changesets {
    my $self            = shift;
    my $send_total      = shift;
    my $send_statements = shift;
    my $db              = $self->db;

    # Kick off receiving changesets as a separate Coro thread
    my $fh   = select;
    my $coro = async {
        select $fh;

        my ( $action, $total ) = $self->read;
        $total //= '*undef*';

        return "expected TOTAL <int> (not $action $total)"
          if $action ne 'TOTAL' or $total !~ m/^\d+$/;

        $self->real_recv_changesets($total);
    };

    # Now send receiving changesets
    $self->write( 'TOTAL', $send_total );

    my $send_status =
      $self->real_send_changesets( $send_total, $send_statements );

    # Cancel the $coro?
    return $send_status unless $send_status eq 'SendChangesets';

    # Collect the recv status
    my $recv_status = $coro->join;
    return $recv_status unless $recv_status eq 'RecvChangesets';

    if ( $self->changes_recv != $self->changes_torecv ) {
        $self->write( 'RecvCountMismatch', $self->changes_recv );
        return (
            'RecvCountMismatch',
            sprintf(
                'expected %d received %d',
                $self->changes_torecv, $self->changes_recv
            )
        );
    }

    $db->xdo(
        insert_into => 'func_merge_changes',
        values      => { merge => 1 },
    );

    $self->write( 'Recv', $self->changes_recv );
    my ( $recv, $count ) = $self->read;
    return $recv unless $recv eq 'Recv' and $count == $send_total;

    return 'ExchangeChangesets';
}

1;

=head1 NAME

=for bif-doc #perl

Bif::Sync - synchronisation role

