#!/usr/bin/env perl
#===============================================================================
#
#         FILE: copy_mysql_pg_2
#
#        USAGE: ./copy_mysql_pg_2
#
#  DESCRIPTION: Copies HPR show data from the MariaDB database to an
#               experimental PostgreSQL database (second version "HPR2")
#
#      OPTIONS: ---
# REQUIREMENTS: ---
#         BUGS: ---
#        NOTES: ---
#       AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
#      VERSION: 0.1.5
#      CREATED: 2017-10-23 19:11:48
#     REVISION: 2019-06-01 16:10:23
#
#===============================================================================

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 SQL::Abstract;

use Data::Dumper;

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

#
# 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/PostgreSQL_Database";
my $configfile1 = "$basedir/.hpr_db.cfg";
my $configfile2 = "$basedir/.hpr_pg2.cfg";
my $database3   = "$basedir/ia.db";     # soft link

my $email_template  = 'host_%s@hackerpublicradio.org';
my $default_licence = 'CC-BY-SA';

my ( $dbh1, $sth1, $h1, $rv1 );
my ( $dbh2, $sth2, $h2, $rv2 );
my ( $dbh3, $sth3, $h3, $rv3 );

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

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

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

#-------------------------------------------------------------------------------
# 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 $cfgfile1
    = ( defined( $options{config} ) ? $options{config} : $configfile1 );
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;
}

#-------------------------------------------------------------------------------
# Configuration file for MySQL/MariaDB - load data
#-------------------------------------------------------------------------------
my $conf1 = Config::General->new(
    -ConfigFile      => $cfgfile1,
    -InterPolateVars => 1,
    -ExtendedAccess  => 1
);
my %config1 = $conf1->getall();

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

#-------------------------------------------------------------------------------
# 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;

#-------------------------------------------------------------------------------
# Connect to the SQLite database
#-------------------------------------------------------------------------------
$dbh3 = DBI->connect( "dbi:SQLite:dbname=$database3", "", "" );


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, $email_template, $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 MySQL '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";
        }
    }
    #---------------------------------------------------------------------------
    # Fill in archive-related fields the 'episodes' table from 'ia.db'
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'archived' && exists( $choices{$phase} ) ) {
        print "Copy archive-related fields to episodes table\n" if ( $verbose > 0 );
        update_episodes_table( $dbh3, $dbh2, $verbose ) unless $dry_run;
    }
    #---------------------------------------------------------------------------
    # Copy the 'assets' table in 'ia.db' to 'assets'
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'assets' && exists( $choices{$phase} ) ) {
        print "Build assets table\n" if ( $verbose > 0 );
        if (check_table($dbh2,'assets')) {
            build_assets_table( $dbh3, $dbh2, $verbose ) unless $dry_run;
        }
        else {
            print "** Table 'assets' is not empty\n";
        }
    }
    #---------------------------------------------------------------------------
    # Perform the 'epilogue' actions
    #---------------------------------------------------------------------------
    elsif ( $phase eq 'epilogue' && exists( $choices{$phase} ) ) {
        print "Perform epilogue actions\n" if ( $verbose > 0 );

        #
        # Resolve the "double host" problems. We have *not* copied these
        # across from the MySQL 'hosts' table, so can resolve them now.
        #
        resolve_double_hosts( $dbh1, $dbh2, $email_template, $default_licence,
            $verbose ) unless $dry_run;

        #
        # 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 $count = 0;

    $count += load_twat_episodes_table( $dbh1, $dbh2, 'twat', $verbose );
    $count += load_eps_table( $dbh1, $dbh2, 'hpr', $verbose );

    print "Added $count records to the 'episodes' table\n"
        if ( $verbose > 0 );

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

}

