#!/usr/bin/env perl
#===============================================================================
#
#         FILE: copy_mysql_pg
#
#        USAGE: ./copy_mysql_pg
#
#  DESCRIPTION: Copies HPR show data from the MariaDB database to an
#               experimental PostgreSQL database
#
#      OPTIONS: ---
# REQUIREMENTS: ---
#         BUGS: ---
#        NOTES: ---
#       AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
#      VERSION: 0.0.4
#      CREATED: 2017-03-15 18:50:08
#     REVISION: 2017-10-17 22:37:08
#
#===============================================================================

use 5.010;
use strict;
use warnings;
use utf8;

use Getopt::Long;
use Pod::Usage;

use Config::General;
use List::MoreUtils qw{uniq apply};

use Text::CSV;
use DBI;

use Data::Dumper;

#
# Version number (manually incremented)
#
our $VERSION = '0.0.4';

#
# Script and directory names
#
( my $PROG = $0 ) =~ s|.*/||mx;
( my $DIR  = $0 ) =~ s|/?[^/]*$||mx;
$DIR = '.' unless $DIR;

#-------------------------------------------------------------------------------
# Declarations
#-------------------------------------------------------------------------------
#
# Constants and other declarations
#
my $basedir     = "$ENV{HOME}/HPR/Database";
my $configfile1 = "$basedir/.hpr_db.cfg";
my $configfile2 = "$basedir/.hpr_pg.cfg";

my $email_template  = 'host_%s@hackerpublicradio.org';

my ( $dbh1, $sth1, $h1, $rv1 );
my ( $dbh2, $sth2, $h2, $rv2 );

my (@phase_choices);
my ( %eps_tags, %data );

my @phases = (
    'episodes', 'hosts', 'eh_xref', 'series', 'es_xref', 'tags',
    'comments', 'twat',  'epilogue'
);

#
# Enable Unicode mode
#
binmode STDOUT, ":encoding(UTF-8)";
binmode STDERR, ":encoding(UTF-8)";

#
# Load database configuration data
#
my $conf1 = Config::General->new(
    -ConfigFile      => $configfile1,
    -InterPolateVars => 1,
    -ExtendedAccess  => 1
);
my %config1 = $conf1->getall();

my $conf2 = Config::General->new(
    -ConfigFile      => $configfile2,
    -InterPolateVars => 1,
    -ExtendedAccess  => 1
);
my %config2 = $conf2->getall();

#-------------------------------------------------------------------------------
# Options and arguments
#-------------------------------------------------------------------------------
my $DEF_DEBUG = 0;

#
# Process options
#
my %options;
Options( \%options );

#
# Default help
#
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 )
    if ( $options{'help'} );

#
# Collect options
#
my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEF_DEBUG );
my $verbose = ( defined( $options{verbose} )   ? $options{verbose}   : 0 );
my $dry_run = ( defined( $options{'dry-run'} ) ? $options{'dry-run'} : 0 );

