#!/usr/bin/perl

BEGIN {
  my ($wd) = $0 =~ m-(.*)/- ;
  $wd ||= '.';
  unshift @INC, $wd;
}

use Data::Dumper;

use BSConfiguration;
use BSUtil;
use BSDB;

$Data::Dumper::Sortkeys = 1;

sub BSSrcServer::Redis::opendb {
  my ($table) = @_;
  die("unsupported table: $table\n") unless $table eq 'result' || $table eq 'oldresult' || $table eq 'scmsync' || $table eq 'jobs';
  require BSRedis;
  die("No redis server configured\n") unless $BSConfig::redisserver;
  die("Redis server must be of scheme redis[s]://<server>[:port]\n") unless $BSConfig::redisserver =~ /^(rediss?):\/\/(?:(?:([^\/\@:]+):)?([^\/\@]*)\@)?([^\/:]+)(?::(\d+))?$/;
  my $red = BSRedis->new('server' => $4, 'port' => $5, 'user' => $2, 'password' => $3, 'tls' => ($1 eq 'rediss' ? 1 : 0));
  return bless { 'red' => $red, 'table' => $table }, 'BSSrcServer::Redis';
}

sub BSSrcServer::Redis::keys {
  my ($db, $path, $value, $lkeys) = @_;
  die("unsupported operation\n") if defined $path;
  my $table = $db->{'table'};
  my $k = $db->{'red'}->run('KEYS', $table.'.*');
  return sort(grep {s/^\Q$table\E\.//} @$k);
}

sub BSSrcServer::Redis::fetch {
  my ($db, $key) = @_;
  my $table = $db->{'table'};
  my $r = $db->{'red'}->run('HGETALL', "$table.$key");
  return { @$r };
}

if (@ARGV && $ARGV[0] eq '--old') {
  shift @ARGV;
  undef $BSConfig::published_db_sqlite;
  undef $BSConfig::source_db_sqlite;
} elsif (@ARGV && $ARGV[0] eq '--new') {
  shift @ARGV;
  $BSConfig::published_db_sqlite = 1;
  $BSConfig::source_db_sqlite = 1;
}

require BSSrcServer::SQLite if $BSConfig::published_db_sqlite || $BSConfig::source_db_sqlite;

use strict;

my $extrepodb = "$BSConfig::bsdir/db/published";
my $sourcedb = "$BSConfig::bsdir/db/source";

BSUtil::drop_privs_to($BSConfig::bsuser, $BSConfig::bsgroup);

die("usage: bs_dbtool binary|pattern|repoinfo|linkinfo|scmsync <cmd> [args]\n") unless @ARGV >= 2;

my $table = shift @ARGV;
my $cmd = shift @ARGV;

my $db;
if ($table eq 'linkinfo' || $table eq 'scmsync') {
  if ($BSConfig::source_db_sqlite) {
    $db = BSSrcServer::SQLite::opendb($sourcedb, $table);
  } else {
    die("scmsync database needs source_db_sqlite\n") if $table eq 'scmsync';
    $db = BSDB::opendb($sourcedb, $table);
    $db->{'allkeyspath'} = 'project';
  }
} elsif ($table eq 'binary' || $table eq 'pattern' || $table eq 'repoinfo') {
  if ($BSConfig::published_db_sqlite) {
    $db = BSSrcServer::SQLite::opendb($extrepodb, $table);
  } else {
    $db = BSDB::opendb($extrepodb, $table);
    $db->{'allkeyspath'} = 'name' if $table eq 'binary';
  }
} elsif ($table eq 'result' || $table eq 'oldresult' || $table eq 'jobs' || $table eq 'scmsync.redis') {
  $db = BSSrcServer::Redis::opendb($table eq 'scmsync.redis' ? 'scmsync' : $table);
} else {
  die("unknown database table $table\n");
}

if ($cmd eq 'keys') {
  my @k = $db->keys(@ARGV);
  print "  - $_\n" for @k;
  exit(0);
}

if ($cmd eq 'values') {
  die("values: need path argument\n") unless @ARGV;
  my @v = $db->values(@ARGV);
  print "  - $_\n" for @v;
  exit(0);
}

if ($cmd eq 'fetch') {
  print Dumper($db->fetch($ARGV[0]));
  exit(0);
}

if ($cmd eq 'repoinfo') {
  my $prp = $ARGV[0];
  my $prp_ext = $prp;
  $prp_ext =~ s/:/:\//g;
  my $repoinfo;
  if ($BSConfig::published_db_sqlite) {
    $repoinfo = $db->getrepoinfo($prp_ext);
  } else {
    $db = BSDB::opendb($extrepodb, 'repoinfo');
    $repoinfo = $db->fetch($prp);
  }
  print Dumper($repoinfo);
  exit(0);
}

if ($cmd eq 'repoorigins') {
  my $prp = $ARGV[0];
  my $prp_ext = $prp;
  $prp_ext =~ s/:/:\//g;
  my $repoorigins;
  if ($BSConfig::published_db_sqlite) {
    $repoorigins = $db->getrepoorigins($prp_ext);
  } else {
    $db = BSDB::opendb($extrepodb, 'repoinfo');
    my $repoinfo = $db->fetch($prp) || {};
    $repoorigins = $repoinfo->{'binaryorigins'};
  }
  print Dumper($repoorigins);
  exit(0);
}

if ($cmd eq 'projectkeys') {
  my @k;
  if (!@ARGV) {
    @k = $db->values($table eq 'linkinfo' ? 'sourceproject' : 'project');
  } elsif ($BSConfig::published_db_sqlite) {
    @k = $db->getprojectkeys($ARGV[0]);
  } else {
    die("the projectkeys command only works with sqlite\n");
  }
  print "  - $_\n" for @k;
  exit(0);
}

if ($cmd eq 'locallinks') {
  my @l;
  if ($BSConfig::source_db_sqlite) {
    @l = $db->getlocallinks(@ARGV);
  } else {
    die("the locallinks command only works with sqlite\n");
  }
  print "  - $_\n" for @l;
  exit(0);
}

if ($cmd eq 'linkers') {
  my @l;
  if ($BSConfig::source_db_sqlite) {
    @l = $db->getlinkers(@ARGV);
  } else {
    die("the linkers command only works with sqlite\n");
  }
  print "  - $_\n" for @l;
  exit(0);
}

if ($cmd eq 'linkpackages') {
  my @l;
  if ($BSConfig::source_db_sqlite) {
    @l = $db->getlinkpackages(@ARGV);
  } else {
    die("the linkpackages command only works with sqlite\n");
  }
  print "  - $_\n" for @l;
  exit(0);
}

if ($cmd eq 'convert' && $table eq 'scmsync') {
  die("the scmsync redis conversion only works with sqlite\n") unless $BSConfig::source_db_sqlite;
  require BSSrcServer::ScmsyncDB;
  my @todo = $db->keys();
  my $done = 0;
  my $todo = @todo;
  print "Converting $todo datasets\n";
  for my $k (@todo) {
    my $scmsyncinfo = $db->fetch($k);
    if (defined(($scmsyncinfo || {})->{'scmsync_repo'})) {
      BSSrcServer::ScmsyncDB::addredisjob('updatescmsync', $k, $scmsyncinfo->{'scmsync_repo'}, $scmsyncinfo->{'scmsync_branch'}, $scmsyncinfo->{'scmsync_trackingbranch'});
    }
    $done++;
    print "$done/$todo\n" if $done % 10 == 0;
  }
  print "$todo/$todo\n";
  exit(0);
}

if ($cmd eq 'convert') {
  require BSSrcServer::SQLite;
  $db = undef;
  my $ndb;
  if ($table eq 'linkinfo') {
    $ndb = BSSrcServer::SQLite::opendb($sourcedb, $table);
    $db = BSDB::opendb($sourcedb, $table);
    $db->{'allkeyspath'} = 'project';
  } elsif ($table eq 'pattern') {
    $ndb = BSSrcServer::SQLite::opendb($extrepodb, $table);
    $db = BSDB::opendb($extrepodb, $table);
  } elsif ($table eq 'binary') {
    $ndb = BSSrcServer::SQLite::opendb($extrepodb, $table);
    $db = BSDB::opendb($extrepodb, 'repoinfo');
  } else {
    die("unsupported table to convert\n");
  }

  my @todo;
  my %todo_pattern;
  if ($table eq 'linkinfo' || $table eq 'binary') {
    @todo = $db->keys();
  } elsif ($table eq 'pattern') {
    for my $key ($db->keys()) {
      my @p = split('/', $key);
      while (@p > 1 && $p[0] =~ /:$/) {
	splice(@p, 0, 2, "$p[0]$p[1]");
      }
      my $project = shift(@p);
      while (@p > 1 && $p[0] =~ /:$/) {
	splice(@p, 0, 2, "$p[0]$p[1]");
      }
      my $repository = shift(@p);
      $key = join('/', @p);
      $todo_pattern{"$project/$repository"}->{$key} = 1;
    }
    @todo = sort keys %todo_pattern;
  }
  my $done = 0;
  my $todo = @todo;

  print "Converting $todo datasets\n";

  if ($table eq 'linkinfo') {
    BSSrcServer::SQLite::init_sourcedb();
  } elsif ($table eq 'pattern' || $table eq 'binary') {
    BSSrcServer::SQLite::init_publisheddb(undef, $table);
  }
  $ndb->asyncmode();

  my @bad;
  for my $prp (@todo) {
    my $prp_ext = $prp;
    $prp_ext =~ s/:/:\//g;

    if ($table eq 'linkinfo') {
      my ($projid, $packid) = split('/', $prp, 2);
      my $linkinfo = $db->fetch($prp);
      if (!$linkinfo) {
	push @bad, $prp;
	warn("could not fetch $bad[-1]\n");
	next;
      }
      $ndb->store_linkinfo($projid, $packid, $linkinfo);
    } elsif ($table eq 'pattern') {
      my $patterninfo = {};
      for my $path (sort keys %{$todo_pattern{$prp}}) {
	my $pi = $db->fetch("$prp_ext/$path");
	if (!$pi) {
	  push @bad, "$prp_ext/$path";
	  warn("could not fetch $bad[-1]\n");
	  next;
	}
	$patterninfo->{$path} = $pi;
      }
      $ndb->updatedb_patterninfo($prp_ext, $patterninfo);
    } elsif ($table eq 'binary') {
      my $repoinfo = $db->fetch($prp);
      if (!$repoinfo) {
	push @bad, $prp;
	warn("could not fetch $bad[-1]\n");
	next;
      }
      $ndb->updatedb_repoinfo($prp_ext, $repoinfo);
    }

    $done++;
    print "$done/$todo\n" if $done % 10 == 0;
  }
  print "$todo/$todo\n";
  if (@bad) {
    print "WARNING: could not fetch the following entries:\n";
    print "  - $_\n" for @bad;
  }
  exit(0);
}

if ($cmd eq 'deleteobsolete') {
  die("the deleteobsolete command only works with the result/scmsync.redis db\n") unless $table eq 'result' || $table eq 'scmsync.redis';
  require BSRevision;
  require BSSrcServer::ScmsyncDB;
  my %projids;
  for my $k ($db->keys()) {
    my ($projid) = split('/', $k, 2);
    push @{$projids{$projid}}, $k;
  }
  my %correct;
  if ($table eq 'scmsync.redis') {
    my $ndb = BSSrcServer::SQLite::opendb($sourcedb, 'scmsync');
    %correct = map {$_ => 1} $ndb->keys();
  }
  my $done = 0;
  my $todo = keys %projids;
  for my $projid (sort keys %projids) {
    if ($table ne 'scmsync.redis') {
      my $proj = BSRevision::readproj_local($projid, 1) || {};
      my @prpas;
      for my $repo (@{$proj->{'repository'} || []}) {
        push @prpas, map {"$projid/$repo->{'name'}/$_"} @{$repo->{'arch'} || []};
      }
      %correct = map {$_ => 1} @prpas;
    }
    my @bad = grep {!$correct{$_}} @{$projids{$projid}};
    if (@bad) {
      printf "  deleting %d entries\n", scalar(@bad);
      if ($table eq 'scmsync.redis') {
        BSSrcServer::ScmsyncDB::addredisjob('updatescmsync', $_) for @bad;
      } else {
	BSSrcServer::ScmsyncDB::addredisjob('deleteresult', $_) for @bad;
      }
    }
    $done++;
    print "$done/$todo\n" if $done % 10 == 0;
  }
  print "$todo/$todo\n";
  exit(0);
}

die("unknown command $cmd\n");