#===  FUNCTION  ================================================================
#         NAME: load_twat_episodes_table
#      PURPOSE: Load the 'twat_episodes' table from the MariaDB database into
#               the PostgreSQL 'episodes' table.
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $key_prefix     String to prefix the 'episode_key' field with
#               $verbose        Verbosity level
#      RETURNS: Number of rows added
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub load_twat_episodes_table {
    my ( $dbh1, $dbh2, $key_prefix, $verbose ) = @_;

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

    #
    # Query to collect the entire 'twat_episodes' table
    #
    $sth1 = $dbh1->prepare('SELECT * FROM twat_episodes ORDER BY ep_num')
        or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Query to insert into the PostgreSQL 'episodes' table
    #
    $sth2 = $dbh2->prepare(
        q{INSERT INTO episodes
            (episode_key, release_date, title, notes, explicit, license)
            VALUES (?,date(to_timestamp(?)),?,?,TRUE,
            id_in_licenses('CC-BY-SA'))}
    ) or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

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

    #
    # Loop though 'twat_episodes' table rows writing them to the PostgreSQL
    # 'episodes' table
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute(
            sprintf( "%s%04d", $key_prefix, $h1->{ep_num} ),
            $h1->{date},
            coalesce( $h1->{topic},   '[undefined]' ),
            coalesce( $h1->{writeup}, '<p>No notes</p>' ),
        );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

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

    return $count;
}