#
# This option is a list, provided as a CSV
#
my $phase_choices = $options{phases};
if ( defined($phase_choices) ) {
    #
    # We have a list which we'll parse, validate, sort, make unique and filter
    #
    my $lcsv = Text::CSV_XS->new( { binary => 1, } );
    if ( $lcsv->parse($phase_choices) ) {
        # Sort fields
        @phase_choices = uniq( sort { $a cmp $b } $lcsv->fields() );
        # Trim leading and trailing spaces
        @phase_choices = apply { $_ =~ s/(^\s*|\s*$)// } @phase_choices;

        # Make a list of invalid keywords
        my %tmp  = map  { $_ => 1 } @phases;
        my @bad = grep { not exists $tmp{$_} } @phase_choices;

        # Deal with all errors
        die "Invalid list; no elements\n" if scalar(@phase_choices) == 0;
        die "Invalid list; too many elements\n"
            if scalar(@phase_choices) > scalar(@phases);
        die "Invalid list elements: ", join( ",", @bad ) . "\n"
            if scalar(@bad) > 0;
    }
    else {
        die "Failed to parse -list='$phase_choices'\n"
            . $lcsv->error_diag() . "\n";
    }
}
else {
    #
    # By default we do all phases
    #
    @phase_choices = @phases;
}

#-------------------------------------------------------------------------------
# Connect to the MariaDB database
#-------------------------------------------------------------------------------
my $dbtype1 = $config1{database}->{type} // 'mysql';
my $dbhost1 = $config1{database}->{host} // '127.0.0.1';
my $dbport1 = $config1{database}->{port} // 3306;
my $dbname1 = $config1{database}->{name};
my $dbuser1 = $config1{database}->{user};
my $dbpwd1  = $config1{database}->{password};
$dbh1
    = DBI->connect( "dbi:$dbtype1:host=$dbhost1;port=$dbport1;database=$dbname1",
    $dbuser1, $dbpwd1, { AutoCommit => 1 } )
    or die $DBI::errstr;

#
# Enable client-side UTF8
#
$dbh1->{mysql_enable_utf8} = 1;

#-------------------------------------------------------------------------------
# Connect to the PostgreSQL database
#-------------------------------------------------------------------------------
my $dbtype2 = $config2{database}->{type} // 'Pg';
my $dbhost2 = $config2{database}->{host} // '127.0.0.1';
my $dbport2 = $config2{database}->{port} // 5432;
my $dbname2 = $config2{database}->{name};
my $dbuser2 = $config2{database}->{user};
my $dbpwd2  = $config2{database}->{password};
$dbh2 = DBI->connect( "dbi:$dbtype2:host=$dbhost2;database=$dbname2;port=$dbport2",
    $dbuser2, $dbpwd2, { PrintError => 0, AutoCommit => 1 } )
    or die $DBI::errstr;

#
# Enable client-side UTF8
#
$dbh2->{pg_enable_utf8} = 1;

my %choices = map  { $_ => 1 } @phase_choices;

#
# Perform phases in order, omitting those that are not in the list
#
for my $phase (@phases) {
    #---------------------------------------------------------------------------
    # Copy the 'eps' table to 'episodes'
    #---------------------------------------------------------------------------
    if ( $phase eq 'episodes' && exists( $choices{$phase} ) ) {
        print "Build episodes table\n" if ( $verbose > 0 );
        if ( check_table( $dbh2, 'episodes' ) ) {
            build_episodes_table( $dbh1, $dbh2, $verbose ) unless $dry_run;
        }
        else {
            print "** Table 'episodes' is not empty\n";
        }
    }
    #---------------------------------------------------------------------------
    # Copy the 'hosts' table to 'hosts'
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'hosts' && exists( $choices{$phase} ) ) {
        print "Build hosts table\n" if ( $verbose > 0 );
        if ( check_table( $dbh2, 'hosts' ) ) {
            build_hosts_table( $dbh1, $dbh2, $verbose ) unless $dry_run;
        }
        else {
            print "** Table 'hosts' is not empty\n";
        }
    }
    #---------------------------------------------------------------------------
    # Generate the 'episodes_hosts_xref' table
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'eh_xref' && exists( $choices{$phase} ) ) {
        print "Build episodes_hosts_xref table\n" if ( $verbose > 0 );
        if ( check_table( $dbh2, 'episodes_hosts_xref' ) ) {
            build_episodes_hosts_xref_table( $dbh1, $dbh2, $verbose )
                unless $dry_run;
        }
        else {
            print "** Table 'episodes_hosts_xref' is not empty\n";
        }
    }
    #---------------------------------------------------------------------------
    # Copy the 'miniseries' table to 'series'
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'series' && exists( $choices{$phase} ) ) {
        print "Build series table\n" if ( $verbose > 0 );
        if ( check_table( $dbh2, 'series' ) ) {
            build_series_table( $dbh1, $dbh2, $verbose ) unless $dry_run;
        }
        else {
            print "** Table 'series' is not empty\n";
        }
    }
    #---------------------------------------------------------------------------
    # Generate the 'episodes_series_xref' table
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'es_xref' && exists( $choices{$phase} ) ) {
        print "Build episodes_series_xref table\n" if ( $verbose > 0 );
        if ( check_table( $dbh2, 'episodes_series_xref' ) ) {
            build_episodes_series_xref_table( $dbh1, $dbh2, $verbose )
                unless $dry_run;
        }
        else {
            print "** Table 'episodes_series_xref' is not empty\n";
        }
    }
    #---------------------------------------------------------------------------
    #  Collect and store the id numbers and tags from the 'eps' table, then add
    #  them to the PostgreSQL tables.
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'tags' && exists( $choices{$phase} ) ) {
        print "Build tags and episodes_tags_xref tables\n" if ( $verbose > 0 );
        if ( check_table( $dbh2, 'tags' )
            && check_table( $dbh2, 'episodes_tags_xref' ) )
        {
            unless ($dry_run) {
                %eps_tags = %{ collect_eps_tags( $dbh1, $verbose ) };

                if (%eps_tags) {
                    build_tags_table( $dbh2, $verbose, \%eps_tags );
                }
            }
        }
        else {
            print "** Tables 'tags' and/or 'episodes_tags_xref' are not empty\n";
        }
    }
    #---------------------------------------------------------------------------
    # Copy the 'comments' table to 'comments'
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'comments' && exists( $choices{$phase} ) ) {
        print "Build comments table\n" if ( $verbose > 0 );
        if (check_table($dbh2,'comments')) {
            build_comments_table( $dbh1, $dbh2, $verbose ) unless $dry_run;
        }
        else {
            print "** Table 'comments' is not empty\n";
        }
    }
    #---------------------------------------------------------------------------
    # Perform the 'twat' actions
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'twat' && exists( $choices{$phase} ) ) {
        print "Perform twat actions\n" if ( $verbose > 0 );

        #
        # Incorporate the TwaT tables (assuming they exist)
        #
        if (check_table($dbh2,'twat_hosts')) {
            load_twat_hosts($dbh1,$dbh2,$verbose) unless $dry_run;
        }
        else {
            print "** Table 'twat_hosts' is not empty\n";
        }

        if (check_table($dbh2,'twat_episodes')) {
            load_twat_episodes($dbh1,$dbh2,$verbose) unless $dry_run;
        }
        else {
            print "** Table 'twat_episodes' is not empty\n";
        }
    }
    #---------------------------------------------------------------------------
    # Perform the 'epilogue' actions
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'epilogue' && exists( $choices{$phase} ) ) {
        print "Perform epilogue actions\n" if ( $verbose > 0 );

        #
        # Determine the first show date per host, assuming that's when they
        # were added to the database
        #
        compute_host_date_added($dbh2) unless $dry_run;
    }
}

