#!/usr/bin/env perl

use strict;
use warnings;

$|++;
our $home;

BEGIN {
  use FindBin;
  FindBin::again();

  $home = ($ENV{NETDISCO_HOME} || $ENV{HOME});

  # try to find a localenv if one isn't already in place.
  if (!exists $ENV{PERL_LOCAL_LIB_ROOT}) {
      use File::Spec;
      my $localenv = File::Spec->catfile($FindBin::RealBin, 'localenv');
      exec($localenv, $0, @ARGV) if -f $localenv;
      $localenv = File::Spec->catfile($home, 'perl5', 'bin', 'localenv');
      exec($localenv, $0, @ARGV) if -f $localenv;

      die "Sorry, can't find libs required for App::Netdisco.\n"
        if !exists $ENV{PERLBREW_PERL};
  }
}

BEGIN {
  use Path::Class;

  # stuff useful locations into @INC and $PATH
  unshift @INC,
    dir($FindBin::RealBin)->parent->subdir('lib')->stringify,
    dir($FindBin::RealBin, 'lib')->stringify;

  use Config;
  $ENV{PATH} = $FindBin::RealBin . $Config{path_sep} . $ENV{PATH};
}

use App::Netdisco;
use Dancer ':script';
use Dancer::Plugin::DBIC 'schema';
use Dancer::Plugin::Passphrase;
use App::Netdisco::Util::Statistics ();

info "App::Netdisco $App::Netdisco::VERSION loaded.";

use 5.010_000;
use Term::UI;
use Term::ReadLine;
use Term::ANSIColor;

use Archive::Extract;
$Archive::Extract::PREFER_BIN = 1;
use File::Slurper 'read_lines';
use HTTP::Tiny;
use Digest::MD5;
use Try::Tiny;
use File::Path ();
use File::Copy ();
use Encode;

=head1 NAME

netdisco-deploy - Database, OUI and MIB deployment for Netdisco

=head1 USAGE

This script deploys the Netdisco database schema, OUI data, and MIBs. Each of
these is an optional service which the user is asked to confirm.

Pre-existing requirements are that there be a database table created and a
user with rights to create tables in that database. Both the table and user
name must match those configured in your environment YAML file (default
F<~/environments/deployment.yml>).

This script will download the latest MAC address vendor prefix data from the
Internet, and update the OUI table in the database. Hence Internet access is
required to run the script.

Similarly the latest Netdisco MIB bundle is also downloaded and placed into
the user's home directory (or C<$ENV{NETDISCO_HOME}>).

If you upgrade Netdisco make sure you run this script again to make sure
your config remains compatible.

Before each upgrade also review the
L<Release notes|https://github.com/netdisco/netdisco/wiki/Release-Notes> since
additional steps might be required!

=cut

print color 'bold cyan';
say 'This is the Netdisco 2 deployment script.';
say '';
say 'Before we continue, the following prerequisites must be in place:';
say ' * Database added to PostgreSQL for Netdisco';
say ' * User added to PostgreSQL with rights to the Netdisco Database';
say ' * "~/environments/deployment.yml" file configured with Database dsn/user/pass';
say ' * A full backup of any existing Netdisco database data';
say ' * Internet access (for OUIs and MIBs)';
say '';
say 'If you are upgrading Netdisco 2 read the release notes:';
say 'https://github.com/netdisco/netdisco/wiki/Release-Notes';
say 'There you will find required and incompatible changes';
say 'which are not covered by this script.';
say '';
say 'You will be asked to confirm all changes to your system.';
say '';
print color 'reset';

my $term = Term::ReadLine->new('netdisco');
my $bool = $term->ask_yn(
  prompt => 'So, is all of the above in place?', default => 'n',
);

exit(0) unless $bool;

say '';
$bool = $term->ask_yn(
  prompt => 'Would you like to deploy the database schema?', default => 'n',
);
deploy_db() if $bool;

say '';
$bool = $term->ask_yn(
  prompt => 'Download and update vendor MAC prefixes (OUI data)?', default => 'n',
);
deploy_oui() if $bool;

say '';
my $default_mibhome = dir($home, 'netdisco-mibs');
if (setting('mibhome') and setting('mibhome') ne $default_mibhome) {
    my $mibhome = $term->get_reply(
      print_me => "MIB home options:",
      prompt   => "Download and update MIB files to...?",
      choices  => [setting('mibhome'), $default_mibhome, 'Skip this.'],
      default  => 'Skip this.',
    );
    deploy_mibs($mibhome) if $mibhome and $mibhome ne 'Skip this.';
}
else {
    $bool = $term->ask_yn(
      prompt => "Download and update MIB files?", default => 'n',
    );
    deploy_mibs($default_mibhome) if $bool;
}

