#!/usr/bin/env perl

use strict;
use warnings;
use DBI;
use Getopt::Long;
use Pod::Usage;

my ($help, $verbose);
GetOptions(
    'help|h'    => \$help,
    'verbose|v' => \$verbose,
) or pod2usage(2);

pod2usage(1) if $help;
pod2usage("$0: Missing required argument: sqlite_db_path") unless @ARGV;

my $sqlite_db   = shift @ARGV;
my $output_file = shift @ARGV;

unless (-f $sqlite_db) {
    die "Error: SQLite database file '$sqlite_db' not found\n";
}

my $fh;
if ($output_file) {
    open($fh, '>', $output_file)
        or die "Error: Cannot open '$output_file' for writing: $!\n";
}
else {
    $fh = \*STDOUT;
}

# Connect to SQLite database
# sqlite_unicode is disabled to prevent corruption of binary data (source_ip)
my $dbh = DBI->connect(
    "dbi:SQLite:dbname=$sqlite_db",
    '', '',
    {   RaiseError     => 1,
        PrintError     => 0,
        sqlite_unicode => 0,
    }
) or die "Cannot connect to SQLite database: $DBI::errstr\n";

print $fh "-- MySQL import file exported from SQLite database\n";
print $fh "-- Generated by dmarc_sqlite_to_mysql\n";
print $fh "-- Source: $sqlite_db\n";
print $fh "-- \n";
print $fh "-- IMPORTANT: Create the MySQL schema FIRST using mail_dmarc_schema.mysql\n";
print $fh "-- before importing this data file. Foreign key lookup tables (fk_*) are\n";
print $fh "-- pre-populated by the schema and are NOT included in this export.\n";
print $fh "-- \n";
print $fh "SET FOREIGN_KEY_CHECKS=0;\n\n";

# Track domain id mappings for handling case-insensitive duplicates
my %domain_id_map = ();

# Define table export order (respecting foreign key dependencies)
my @tables = (
    'author',                  'domain',
    'report',                  'report_error',
    'report_policy_published', 'report_record',
    'report_record_reason',    'report_record_dkim',
    'report_record_spf',
);

# Special handling for domain table to normalize case and handle collisions
export_domain_table($dbh, $fh, \%domain_id_map, $verbose);

# Export remaining tables
foreach my $table (@tables) {
    next if $table eq 'domain';    # Already exported
    export_table($dbh, $fh, $table, $verbose, \%domain_id_map);
}

print $fh "SET FOREIGN_KEY_CHECKS=1;\n";

$dbh->disconnect();
close($fh) if $output_file;

print STDERR "Export complete" . ($output_file ? " to $output_file" : "") . "\n"
    if $verbose;

exit 0;

sub export_domain_table {
    my ($dbh, $fh, $domain_id_map, $verbose) = @_;

    print STDERR "Exporting table: domain (with case normalization)\n" if $verbose;

    # Get all domains from SQLite with their ids
    my $sth = $dbh->prepare("SELECT id, domain FROM domain ORDER BY id");
    $sth->execute();

    my %seen_lower = ();    # Track lowercase domains we've already exported
    my $new_id     = 1;

    print $fh "-- Table domain: normalized for case-insensitive collisions\n";

    while (my $row = $sth->fetchrow_hashref()) {
        my $orig_id      = $row->{id};
        my $domain       = $row->{domain};
        my $domain_lower = lc($domain);

        if (exists $seen_lower{$domain_lower}) {

            # Collision: map this id to the one we already exported
            $domain_id_map->{$orig_id} = $seen_lower{$domain_lower};
            print STDERR
                "  Domain collision: '$domain' (id $orig_id) -> normalized to id $seen_lower{$domain_lower}\n"
                if $verbose;
        }
        else {
            # First time seeing this domain (case-insensitive)
            $seen_lower{$domain_lower} = $new_id;
            $domain_id_map->{$orig_id} = $new_id;

            # Export normalized domain
            my $escaped_domain = $domain_lower;
            $escaped_domain =~ s/\\/\\\\/g;
            $escaped_domain =~ s/'/\\'/g;

            print $fh
                "INSERT INTO `domain` (`id`, `domain`) VALUES ($new_id, '$escaped_domain');\n";
            $new_id++;
        }
    }
    $sth->finish();
    print $fh "\n";
}