exit;

#===  FUNCTION  ================================================================
#         NAME: build_episodes_table
#      PURPOSE: Copy the data from the MariaDB 'eps' table to the Pg
#               'episodes' table
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub build_episodes_table {
    my ( $dbh1, $dbh2, $verbose ) = @_;

    my ( $sth1, $h1, $sth2, $count );

    $sth1 = $dbh1->prepare('SELECT * FROM eps') or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    $sth2 = $dbh2->prepare('INSERT INTO episodes VALUES (?,?,?,?,?,?,?,?,?)')
        or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    #
    # Query MariaDB for the entire 'eps' table
    #
    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    #
    # Loop though 'eps' table rows writing them to the PostgreSQL 'episodes'
    # table
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute(
            $h1->{id}, $h1->{date},
            $h1->{title},   nullif( $h1->{summary}, '^\s*$' ),
            $h1->{notes},   $h1->{explicit},
            $h1->{license}, $h1->{duration},
            $h1->{downloads},
        );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Copied $count records\n" if ($verbose > 0);

    #
    # Set the sequence to the correct value
    #
    alter_seq($dbh2,'episodes','episode_seq');

}

#===  FUNCTION  ================================================================
#         NAME: build_hosts_table
#      PURPOSE: Copy the data from the Mariadb 'hosts' table to the Pg 'hosts'
#               table
#   PARAMETERS: $dbh1           Handle for the MariaDB table
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub build_hosts_table {
    my ( $dbh1, $dbh2, $verbose ) = @_;

    my ( $sth1, $h1, $sth2, $rv, $count );

    $sth1 = $dbh1->prepare('SELECT * FROM hosts') or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    $sth2 = $dbh2->prepare(
        q{INSERT INTO hosts
            (id, host, email, profile, license, local_image, gpg, valid)
            VALUES (?,?,?,?,?,?,?,?)}
    ) or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute(
            $h1->{hostid},
            $h1->{host},
            default_email(
                $h1->{email},    '^(\s*|admin@hackerpublicradio.org)$',
                $email_template, $h1->{hostid}
            ),
            nullif( $h1->{profile}, '^\s*$' ),
            $h1->{license},
            $h1->{local_image},
            nullif( $h1->{gpg}, '^\s*$' ),
            $h1->{valid},
        );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Copied $count records\n" if ( $verbose > 0 );

    #
    # Set the sequence to the correct value
    #
    alter_seq( $dbh2, 'hosts', 'host_seq' );

}