sub deploy_db {
  system('netdisco-db-deploy') == 0 or die "\n";
  print color 'bold blue';
  say 'DB schema update complete.';
  print color 'reset';

  print color 'bold blue';
  print 'Updating statistics... ';
  App::Netdisco::Util::Statistics::update_stats();
  say 'done.';
  print color 'reset';

  if (not setting('safe_password_store')) {
      say '';
      print color 'bold red';
      say '*** WARNING: Weak password hashes are being stored in the database! ***';
      say '*** WARNING: Please add "safe_password_store: true" to your ~/environments/deployment.yml file. ***';
      print color 'reset';
  }

  sub _make_password {
    my $pass = (shift || passphrase->generate_random);
    if (setting('safe_password_store')) {
        return passphrase($pass)->generate;
    }
    else {
        return Digest::MD5::md5_hex($pass),
    }
  }

  # roll everything back if we're testing
  my $txn_guard = $ENV{ND2_DB_ROLLBACK}
    ? schema('netdisco')->storage->txn_scope_guard : undef;

  # set up initial admin user
  my $users = schema('netdisco')->resultset('User');
  if ($users->search({-bool => 'admin'})->count == 0) {
      say '';
      print color 'bold green';
      say 'We need to create a user for initial login. This user will be a full Administrator.';
      say 'Afterwards, you can go to Admin -> User Management to manage users.';
      print color 'reset';
      say '';

      my ($name, $pass) = get_userpass($term);
      $users->create({
        username => $name,
        password => _make_password($pass),
        admin => 'true',
        port_control => 'true',
      });

      print color 'bold blue';
      say 'New user created.';
      print color 'reset';
  }

  # set initial dancer web session cookie key
  schema('netdisco')->resultset('Session')->find_or_create(
    {id => 'dancer_session_cookie_key', a_session => \'md5(random()::text)'},
    {key => 'primary'},
  );
}

sub get_userpass {
  my $upterm = shift;
  my $name = $upterm->get_reply(prompt => 'Username: ');
  my $pass = $upterm->get_reply(prompt => 'Password: ');

  unless ($name and $pass) {
    say 'username and password cannot be empty, please try again.';
    ($name, $pass) = get_userpass($upterm);
  }

  return ($name, $pass);
}

sub deploy_oui {
  print color 'bold blue';
  print 'Updating OUI, Manufacturers, Enterprises, and Products... ';

  my $latest = 'https://raw.githubusercontent.com/netdisco/upstream-sources/refs/heads/master/bootstrap/netdisco-lookup-tables.sql';
  my $file = file($home, 'netdisco-lookup-tables.sql');
  my $resp = HTTP::Tiny->new->mirror($latest, $file);

  if ($resp->{success}) {
      # by loading App::Netdisco, Configuration has set necessary psql env vars
      system("psql -X -v ON_ERROR_STOP=0 -v ON_ERROR_ROLLBACK=on -q -f ${file}");
      unlink $file;
      say 'done.';
  }
  else {
      print color 'bold red';
      say 'SQL download failed!';
  }

  print color 'reset';
}

sub deploy_mibs {
  my $mibhome = dir(shift); # /path/to/netdisco-mibs
  my $fail = 0;

  my $latest = 'https://github.com/netdisco/netdisco-mibs/releases/latest';
  my $resp = HTTP::Tiny->new->get($latest);

  if ($resp->{url} =~ m/([0-9.]+)$/) {
    my $ver = $1;
    my $url = "https://github.com/netdisco/netdisco-mibs/releases/download/${ver}/netdisco-mibs.tar.gz";
    my $file = file($home, 'netdisco-mibs.tar.gz');
    $resp = HTTP::Tiny->new->mirror($url, $file);

    if ($resp->{success}) {
      my $ae = Archive::Extract->new(archive => $file, type => 'tgz');
      $ae->extract(to => $mibhome->parent->stringify);

      my $from = file($mibhome->parent->stringify, "netdisco-mibs-$ver");
      my $to = file($mibhome->parent->stringify, 'netdisco-mibs');

      if (-d $from) {
        File::Path::remove_tree($to, { verbose => 0 });
        File::Copy::move($from, $to);
      }
      unlink $file;
    }
    else { ++$fail }
  }
  else { ++$fail }

  if ($fail) {
    print color 'bold red';
    say 'MIB download failed!';
  }
  else {
    print color 'bold blue';
    say 'MIBs update complete.';

    if (schema('netdisco')->resultset('SNMPObject')->count) {
      print 'Updating SNMP Browser... ';
      system('netdisco-do loadmibs --quiet');
      say 'done.';
    }
  }

  print color 'reset';
}

exit 0;
