#!/usr/bin/perl -w
# Copyright (C) 2014-2018 Miquel Sabaté Solà <mikisabate@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# This script allows us to connect with psql properly for the given environment.
#
# The environments that we can use have to be specified in a JSON file. This
# JSON file has to be specified in the "$json" configurable value (by default
# is "db/database.json"). This JSON file has to specify the environments
# available and a set of values. Each environment has to specify the following
# values:
#
#   * dbname: the name of the database.
#   * host: the host to be used.
#   * user: the user that is accessing the database.
#   * password: the password for the user@host.
#
# More values can be specified, but they will be ignored by this script. You
# can find an example of this in the examples/database.json file.
#
# You can call this script with the following optional arguments:
#
#   -e name
#       This argument can be used to specify the environment that we want to
#       connect to. If this argument has not been passed, it will take the
#       value of the "$default" configurable value ("development" by default).
#       Note that this argument can be used in combination with other
#       arguments.
#
#   -m, --migrate
#       This argument can be used to migrate a set of .sql files into the
#       database. The path to these files can be set with the "$migrate"
#       configurable value (by default it is "db/migrate"). Keep in mind that
#       migration files can have the comment "-- ignore." in the beginning of
#       the file (other previous comments or blank lines will be ignored). This
#       comment tells this script to ignore this migration file.
#
#   -b, --backup
#       Spit to the STDOUT a backup of the database.
#
# Therefore, this command has the following usage:
#
#   $ db [-e name] [[-m | --migrate] | [-b | --backup]]
#

use strict;
use File::Basename;
use Cwd 'abs_path', 'getcwd';


##
# Config values. Ideally you should only modify these values to adapt this
# script to your project.
#
# NOTE: path values should *not* start and/or end with a slash.


# The base path that this script will use. By default it picks the current
# working directory.
my $base = abs_path(getcwd);

# The relative path to the json file (including the name of the json file
# itself).
my $json = 'db/database.json';

# The default environment. This is the environment to be used when no
# environment has been given through the '-e' option.
my $default = 'development';

# The relative path with migration files. Migration files are plain .sql files
# that will be used with the '-m' option.
my $migrate = 'db/migrate';


##
# And here starts the program itself.


# Show the usage string and exit.
sub usage {
    print "Usage: db [-e name] [[-m | --migrate] | [-b | --backup]]\n";
    print "  -e  Specify an environment.\n";
    print "  -m  Migrate all the files from the db/migrate directory.\n";
    print "  -b  Backup the proper DB.\n";
    exit(1);
}

# Check that a given value has been defined in the given hash.
sub check {
    my ($ary, $value) = @_;
    if (!defined($ary->{$value})) {
        print "ERROR: you haven't defined the '$value' value.\n";
        exit(1);
    }
}

# Returns true if we should ignore the given migration.
sub should_ignore {
    my ($path) = @_;
    my $res = 0;

    if (open(MIGRATION, $path)) {
        while (<MIGRATION>) {
            if ($_ =~ /^--\s*ignore\.?/) {
                # Ok boss, let's ignore it.
                $res = 1;
                last;
            } elsif ($_ =~ /^\s*$/) {
                # It's a blank line, next.
                next;
            } elsif ($_ =~ /^--/) {
                # It's an SQL comment, next.
                next;
            }
            last;
        }
        close(MIGRATION);
    }
    return $res;
}

# Apply all the migrations if needed.
sub migrate {
    my ($cmd, $path) = @_;

    print "Starting migration\n";
    opendir(DIR, $path) or die $!;
    while (my $file = readdir(DIR)) {
        next if ($file =~ m/^\./);
        next if ($file !~ m/\.sql$/);

        if (should_ignore("$path/$file")) {
            print "Ignoring migration: $file\n";
        } else {
            print "Applying migration: $file\n";
            system("$cmd < $path/$file\n");
        }
    }
    closedir(DIR);
}

# Parsing options.
my %opts = ('e', $default, 'm', 0, 'b', 0);
for (my $it = 0; $it < @ARGV; $it++) {
    if ($ARGV[$it] eq '-e') {
        usage() if (!$ARGV[$it + 1]);
        $opts{e} = $ARGV[$it + 1];
        $it++;
    } elsif ($ARGV[$it] eq '-m' || $ARGV[$it] eq '--migrate') {
        $opts{m} = 1;
    } elsif ($ARGV[$it] eq '-b' || $ARGV[$it] eq '--backup') {
        $opts{b} = 1;
    } else {
        if ($ARGV[$it] ne '-h' && $ARGV[$it] ne '--help') {
            print "Unknown option `$ARGV[$it]'\n\n";
        }
        usage();
    }
}

# Let's fetch the configuration of the environment.
my $config = "$base/$json";
if (!-f $config) {
    print "ERROR: the $config file does not exist!\n";
    exit(1)
}

open(FILE, $config);
my $file = '';
while (<FILE>) {
    my $line = $_;
    chomp($line);
    $file .= " $line";
}
close(FILE);

# Fetch the values.
$file =~ m/"$opts{e}":\s?({(.*?)})/;
my $contents = $1;
if (!defined($contents)) {
    print "The `$opts{e}' environment does not exist.\n";
    exit(1);
}

# Setup the command.
my %ary = ();
$ary{$1} = $2 while ($contents =~ /"(\w+)":\s?"([\w\-]*)"/g);
foreach (qw{dbname user host password}) {
    check(\%ary, $_);
}

my $opts = "-d $ary{'dbname'} -U $ary{'user'} -h $ary{'host'}";
my $cmd = "PGPASSWORD=$ary{'password'} psql $opts";

# Now execute the command properly.
if ($opts{m}) {
    migrate($cmd, "$base/$migrate");
} elsif ($opts{b}) {
    my $opts = "-d $ary{'dbname'} -U $ary{'user'}";
    system("PGPASSWORD=$ary{'password'} pg_dump $opts");
} else {
    system("$cmd");
}