#===  FUNCTION  ================================================================
#         NAME: build_episodes_hosts_xref_table
#      PURPOSE: Generates the cross reference table by examining the 'eps' and
#               'hosts' tables in the MariaDB database.
#   PARAMETERS: $dbh1           Handle for the MariaDB table
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub build_episodes_hosts_xref_table {
    my ( $dbh1, $dbh2, $verbose ) = @_;

    my ( $sth1, $h1, $sth2, $count );

    $sth1
        = $dbh1->prepare(
        'SELECT e.id,h.hostid FROM eps e, hosts h WHERE e.hostid = h.hostid')
        or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    $sth2 = $dbh2->prepare('INSERT INTO episodes_hosts_xref VALUES (?,?)')
        or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute( $h1->{id}, $h1->{hostid}, );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Copied $count records\n" if ($verbose > 0);

}

#===  FUNCTION  ================================================================
#         NAME: build_series_table
#      PURPOSE: Copy the data from the Mariadb 'miniseries' table to the Pg
#               'series' table
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub build_series_table {
    my ( $dbh1, $dbh2, $verbose ) = @_;

    my ( $sth1, $h1, $sth2, $count );

    $sth1 = $dbh1->prepare('SELECT * FROM miniseries') or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    $sth2 = $dbh2->prepare('INSERT INTO series VALUES (?,?,?,?,?,?)')
        or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    #
    # Query MariaDB for the entire 'miniseries' table
    #
    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    #
    # Loop though 'miniseries' table rows writing them to the PostgreSQL
    # 'series' table
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute(
            $h1->{id}, $h1->{name},
            $h1->{description}, $h1->{private},
            nullif( $h1->{image}, '^\s*$' ),
            $h1->{valid},
        );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Copied $count records\n" if ($verbose > 0);

    #
    # Set the sequence to the correct value
    #
    alter_seq($dbh2,'series','series_seq');

}

#===  FUNCTION  ================================================================
#         NAME: build_episodes_series_xref_table
#      PURPOSE: Generates the cross reference table by examining the 'eps' and
#               'series' tables in the MariaDB database.
#   PARAMETERS: $dbh1           Handle for the MariaDB table
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub build_episodes_series_xref_table {
    my ( $dbh1, $dbh2, $verbose ) = @_;

    my ( $sth1, $h1, $sth2, $count );

    $sth1
        = $dbh1->prepare(
        'SELECT e.id AS epid, m.id AS msid FROM eps e, miniseries m WHERE e.series = m.id')
        or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    $sth2 = $dbh2->prepare('INSERT INTO episodes_series_xref VALUES (?,?)')
        or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute( $h1->{epid}, $h1->{msid}, );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Copied $count records\n" if ($verbose > 0);

}