#===  FUNCTION  ================================================================
#         NAME: load_eps_table
#      PURPOSE: Load the 'eps' table from the MariaDB database into the
#               PostgreSQL 'episodes' table.
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $key_prefix     String to prefix the 'episode_key' field with
#               $verbose        Verbosity level
#      RETURNS: Number of rows added
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub load_eps_table {
    my ( $dbh1, $dbh2, $key_prefix, $verbose ) = @_;

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

    #
    # Query to collect rows from the old dfatabase
    #
    $sth1 = $dbh1->prepare('SELECT * FROM eps ORDER BY id')
        or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Query to insert rows into the new database
    #
    $sth2 = $dbh2->prepare(
        q{INSERT INTO episodes
            (episode_key, release_date, title, summary, notes, explicit,
            license, duration, downloads)
            VALUES (?,?,?,?,?,?,id_in_licenses(?),? * '1 second'::interval,?)}
    ) 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(
            sprintf( "%s%04d", $key_prefix, $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 from 'eps'\n" if ( $verbose > 0 );

    return $count;
}

#===  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
#               $template       Template for building the default email
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION: Copies records from one table into the other. However, some
#               transformations are made along the way: blank emails and
#               'admin@hackerpublicradio.org' are converted to 'host_NNN@hpr',
#               empty profiles and GPG strings are turned into NULL values,
#               and licence settings are converted to the id of the licence
#               in the 'licenses' table (in the database).
#               The double host problem is an issue though. Since the email
#               address field is unique we get errors since the double hosts
#               have duplicate addresses.
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub build_hosts_table {
    my ( $dbh1, $dbh2, $template, $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
            (host_id, host, email, profile, license, local_image, gpg, espeak_name)
            VALUES (?,?,?,?,id_in_licenses(?),?,?,?)}
    ) 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 ) {
        #
        # Ignore "double host" entries
        #
        if ( $h1->{host} !~ /^(.+)\s+and\s+(.+)$/ ) {
            $count++;
            $sth2->execute(
                $h1->{hostid},
                $h1->{host},
                default_email(
                    $h1->{email}, '^(\s*|admin@hackerpublicradio.org)$',
                    $template,    $h1->{hostid}
                ),
                nullif( $h1->{profile}, '^\s*$' ),
                $h1->{license},
                $h1->{local_image},
                nullif( $h1->{gpg}, '^\s*$' ),
                $h1->{espeak_name},
            );
            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_id', '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 $count = 0;

    $count += link_twat_episodes( $dbh1, $dbh2, $verbose );
    $count += link_hpr_episodes( $dbh1, $dbh2, $verbose );

    print "Added a total of $count links\n" if ( $verbose > 0 );

}

#===  FUNCTION  ================================================================
#         NAME: link_twat_episodes
#      PURPOSE: Generate cross reference links between TwaT episodes and hosts
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: The count of links
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub link_twat_episodes {
    my ( $dbh1, $dbh2, $verbose ) = @_;

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

    #
    # Query to find the TwaT episode and hostid from the old database. Use
    # LEFT JOIN so we see when there's no host match.
    #
    $sth1 = $dbh1->prepare(
#        q{SELECT t.ep_num,h.hostid
#            FROM twat_episodes t, hosts h
#            WHERE t.host = h.host}
        q{SELECT t.ep_num, h.hostid
            FROM twat_episodes t
            LEFT JOIN hosts h ON (t.host = h.host)}
    ) or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Query to add a cross reference link to the new database
    #
    $sth2 = $dbh2->prepare(
        q{INSERT INTO episodes_hosts_xref
            SELECT e.episode_id, h.host_id
            FROM episodes e, hosts h
            WHERE e.episode_key = ? AND h.host_id = ?}
    ) or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    #
    # Collect all the TwaT episodes and host id numbers
    #
    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    #
    # Loop through the result of the query, adding links to the new database.
    # If the returned hostid is NULL then we don't have this host in the
    # 'hosts' table and need to warn about it.
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $episode_key = sprintf( "twat%04d", $h1->{ep_num} );

        if (defined($h1->{hostid})) {
            $count++;
            $sth2->execute( $episode_key, $h1->{hostid}, );
            if ( $dbh2->err ) {
                die $dbh2->errstr;
            }
        }
        else {
            warn "Unable to link $episode_key to a host\n";
        }
    }

    print "Added $count links to TwaT episodes\n" if ( $verbose > 0 );

    return $count;
}

#===  FUNCTION  ================================================================
#         NAME: link_hpr_episodes
#      PURPOSE: Generate cross reference links between HPR episodes and hosts
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: The count of links
#  DESCRIPTION:
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub link_hpr_episodes {
    my ( $dbh1, $dbh2, $verbose ) = @_;

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

    #
    # Query to find the HPR episode and hostid from the old database
    #
    $sth1 = $dbh1->prepare(
        q{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;
    }

    #
    # Query to add a cross reference link to the new database
    #
    $sth2 = $dbh2->prepare(
        q{INSERT INTO episodes_hosts_xref
            SELECT e.episode_id, h.host_id
            FROM episodes e, hosts h
            WHERE e.episode_key = ? AND h.host_id = ?}
    ) or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    #
    # Collect all the HPR episodes and host id numbers
    #
    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    #
    # Loop through the result of the query, adding links to the new database
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $episode_key = sprintf( "hpr%04d", $h1->{id} );
        $sth2->execute( $episode_key, $h1->{hostid}, );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Added $count links to HPR episodes\n" if ( $verbose > 0 );

    return $count;

}

#===  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_id', '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, $episode_key );

    #
    # Query to find the HPR episode and series id from the old database
    #
    $sth1 = $dbh1->prepare(
        q{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;
    }

    #
    # Query to add a cross reference link to the new database
    #
    $sth2 = $dbh2->prepare(
        q{INSERT INTO episodes_series_xref
            SELECT e.episode_id, s.series_id
            FROM episodes e, series s
            WHERE e.episode_key = ? AND s.series_id = ?}
    ) or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

    #
    # Collect all the HPR episodes and host id numbers (no TwaT shows are in
    # series)
    #
    $sth1->execute;
    if ( $dbh1->err ) {
        die $dbh1->errstr;
    }

    #
    # Loop through the result of the query, adding links to the new database
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $episode_key = sprintf( "hpr%04d", $h1->{epid} );
        $sth2->execute( $episode_key, $h1->{msid} );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Added a total of $count series links\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: Before being called the tags in the MariaDB database are
#               gathered into a hash which is passed to this function as an
#               argument. The hash is keyed by episode number and each value
#               consists of an arrayref containing the tag strings. These tags
#               are then processed to add to the PostgreSQL database.
#               For each episode (in sorted order) the tag array is processed.
#               If a tag already exists in the 'tags' table the tag id is
#               stored for later, otherwise the tag is added to the 'tags'
#               table. The tag id from either source is used to add to the
#               cross reference table 'episodes_tags_xref'.
#               Adding a link requires the provision of the episode key, which
#               in this case will be 'hprNNNN' where 'NNNN' is a 4 digit zero
#               padded number. This is converted to the episode id by
#               a PostgreSQL function.
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub build_tags_table {
    my ( $dbh, $verbose, $tag_hash ) = @_;

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

    #
    # Query to find if a tag already exists in the PostgreSQL database
    #
    $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(id_in_episodes(?),?)}
    );

    $count1 = $count2 = 0;
    foreach my $id ( sort { $a <=> $b } keys( %{$tag_hash} ) ) {
        #
        # The episode key is now more than a number
        #
        $episode_key = sprintf("hpr%04d", $id);

        #
        # 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->{tag_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( $episode_key, $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 hash 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 );

    #
    # Query to collect comments from the old database
    #
    $sth1 = $dbh1->prepare('SELECT * FROM comments') or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Query to insert comment data into the new database
    #
    $sth2 = $dbh2->prepare(
        q{INSERT INTO comments VALUES (?,id_in_episodes(?),?,?,?,?,?)}
    ) 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},
            sprintf("hpr%04d",$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 comments\n" if ($verbose > 0);

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

}

#===  FUNCTION  ================================================================
#         NAME: update_episodes_table
#      PURPOSE: Initialise archiv-related fields in the Pg 'episodes' table
#               from the SQLite 'episodes' table
#   PARAMETERS: $dbh1           Handle for the SQLite database
#               $dbh2           Handle for the Pg database
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub update_episodes_table {
    my ( $dbh1, $dbh2, $verbose ) = @_;

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

    #
    # Query to collect episodes from the SQLite database
    #
    $sth1 = $dbh1->prepare('SELECT * FROM episodes') or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Query to insert fields into the Pg database 'episodes' table. We just
    # write whatever comes in, no check for existing data.
    #
    $sth2 = $dbh2->prepare(
        q{UPDATE episodes SET
            archived = ?,
            archive_date = ?,
            IA_URL = ?
          WHERE episode_key = ?}
    ) or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

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

    #
    # Loop though 'episodes' table rows writing selected fields to the
    # PostgreSQL 'episodes' table
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute(
            $h1->{uploaded},
            $h1->{archive_date},
            $h1->{IA_URL},
            sprintf("hpr%04d",$h1->{id}),
        );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

    print "Updated $count rows in the episodes table\n" if ($verbose > 0);

}

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

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

    #
    # Query to collect assets from the SQLite database
    #
    $sth1 = $dbh1->prepare('SELECT * FROM assets') or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Query to insert asset data into the new database
    #
    $sth2 = $dbh2->prepare(
        q{INSERT INTO assets VALUES (DEFAULT,id_in_episodes(?),?,?,?)}
    ) or die $DBI::errstr;
    if ( $dbh2->err ) {
        warn $dbh2->errstr;
    }

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

    #
    # Loop though 'assets' table rows writing them to the PostgreSQL
    # 'assets' table
    #
    $count = 0;
    while ( $h1 = $sth1->fetchrow_hashref ) {
        $count++;
        $sth2->execute(
            sprintf("hpr%04d",$h1->{episode_id}),
            $h1->{URL},
            $h1->{filename},
            $h1->{uploaded},
        );
        if ( $dbh2->err ) {
            die $dbh2->errstr;
        }
    }

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

    #
    # Set the sequence to the correct value
    #
    alter_seq( $dbh2, 'assets', 'asset_id', 'asset_seq' );

}

#===  FUNCTION  ================================================================
#         NAME: resolve_double_hosts
#      PURPOSE: Turn any double host entries in the 'hosts' table into
#               "singletons"
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $template       Template for building the default email
#               $licence        string version of the short CC licence name
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub resolve_double_hosts {
    my ( $dbh1, $dbh2, $template, $licence, $verbose ) = @_;

    my ( $sth1, $h1, $rv1, $sth2, $h2, $rv2, $sth3, $h3, $rv3, $sth4, $h4, $rv4 );

    my ( %doubles, @h, %hosts, $count, $unknown );

    #
    # Search the 'hosts' table in the old database for "host1 and host2"
    # strings
    #
    $count = find_double_hosts($dbh1, \%doubles, \%hosts);

    #
    # If no doubles there's nothing to do
    #
    if ($count == 0) {
        print "No doubles found\n" if ($verbose > 0);
        return;
    }

    #
    # Scan the list of individual hosts we stored earlier and find them in
    # the 'hosts' table (assuming that the hostnames we have extracted match
    # exactly)
    #
    $unknown = find_hosts($dbh1, \%doubles, \%hosts);

    #
    # Allocate all unknown hosts a host id in the PostgreSQL database, and give an
    # unique email address.
    #
    if ( $unknown > 0 ) {
        register_unknown($dbh2, \%doubles, \%hosts, $template,
            $licence);
    }

    #
    # Now %doubles contains all the original names and host ids and %hosts
    # contains the parsed out names and their ids. We can look for shows
    # attributed to the double hosts and re-attribute them to the single
    # hosts.
    #
    link_double_twat_episodes($dbh1,$dbh2,\%doubles,$verbose);
    link_double_hpr_episodes($dbh1,$dbh2,\%doubles,$verbose);

    return;

}

#===  FUNCTION  ================================================================
#         NAME: find_double_hosts
#      PURPOSE: Search the 'hosts' table in the old database for "host1 and
#               host2" strings.
#   PARAMETERS: $dbh            handle for the MariaDB database
#               $doubles        hashref where details will be stored
#               $hosts          hashref to contain single host details
#      RETURNS: The number of double hosts found
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub find_double_hosts {
    my ($dbh, $doubles, $hosts) = @_;

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

    #
    # Find the double hosts in the old database
    #
    $sth = $dbh->prepare(
        q{ SELECT hostid, host FROM hosts
        WHERE host regexp '[[:<:]]and[[:>:]]'
        ORDER BY hostid
        }
    ) or die $DBI::errstr;
    if ( $dbh->err ) {
        warn $dbh->errstr;
        return 0;
    }

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

    #
    # Loop through the doubles we found, counting them as we go
    #
    $count = 0;
    while ( $h = $sth->fetchrow_hashref ) {
        #
        # Each hash value is a hash containing the original id, and, in a sub-hash
        # the replacement ids
        #
        $doubles->{$h->{host}} = {
            double => $h->{hostid},
            singles => {},
        };

        #
        # Parse the double host string into an array
        #
        @h = ( $h->{host} =~ /^(.+)\s+and\s+(.+)$/ );

        #
        # Initialise the entries for %doubles and %hosts
        #
        for my $host (@h) {
            $doubles->{$h->{host}}->{singles}->{$host} = undef;
            unless ( exists( $hosts->{$host} ) ) {
                $hosts->{$host} = 0;
            }
        }

        $count++;
    }

    return $count;
}

#===  FUNCTION  ================================================================
#         NAME: find_hosts
#      PURPOSE: Find individual hosts in the old MariaDB database using the
#               names we found as double hosts
#   PARAMETERS: $dbh            handle for the MariaDB database
#               $doubles        hashref where details will be stored
#               $hosts          hashref to contain single host details
#      RETURNS: The number of unknown hosts
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub find_hosts {
    my ($dbh, $doubles, $hosts) = @_;

    my ($sth, $h, $rv, @h, $unknown);

    #
    # Query to find the host by name in the old database
    #
    $sth = $dbh->prepare(q{SELECT hostid FROM hosts WHERE host = ?})
        or die $DBI::errstr;
    if ( $dbh->err ) {
        warn $dbh->errstr;
    }

    #
    # Scan the list of individual hosts we stored earlier and find them in
    # the 'hosts' table (assuming that the hostnames we have extracted match
    # exactly)
    #
    $unknown = 0;
    foreach my $host ( sort(keys(%{$hosts})) ) {
        $rv = $sth->execute($host);
        if ( $dbh->err ) {
            die $dbh->errstr;
        }
        $rv = 0 if ( $rv eq '0E0' );

        if ($rv) {
            #
            # Found id for host. Save in the %doubles hash
            #
            $h = $sth->fetchrow_hashref;
            $hosts->{$host} = $h->{hostid};
            save_hostid($doubles,$host,$h->{hostid});
        }
        else {
            #
            # Can't find this host
            #
            $unknown++;
        }
    }

    return $unknown;
}

#===  FUNCTION  ================================================================
#         NAME: register_unknown
#      PURPOSE: Create an entry in the 'hosts' table of the new database
#               wherever there's a host that's unknown (has a zero id in the
#               %hosts hash).
#   PARAMETERS: $dbh            handle for the MariaDB database
#               $doubles        hashref where details will be stored
#               $hosts          hashref to contain single host details
#               $email_template template for making an email address
#               $licence        string version of the short CC licence name
#      RETURNS: Nothing
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub register_unknown {
    my ( $dbh, $doubles, $hosts, $email_template, $licence ) = @_;

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

    #
    # PostgreSQL query to register an unknown host
    #
    $sth = $dbh->prepare(
        q{INSERT INTO hosts (host,email,license)
        VALUES (?,?,id_in_licenses(?))}
    ) or die $DBI::errstr;
    if ( $dbh->err ) {
        warn $dbh->errstr;
    }

    foreach my $host ( sort( keys( %{$hosts} ) ) ) {
        if ( $hosts->{$host} == 0 ) {
            #
            # Write a row to the 'hosts' table. There has to be an email,
            # but we can't compute one until the row has been added.
            #
            $rv = $sth->execute( $host, 'placeholder', $licence );
            if ( $dbh->err ) {
                die $dbh->errstr;
            }

            #
            # Save the id number we just generated as the new host id
            #
            my $newid = $dbh->last_insert_id( undef, undef, undef, undef,
                { sequence => 'host_seq' } );
            $hosts->{$host} = $newid;
            save_hostid( $doubles, $host, $newid );

            printf "Created new host %s (%d)\n", $host, $newid
                if ( $verbose > 0 );

            #
            # Give the new host entry a default email address
            #
            $new_email = sprintf( $email_template, $newid );
            $rv = $dbh->do( 'UPDATE hosts SET email = ? WHERE host_id = ?',
                undef, $new_email, $newid );
            if ( $dbh->err ) {
                warn $dbh->errstr;
            }
            $rv = 0 if ( $rv eq '0E0' );

            warn "Failed to set email address $new_email for $host\n"
                unless ( defined($rv) );

        }
    }
}

#===  FUNCTION  ================================================================
#         NAME: link_double_twat_episodes
#      PURPOSE: Find and link TwaT episodes with double hosts
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $doubles        hashref holding double host details
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub link_double_twat_episodes {
    my ( $dbh1, $dbh2, $doubles, $verbose ) = @_;

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

    #
    # Query to find TwaT shows with particular host names in the old database
    #
    $sth = $dbh1->prepare(
        q{
            SELECT ep_num
            FROM twat_episodes
            WHERE host = ?
        }
    ) or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Loop through the double hosts we collected
    #
    foreach my $double ( sort( keys(%{$doubles}) ) ) {
        #
        # Extract saved double and corresponding single details
        #
        my ( $doubleid, @newhosts ) = (
            $doubles->{$double}->{double},
            keys( %{ $doubles->{$double}->{singles} } )
        );

        #
        # Find TwaT shows marked as belonging to this double-host (in the old
        # database)
        #
        $sth->execute($double);
        if ( $dbh1->err ) {
            die $dbh1->errstr;
        }

        #
        # Process all the shows we found
        #
        while ( $h = $sth->fetchrow_hashref ) {
            my $eps_id = $h->{ep_num};
            my $episode_key = sprintf("twat%04d",$eps_id);

            #
            # If one insert into the new database fails they all do
            #
            $dbh2->begin_work();

            #
            # Add links for the single hosts to the new database
            #
            foreach my $host (@newhosts) {
                $rv = $dbh2->do(
                    q{INSERT INTO episodes_hosts_xref
                        SELECT e.episode_id, h.host_id
                        FROM episodes e, hosts h
                        WHERE e.episode_key = ? AND h.host = ?},
                        undef, $episode_key, $host);
                if ( $dbh2->err ) {
                    warn $dbh2->errstr;
                }
                $rv = 0 if ( $rv eq '0E0' );

                unless ( defined($rv) ) {
                    warn "Problem adding link to episodes_hosts_xref for "
                        . "$episode_key,$host\n";
                }
                else {
                    printf "Linked show %s for host %s\n", $episode_key, $host;
                }

            }

            #
            # Commit the inserts above
            #
            $dbh2->commit();

        }
    }

}

#===  FUNCTION  ================================================================
#         NAME: link_double_hpr_episodes
#      PURPOSE: Find and link HPR episodes with double hosts
#   PARAMETERS: $dbh1           Handle for the MariaDB database
#               $dbh2           Handle for the Pg database
#               $doubles        hashref holding double host details
#               $verbose        Verbosity level
#      RETURNS: Nothing
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub link_double_hpr_episodes {
    my ( $dbh1, $dbh2, $doubles, $verbose ) = @_;

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

    #
    # Query to find HPR shows with particular host ids in the old database
    #
    $sth = $dbh1->prepare(
        q{
            SELECT id AS eps_id
            FROM eps
            WHERE hostid = ?
        }
    ) or die $DBI::errstr;
    if ( $dbh1->err ) {
        warn $dbh1->errstr;
    }

    #
    # Loop through the double hosts we collected
    #
    foreach my $double ( sort( keys(%{$doubles}) ) ) {
        #
        # Extract saved double and corresponding single details
        #
        my ( $doubleid, @newhosts ) = (
            $doubles->{$double}->{double},
            keys( %{ $doubles->{$double}->{singles} } )
        );

        #
        # Find HPR shows marked as belonging to this double-host (in the old
        # database)
        #
        $sth->execute($doubleid);
        if ( $dbh1->err ) {
            die $dbh1->errstr;
        }

        #
        # Process all the shows we found
        #
        while ( $h = $sth->fetchrow_hashref ) {
            my $eps_id = $h->{eps_id};
            my $episode_key = sprintf("hpr%04d",$eps_id);

            #
            # If one insert into the new database fails they all do
            #
            $dbh2->begin_work();

            #
            # Add links for the single hosts to the new database
            #
            foreach my $host (@newhosts) {
                $rv = $dbh2->do(
                    q{INSERT INTO episodes_hosts_xref
                        SELECT e.episode_id, h.host_id
                        FROM episodes e, hosts h
                        WHERE e.episode_key = ? AND h.host = ?},
                        undef, $episode_key, $host);
                if ( $dbh2->err ) {
                    warn $dbh2->errstr;
                }
                $rv = 0 if ( $rv eq '0E0' );

                unless ( defined($rv) ) {
                    warn "Problem adding link to episodes_hosts_xref for "
                        . "$episode_key,$host\n";
                }
                else {
                    printf "Linked show %s for host %s\n", $episode_key, $host;
                }

            }

            #
            # Commit the inserts above
            #
            $dbh2->commit();

        }
    }

}

#===  FUNCTION  ================================================================
#         NAME: save_hostid
#      PURPOSE: Saves the host id after searching for the key in the %doubles
#               hash
#   PARAMETERS: $doubles                hashref to %doubles
#               $host                   host key
#               $hostid                 host id number
#      RETURNS: Nothing
#  DESCRIPTION: Searches the %doubles hash for particular keys in the
#               'singles' sub-hash. If found saves the corresponding host id
#               there.
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub save_hostid {
    my ( $doubles, $host, $hostid ) = @_;

    foreach my $key ( keys(%$doubles) ) {
        if ( exists( $doubles->{$key}->{singles}->{$host} ) ) {
            $doubles->{$key}->{singles}->{$host} = $hostid;
        }
    }
}

#===  FUNCTION  ================================================================
#         NAME: compute_host_date_added
#      PURPOSE: Determine the 'hosts.when_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 when_added values where possible
    #
    $rv = $dbh->do(
        q{
        UPDATE hosts
            SET when_added = sq.when_added
        FROM (
            SELECT h.host_id,min(e.release_date) AS when_added
            FROM episodes e
            JOIN episodes_hosts_xref eh ON (e.episode_id = eh.episode_id)
            JOIN hosts h ON (h.host_id = eh.host_id)
            GROUP BY h.host_id
            ORDER by min(e.release_date)) AS sq
        WHERE hosts.host_id = sq.host_id
    }
    );
    if ( $dbh->err ) {
        warn $dbh->errstr;
    }
    $rv = 0 if ( $rv eq '0E0' );

    print "Added $rv dates to the 'when_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
#               $idname         Id field name (primary key)
#               $sequence       Sequence name
#      RETURNS: Nothing
#  DESCRIPTION: 
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub alter_seq {
    my ( $dbh, $table, $idname, $sequence ) = @_;

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

    #
    # Find the maximum id number in the table
    #
    $sth = $dbh->prepare("SELECT max($idname) 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: coalesce
#      PURPOSE: To find the first defined argument and return it
#   PARAMETERS: Arbitrary number of arguments
#      RETURNS: The first defined argument or undef if there are none
#  DESCRIPTION: Modelled on the SQL function of the same name. It takes a list
#               of arguments, scans it for the first one that is not undefined
#               and returns it. If an argument is defined and it's an arrayref
#               then the referenced array is returned comma-delimited. This
#               allows calls such as "coalesce($var,'undef')" which returns
#               the value of $var if it's defined, and 'undef' if not and
#               doesn't break anything along the way.
#       THROWS: No exceptions
#     COMMENTS: None
#     SEE ALSO: N/A
#===============================================================================
sub coalesce {
    foreach (@_) {
        if ( defined($_) ) {
            if ( ref($_) eq 'ARRAY' ) {
                return join( ',', @{$_} );
            }
            else {
                return $_;
            }
        }
    }
    return; # implicit undef
}

#===  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", "config=s", "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.1.5


=head1 USAGE

 copy_mysql_pg2 -verbose
 copy_mysql_pg2 -config=.hpr_livedb.cfg -verbose
 copy_mysql_pg2 -verbose \
    -phase='episodes,hosts,eh_xref,series,es_xref,tags,comments,archived,assets,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 its own 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<archived>

Causes the archive-related fields of the B<episodes> table to be filled from
the SQLite database B<ia.db>.

=item B<assets>

Causes the B<assets> table to be filled from the SQLite database B<ia.db>.

=item B<epilogue>

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

=back

=back

=item B<-config=FILE>

This option allows an alternative configuration file to be used. This file
defines the location of the database, its port, its name and the username and
password to be used to access it. This feature was added to allow the script
to access alternative databases or the live database over an SSH tunnel.

See the CONFIGURATION AND ENVIRONMENT section below for the file format.

If the option is omitted the default file is used: B<.hpr_db.cfg>

=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

The script obtains the credentials it requires to open the HPR database from
a configuration file. The name of the file it expects is B<.hpr_db.cfg> in the
directory holding the script. To change this will require changing the script.

The configuration file format is as follows:

 <database>
     host = 127.0.0.1
     port = PORT
     name = DATABASE
     user = USERNAME
     password = PASSWORD
 </database>

=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 BUGS AND LIMITATIONS

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

=head1 AUTHOR

Dave Morriss  (Dave.Morriss@gmail.com)

=head1 LICENCE AND COPYRIGHT

Copyright (c) 2017-2019 Dave Morriss (Dave.Morriss@gmail.com). All rights reserved.

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
