#!/usr/bin/env perl

use strict;
use warnings;

use Getopt::Long qw(:config no_ignore_case);
use Pod::Usage;
use DBIx::Class::MockData;

=head1 NAME

dbic-mockdata - Generate mock test data for a DBIx::Class schema

=head1 VERSION

Version 0.04

=head1 SYNOPSIS

  dbic-mockdata --schema-dir /path/to/lib \
                --namespace MyApp::Schema \
                --dsn "dbi:SQLite:dbname=test.db" [options]

=head1 DESCRIPTION

Connects the given DBIx::Class schema and hands it to
L<DBIx::Class::MockData> to generate and insert mock rows.

=head1 OPTIONS

  --schema-dir      Directory containing the schema classes           (required)
  --namespace       Top-level schema package name, e.g. MyApp::Schema (required)
  --dsn             DBI DSN string, e.g. "dbi:Pg:dbname=mydb"         (required)
  --user            Database username
  --password        Database password
  --rows            Rows to insert per table (default: 5)
  --rows-per-table  JSON string for per-table row counts, e.g. '{"Author":10,"Book":3}'
  --generators      JSON string for custom generators, e.g. '{"email":"user{row}@example.com","status":"active"}'
  --seed            Random seed for reproducible output
  --deploy          Create tables that do not yet exist
  --wipe            Drop and recreate all tables before inserting (destructive)
  --dry-run         Print generated values without inserting
  --only            Comma-separated list of tables to populate (e.g. Author,Book)
  --exclude         Comma-separated list of tables to skip
  --verbose         Print debug output
  --help            Show this help

=head1 ADVANCED OPTIONS

=head2 Rows Per Table

  --rows-per-table '{"Author":10,"Book":3}'

Allows different row counts for specific tables. Tables not specified use the
global --rows value.

=head2 Custom Generators

  --generators '{"email":"user{row}@example.com","status":"active"}'

Provides custom value generation for specific columns. The syntax supports:

  * Static values: "active", "pending"
  * Row number interpolation: "user{row}@example.com" becomes user1@example.com
  * Perl code evaluation: "sub { return 'prefix_' . int(rand(1000)) }"

For complex generators, use Perl code syntax:

  --generators '{"email":"sub { my ($col,$info,$n,$mock)=@_; return \"user$n\@example.com\"; }"}'

=cut

my %opt = (rows => 5);

GetOptions(
    \%opt,
    'schema-dir=s',
    'namespace=s',
    'dsn=s',
    'user=s',
    'password=s',
    'rows=i',
    'rows-per-table=s',
    'generators=s',
    'seed=i',
    'deploy',
    'wipe',
    'dry-run',
    'only=s',
    'exclude=s',
    'verbose',
    'help|h',
) or pod2usage(2);

pod2usage(0) if $opt{help};
die "ERROR: --schema-dir is required\n"  unless $opt{'schema-dir'};
die "ERROR: --namespace is required\n"   unless $opt{namespace};
die "ERROR: --dsn is required\n"         unless $opt{dsn} || $opt{'dry-run'};
die "ERROR: --only and --exclude cannot both be specified\n"
    if $opt{only} && $opt{exclude};

# Parse JSON options if provided
my $rows_per_table = {};
my $generators     = {};

if ($opt{'rows-per-table'}) {
    require JSON;
    $rows_per_table = eval { JSON::decode_json($opt{'rows-per-table'}) };
    die "ERROR: Invalid JSON for --rows-per-table: $@\n" if $@;
    die "ERROR: --rows-per-table must be a hash reference\n"
        unless ref($rows_per_table) eq 'HASH';
}

if ($opt{generators}) {
    require JSON;
    my $gen_spec = eval { JSON::decode_json($opt{generators}) };
    die "ERROR: Invalid JSON for --generators: $@\n" if $@;
    die "ERROR: --generators must be a hash reference\n"
        unless ref($gen_spec) eq 'HASH';

    # Convert string specifications to executable code where needed
    while (my ($col, $spec) = each %$gen_spec) {
        if ($spec =~ /^sub\s*\{/) {
            # Perl code - evaluate to get code reference
            my $code = eval $spec;
            die "ERROR: Invalid Perl code for column '$col': $@\n" if $@;
            $generators->{$col} = $code;
        }
        elsif ($spec =~ /{row}/) {
            # Simple template with {row} placeholder
            my $template = $spec;
            $generators->{$col} = sub {
                my ($col, $info, $n, $mock) = @_;
                my $value = $template;
                $value =~ s/{row}/$n/g;
                return $value;
            };
        }
        else {
            # Static value
            my $static = $spec;
            $generators->{$col} = sub { $static };
        }
    }
}

# Connect the schema — this is DBIx::Class's job, not ours

my $schema_dir = $opt{'schema-dir'};
my $namespace  = $opt{namespace};

{
    use File::Spec;
    my $dir = File::Spec->rel2abs($schema_dir);
    # Step up one level if dir IS the top namespace component
    my $top  = (split /::/, $namespace)[0];
    my $base = (File::Spec->splitdir($dir))[-1];
    if ($base eq $top) {
        $dir = File::Spec->catdir((File::Spec->splitdir($dir))[0 .. -2]);
    }
    push @INC, $dir unless grep { $_ eq $dir } @INC;
}

(my $mod = $namespace) =~ s{::}{/}g;
eval { require "$mod.pm" };
die "Could not load '$namespace': $@\n" if $@;

{
    no strict 'refs';
    if ($namespace->can('load_namespaces') && !$namespace->sources) {
        eval { $namespace->load_namespaces };
    }
}

my @connect_info = ($opt{dsn} // 'dbi:SQLite::memory:');
push @connect_info, $opt{user}     if defined $opt{user};
push @connect_info, $opt{password} if defined $opt{password};

my $schema = eval { $namespace->connect(@connect_info) };
die "Could not connect schema: $@\n" if $@;

# Hand the connected schema to MockData

my $mock = DBIx::Class::MockData->new(
    schema         => $schema,
    schema_dir     => $schema_dir,
    rows           => $opt{rows},
    rows_per_table => $rows_per_table,
    generators     => $generators,
    verbose        => $opt{verbose} // 0,
    (defined $opt{seed}    ? (seed    => $opt{seed})                    : ()),
    (defined $opt{only}    ? (only    => [split /,\s*/, $opt{only}   ]) : ()),
    (defined $opt{exclude} ? (exclude => [split /,\s*/, $opt{exclude}]) : ()),
);

eval {
    if    ($opt{'dry-run'}) { $mock->dry_run          }
    elsif ($opt{wipe})      { $mock->wipe->generate   }
    elsif ($opt{deploy})    { $mock->deploy->generate }
    else                    { $mock->generate         }
};
die $@ if $@;