#===  FUNCTION  ================================================================
#         NAME: build_tags_table
#      PURPOSE: Using the data structure built from the MariaDB database
#               populate the many-to-many table in the Pg database
#   PARAMETERS: $dbh            Handle for the Pg database
#               $verbose        Verbosity level
#               $tag_hash       Reference to a hash of episode ids and tags
#                               for each episode
#      RETURNS: Nothing
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub build_tags_table {
    my ( $dbh, $verbose, $tag_hash ) = @_;

    my ( $sth1, $h1, $sth2, $sth3, $rv, $tags, $tid, $count1, $count2 );

    #
    # Query to find if a tag already exists
    #
    $sth1 = $dbh->prepare(q{SELECT * FROM tags WHERE tag = ?});

    #
    # Query to add a new tag
    #
    $sth2 = $dbh->prepare(q{INSERT INTO tags (tag) VALUES(?)});

    #
    # Query to add a new joining row
    #
    $sth3 = $dbh->prepare(q{INSERT INTO episodes_tags_xref VALUES(?,?)});

    $count1 = $count2 = 0;
    foreach my $id ( sort { $a <=> $b } keys( %{$tag_hash} ) ) {
        #
        # Get the array of tags for this episode id
        #
        $tags = $tag_hash->{$id};

        #
        # Loop through the array of tags (using an integer so we can index the
        # current tag)
        #
        for my $i ( 0 .. $#$tags ) {
            #
            # Look to see if this tag exists
            #
            $sth1->execute( $tags->[$i] );
            if ( $dbh->err ) {
                warn $dbh->errstr;
            }

            #
            # If it's already in the table just store the id for later
            # otherwise add a new entry
            #
            if ( $h1 = $sth1->fetchrow_hashref ) {
                $tid = $h1->{id};
            }
            else {
                #
                # Add the tag to 'tags'
                #
                $count1++;
                $rv = $sth2->execute( $tags->[$i] );
                if ( $dbh->err ) {
                    warn $dbh->errstr;
                }
                $rv = 0 if ( $rv eq '0E0' );

                #
                # Get the id number of the inserted tag
                #
                $tid = $dbh->last_insert_id( undef, undef, undef, undef,
                    { sequence => 'tag_seq' } );
            }

            $count2++;
            $rv = $sth3->execute( $id, $tid );
            if ( $dbh->err ) {
                warn $dbh->errstr;
            }
            $rv = 0 if ( $rv eq '0E0' );
        }
    }

    if ($verbose > 0) {
        print "Added $count1 tags\n";
        print "Added $count2 cross references\n";
    }

}

#===  FUNCTION  ================================================================
#         NAME: collect_eps_tags
#      PURPOSE: Collects the tags from the eps.tags field
#   PARAMETERS: $dbh            Database handle
#               $verbose        Verbosity level
#      RETURNS: A reference to the hash created by collecting all the tags
#  DESCRIPTION: Queries the MariaDB 'eps' table for all of the rows containing
#               tags, returning the comma-separated list with the id number.
#               Each CSV list is then parsed and the result turned into a hash
#               keyed on the id number and containing a sorted array of tags.
#               If the level of verbosity is greater than 2 the tags has is
#               dumped (ironically, as a CSV list!).
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub collect_eps_tags {
    my ( $dbh, $verbose ) = @_;

    my ( $status, @fields, %hash );
    my ( $sth, $h );

    #
    # For parsing the field as CSV
    #
    my $csv = Text::CSV_XS->new;

    #
    # Query the MariaDB 'eps' table for all the id and tags
    #
    $sth = $dbh->prepare(
        q{SELECT id,tags FROM eps
            WHERE length(tags) > 0
            ORDER BY id}
    ) or die $DBI::errstr;
    if ( $dbh->err ) {
        warn $dbh->errstr;
    }

    $sth->execute;
    if ( $dbh->err ) {
        warn $dbh->errstr;
    }

    #
    # Loop through what we got
    #
    while ( $h = $sth->fetchrow_hashref ) {
        #
        # Parse the tag list
        #
        $status = $csv->parse( $h->{tags} );
        unless ($status) {
            #
            # Report any errors
            #
            print "Parse error on episode ", $h->{id}, "\n";
            print $csv->error_input(), "\n";
            next;
        }
        @fields = $csv->fields();

        next unless (@fields);

        #
        # Trim all tags (don't alter $_ when doing it)
        #
        @fields = map {
            my $t = $_;
            $t =~ s/(^\s+|\s+$)//g;
            $t;
        } @fields;

        #print "$h->{id}: ",join(",",@fields),"\n";

        #
        # Save the id and its tags, sorted for comparison
        #
        $hash{ $h->{id} } = [ sort @fields ];

    }

    #
    # Dump all id numbers and tags if the verbose level is high enough
    #
    if ( $verbose >= 2 ) {
        print "\nTags collected from the 'eps' table\n\n";
        foreach my $id ( sort { $a <=> $b } keys(%hash) ) {
            printf "%04d: %s\n", $id, join( ",", @{ $hash{$id} } );
        }
    }

    return \%hash;

}