sub export_table {
    my ($dbh, $fh, $table, $verbose, $domain_id_map) = @_;

    print STDERR "Exporting table: $table\n" if $verbose;

    # Get column information
    my $sth = $dbh->prepare("PRAGMA table_info($table)");
    $sth->execute();
    my @columns;
    while (my $row = $sth->fetchrow_hashref()) {
        push @columns, $row->{name};
    }
    $sth->finish();

    unless (@columns) {
        print STDERR "Warning: Table '$table' has no columns or doesn't exist\n";
        return;
    }

    # Get row count
    my $count_sth = $dbh->prepare("SELECT COUNT(*) FROM $table");
    $count_sth->execute();
    my ($row_count) = $count_sth->fetchrow_array();
    $count_sth->finish();

    if ($row_count == 0) {
        print $fh "-- Table $table: 0 rows\n\n";
        return;
    }

    print $fh "-- Table $table: $row_count rows\n";

    # Get all rows
    my $query    = "SELECT " . join(', ', map {"`$_`"} @columns) . " FROM `$table`";
    my $data_sth = $dbh->prepare($query);
    $data_sth->execute();

    my $col_names = '`' . join('`, `', @columns) . '`';

    while (my @row = $data_sth->fetchrow_array()) {
        my @values;
        for my $i (0 .. $#row) {
            if (!defined $row[$i]) {
                push @values, 'NULL';
            }
            elsif ($columns[$i] eq 'source_ip') {

                # Handle binary IP data - convert to hex format
                if ($row[$i] eq '' || length($row[$i]) == 0) {
                    push @values, "0x";
                }
                else {
                    # Convert binary data to hexadecimal
                    my $hex = unpack('H*', $row[$i]);
                    push @values, "0x$hex";
                }
            }
            else {
                my $val = $row[$i];

                # Remap domain IDs if this column references the domain table
                if ((      $columns[$i] eq 'from_domain_id'
                        || $columns[$i] eq 'rcpt_domain_id'
                        || $columns[$i] eq 'domain_id'
                        || $columns[$i] eq 'envelope_to_did'
                        || $columns[$i] eq 'envelope_from_did'
                        || $columns[$i] eq 'header_from_did'
                    )
                    && defined $val
                    && exists $domain_id_map->{$val}
                    )
                {
                    $val = $domain_id_map->{$val};
                }

                # Escape single quotes and handle special characters
                $val =~ s/\\/\\\\/g;    # Backslash first
                $val =~ s/'/\\'/g;      # Single quotes
                push @values, "'$val'";
            }
        }
        print $fh "INSERT INTO `$table` ($col_names) VALUES ("
            . join(', ', @values) . ");\n";
    }
    $data_sth->finish();

    print $fh "\n";
}

__END__

=head1 NAME

dmarc_sqlite_to_mysql - Export SQLite database to MySQL SQL format

=head1 SYNOPSIS

dmarc_sqlite_to_mysql [options] <sqlite_db_path> [output_file]

=head1 OPTIONS

=over 4

=item B<-h, --help>

Print help message and exit.

=item B<-v, --verbose>

Print status messages to STDERR during export.

=back

=head1 ARGUMENTS

=over 4

=item B<sqlite_db_path>

Path to the SQLite database file to export.

=item B<output_file> (optional)

Output file for the SQL statements. If omitted, writes to STDOUT.

=back

=head1 DESCRIPTION

This script exports data from a SQLite database into MySQL-compatible SQL
INSERT statements. It respects foreign key dependencies by exporting tables
in the correct order.

B<Important:> The script automatically normalizes domain names to lowercase
and handles case-insensitive collisions. Since MySQL's default collation is
case-insensitive, domains like "Example.com" and "example.com" would cause
UNIQUE constraint violations. The script deduplicates these by:
  - Normalizing all domains to lowercase
  - Tracking id mappings for case-insensitive duplicates
  - Automatically remapping all foreign key references

Binary IP address data (source_ip varbinary field) are converted
to hexadecimal format for safe import into MySQL.

B<Note:> Foreign key lookup tables (fk_disposition, fk_disposition_reason,
fk_dkim_result, fk_spf_result, fk_spf_scope) are NOT exported as they are
pre-populated by the MySQL schema file. You must create the MySQL database
schema FIRST using mail_dmarc_schema.mysql before importing the data.

The output is suitable for importing directly into MySQL using:

    mysql -u user -p database < share/mail_dmarc_schema.mysql
    mysql -u user -p database < export.sql

=head1 EXAMPLE

Export to file:
    dmarc_sqlite_to_mysql mail_dmarc.db mail_dmarc_export.sql

Complete migration workflow:
    # 1. Create the MySQL database and schema first
    mysql -u root -p -e "CREATE DATABASE mail_dmarc"
    mysql -u root -p mail_dmarc < share/mail_dmarc_schema.mysql

    # 2. Export SQLite data
    dmarc_sqlite_to_mysql mail_dmarc.db mail_dmarc_export.sql

    # 3. Import the data
    mysql -u root -p mail_dmarc < mail_dmarc_export.sql

Export directly to MySQL:
    dmarc_sqlite_to_mysql mail_dmarc.db | mysql -u user -p database

=cut