#===  FUNCTION  ================================================================
#         NAME: build_comments_table
#      PURPOSE: Copy the data from the Mariadb 'comments' table to the Pg
#               'comments' table
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub build_comments_table {
    my ( $dbh1, $dbh2, $verbose ) = @_;

    my ( $sth1, $h1, $sth2, $count );

    $sth1 = $dbh1->prepare('SELECT * FROM comments') or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    $sth2 = $dbh2->prepare('INSERT INTO comments VALUES (?,?,?,?,?,?,?)')
        or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    #
    # Query MariaDB for the entire 'comments' table
    #
    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    #
    # Loop though 'comments' table rows writing them to the PostgreSQL
    # 'comments' table
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute(
            $h1->{id},
            $h1->{eps_id},
            $h1->{comment_timestamp},
            nullif( $h1->{comment_author_name}, '^\s*$' ),
            nullif( $h1->{comment_title},       '^\s*$' ),
            $h1->{comment_text},
            $h1->{last_changed},
        );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Copied $count records\n" if ($verbose > 0);

    #
    # Set the sequence to the correct value
    #
    alter_seq($dbh2,'comments','comment_seq');

}

#===  FUNCTION  ================================================================
#         NAME: load_twat_hosts
#      PURPOSE: Copy the temporary twat_hosts table from the MySQL database
#               into the PostgreSQL equivalent ready for merging with the HPR
#               data
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub load_twat_hosts {
    my ( $dbh1, $dbh2, $verbose ) = @_;

    my ( $sth1, $h1, $sth2, $count );

    #
    # Copy the 'twat_hosts' table
    #
    $sth1 = $dbh1->prepare('SELECT * FROM twat_hosts') or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Omit the 'id' here because it's an addition for PostgreSQL
    #
    $sth2
        = $dbh2->prepare(
        'INSERT INTO twat_hosts (host,email,website,repeat) VALUES (?,?,?,?)')
        or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    #
    # Query MariaDB for the entire 'comments' table
    #
    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    #
    # Loop though 'twat_hosts' table rows writing them to the PostgreSQL
    # 'twat_hosts' table
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute(
            $h1->{host},
            nullif( $h1->{email},   '^\s*$' ),
            nullif( $h1->{website}, '^\s*$' ),
            $h1->{repeat},
        );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Copied $count records\n" if ( $verbose > 0 );

}

#===  FUNCTION  ================================================================
#         NAME: load_twat_episodes
#      PURPOSE: Copy the temporary twat_episodes table from the MySQL database
#               into the PostgreSQL equivalent ready for merging with the HPR
#               data
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub load_twat_episodes {
    my ( $dbh1, $dbh2, $verbose ) = @_;

    my ( $sth1, $h1, $sth2, $count );

    #
    # Copy the 'twat_episodes' table
    #
    $sth1 = $dbh1->prepare('SELECT * FROM twat_episodes') or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Omit the 'id' here because it's an addition for PostgreSQL
    #
    $sth2
        = $dbh2->prepare(
        'INSERT INTO twat_episodes VALUES (?,?,?,?,?,?)')
        or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    #
    # Query MariaDB for the entire 'comments' table
    #
    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    #
    # Loop though 'twat_hosts' table rows writing them to the PostgreSQL
    # 'twat_hosts' table
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute(
            $h1->{ep_num},
            $h1->{date},
            $h1->{host},
            $h1->{topic},
            nullif( $h1->{writeup}, '^\s*$' ),
            $h1->{url},
        );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Copied $count records\n" if ( $verbose > 0 );

}

#===  FUNCTION  ================================================================
#         NAME: compute_host_date_added
#      PURPOSE: Determine the 'hosts.date_added' field once the database is
#               fully populated.
#   PARAMETERS: $dbh            Handle for the PostgreSQL database
#      RETURNS: Nothing
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub compute_host_date_added {
    my ($dbh) = @_;

    my $rv;

    #
    # Allocate date_added values where possible
    #
    $rv = $dbh->do(
        q{
        UPDATE hosts
            SET date_added = sq.date_added
        FROM (
            SELECT h.id,min(e.release_date) AS date_added
            FROM episodes e
            JOIN episodes_hosts_xref eh ON (e.id = eh.episodes_id)
            JOIN hosts h ON (h.id = eh.hosts_id)
            GROUP BY h.id
            ORDER by min(e.release_date)) AS sq
        WHERE hosts.id = sq.id
    }
    );
    if ( $dbh->err ) {
        warn $dbh->errstr;
    }
    $rv = 0 if ( $rv eq '0E0' );

    print "Added $rv dates to the 'date_added' column\n";

}

#===  FUNCTION  ================================================================
#         NAME: alter_seq
#      PURPOSE: Ensure the PostgreSQL sequence associated with a table has the
#               correct value.
#   PARAMETERS: $dbh            Handle for the PostgreSQL database
#               $table          Table name for the query
#               $sequence       Sequence name
#      RETURNS: Nothing
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub alter_seq {
    my ( $dbh, $table, $sequence ) = @_;

    my ( $sth, $h, $rv, $maxid );

    #
    # Find the maximum id number in the table
    #
    $sth = $dbh->prepare("SELECT max(id) as maxid FROM $table")
        or die $DBI::errstr;
    if ( $dbh->err ) {
        warn $dbh->errstr;
    }

    $sth->execute;
    if ( $dbh->err ) {
        die $dbh->errstr;
    }

    #
    # Save the maximum
    #
    if ( $h = $sth->fetchrow_hashref ) {
        $maxid = $h->{maxid};
        $sth->finish;
    }

    #
    # Reset the sequence one more than the maximum
    #
    $maxid++;
    $rv = $dbh->do("ALTER SEQUENCE $sequence RESTART WITH $maxid");
    if ( $dbh->err ) {
        warn $dbh->errstr;
    }
    $rv = 0 if ( $rv eq '0E0' );

    warn "Failed to reset $sequence\n" unless (defined($rv));

    return;
}

#===  FUNCTION  ================================================================
#         NAME: check_table
#      PURPOSE: Check that a given PostgreSQL table is empty
#   PARAMETERS: $dbh            Handle for the PostgreSQL database
#               $table          Name of table
#      RETURNS: True if empty, otherwise false
#  DESCRIPTION: Simply perform a query on the nominated table which counts
#               rows. If the table does not exist a DBI method will fail (the
#               execute?), so we treat this as a 'no empty' to make the caller
#               take error action.
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub check_table {
    my ( $dbh, $table ) = @_;

    my ( $sth, $h, $count );

    $sth = $dbh->prepare("SELECT count(*) AS count FROM $table")
        or die $DBI::errstr;
    if ( $dbh->err ) {
        warn $dbh->errstr;
        return 0;
    }

    $sth->execute;
    if ( $dbh->err ) {
        warn $dbh->errstr;
        return 0;
    }

    if ( $h = $sth->fetchrow_hashref ) {
        $count = $h->{count};
        $sth->finish;
    }

    return $count == 0;
}

#===  FUNCTION  ================================================================
#         NAME: default_email
#      PURPOSE: Make a default email address for hosts with none
#   PARAMETERS: $email          Original email address
#               $regex          Regular expression to check the email against
#               $template       Template for building the default
#               $hostid         Host id number to use in the default
#      RETURNS: The email address to be used
#  DESCRIPTION: If the email address matches a regular expression then
#               generate a default from the template and the host id,
#               otherwise just return the address untouched.
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub default_email {
    my ( $email, $regex, $template, $hostid ) = @_;

    return (
        $email =~ $regex
        ? sprintf( $template, $hostid )
        : $email
    );
}

#===  FUNCTION  ================================================================
#         NAME: nullif
#      PURPOSE: Tests a value and makes it 'undef' (equivalent to NULL in the
#               database) if it matches a regular expression.
#   PARAMETERS: $value          Value to test
#               $regex          Regular expression to match against
#      RETURNS: 'undef' if the values match, otherwise the original value
#  DESCRIPTION: This is very simple, just a wrapper around the test.
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub nullif {
    my ( $value, $regex ) = @_;

    return $value unless defined($value);
    return ( $value =~ $regex ? undef : $value );
}

#===  FUNCTION  ================================================================
#         NAME: Options
#      PURPOSE: Processes command-line options
#   PARAMETERS: $optref     Hash reference to hold the options
#      RETURNS: Undef
#  DESCRIPTION:
#       THROWS: no exceptions
#     COMMENTS: none
#     SEE ALSO: n/a
#===============================================================================
sub Options {
    my ($optref) = @_;

    my @options = ( "help", "debug=i", "dry-run!", "verbose+", "phases=s" );

    if ( !GetOptions( $optref, @options ) ) {
        pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 );
    }

    return;
}

__END__

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#  Application Documentation
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
#{{{

=head1 NAME

copy_mysql_pg - copy the HPR database from MySQL to PostgreSQL

=head1 VERSION

This documentation refers to B<copy_mysql_pg> version 0.0.4


=head1 USAGE

 copy_mysql_pg -verbose
 copy_mysql_pg -verbose -verbose
 copy_mysql_pg -verbose \
    -phase='episodes,hosts,eh_xref,series,es_xref,tags,comments,epilogue'

=head1 OPTIONS

=over 8

=item B<-help>

Prints a brief help message describing the usage of the program, and then exits.

=item B<-debug=N>

Selects a level of debugging. Debug information consists of a line or series
of lines prefixed with the characters 'D>':

=over 4

=item B<0>

No debug output is generated: this is the default

=back

=item B<-[no]dry-run>

When enabled (B<-dry-run>) the script will report what it would do, but will
make no changes to the target database. In the default state (B<-nodry-run>)
then changes are made.

=item B<-verbose>

Makes the script verbose resulting in the production of more information about
what it is doing.

The option may be repeated to increase the level of verbosity. The levels are:

=over 4

=item B<0>

No output is generated (apart from errors and warnings if appropriate). This
is the default level.

=item B<1>

A message is generated per phase to indicate which actions are taking place.
This includes a report of the number of rows copied from the MySQL database to
the PostgreSQL one.

=item B<2>

Following the process of collecting the CSV tags from the MySQL 'episodes' table
these are reported as a list per episode. This output will be long!

=back

=item B<-phase=CSV_LIST>

This option allows the phases of the copying process to be selected
individually. The argument B<CSV_LIST> is a list of phase names, which have to
be typed exactly. The order is not important since the script will scan its
version of the list of phases in order and will check to see if each has been
selected.

The phase names are:

=over 4

=item B<episodes>

Causes the B<episodes> table to be filled.

=item B<hosts>

Causes the B<hosts> table to be filled.

=item B<eh_xref>

Causes the B<episodes_hosts_xref> table to be filled.

=item B<series>

Causes the B<series> table to be filled.

=item B<es_xref>

Causes the B<episodes_series_xref> table to be filled.

=item B<tags>

Causes the B<tags> and the B<episodes_tags_xref> tables to be filled.

=item B<comments>

Causes the B<comments> table to be filled.

=item B<epilogue>

Runs various tasks that can only be carried out after the database has been
populated.

=back

=back

=head1 DESCRIPTION

A full description of the application and its features.
May include numerous subsections (i.e. =head2, =head3, etc.)


=head1 DIAGNOSTICS

A list of every error and warning message that the application can generate
(even the ones that will "never happen"), with a full explanation of each
problem, one or more likely causes, and any suggested remedies. If the
application generates exit status codes (e.g. under Unix) then list the exit
status associated with each error.


=head1 CONFIGURATION AND ENVIRONMENT

A full explanation of any configuration system(s) used by the application,
including the names and locations of any configuration files, and the
meaning of any environment variables or properties that can be set. These
descriptions must also include details of any configuration language used


=head1 DEPENDENCIES

A list of all the other modules that this module relies upon, including any
restrictions on versions, and an indication whether these required modules are
part of the standard Perl distribution, part of the module's distribution,
or must be installed separately.


=head1 INCOMPATIBILITIES

A list of any modules that this module cannot be used in conjunction with.
This may be due to name conflicts in the interface, or competition for
system or program resources, or due to internal limitations of Perl
(for example, many modules that use source code filters are mutually
incompatible).


=head1 BUGS AND LIMITATIONS

A list of known problems with the module, together with some indication
whether they are likely to be fixed in an upcoming release.

Also a list of restrictions on the features the module does provide:
data types that cannot be handled, performance issues and the circumstances
in which they may arise, practical limitations on the size of data sets,
special cases that are not (yet) handled, etc.

The initial template usually just has:

There are no known bugs in this module.
Please report problems to <Maintainer name(s)>  (<contact address>)
Patches are welcome.

=head1 AUTHOR

<Author name(s)>  (<contact address>)


=head1 LICENCE AND COPYRIGHT

Copyright (c) <year> <copyright holder> (<contact address>). All rights reserved.

Followed by whatever licence you wish to release it under.
For Perl code that is often just:

This module is free software; you can redistribute it and/or
modify it under the same terms as Perl itself. See perldoc perlartistic.

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.

=cut

#}}}

# [zo to open fold, zc to close]

# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker

