#!/usr/bin/env raku
use Red::Cli;
use Red::Do;
use Red::Database;
use Red::Migration::Configuration;
use Red::Migration::Manager;

my %*SUB-MAIN-OPTS =
  :named-anywhere,
;

my $*RED-DEBUG = False;

proto MAIN(Str :$I, Bool :$debug, |) {
    dyn-lib .split: "," with $I;
    $*RED-DEBUG = $debug;
    {*}
}

#| Open the native database client for a configured connection.
multi MAIN('connect', Str :$config = 'red.rakuconf', Str :$connection = 'default') {
    use Red::Migration::Environment;
    my $settings = Red::Migration::Configuration.load($config.IO);
    my $db = %*ENV<RED_MIGRATION_TEST>
        ?? test-database() !! $settings.connections{$connection};
    $db = $db() if $db ~~ Callable;
    X::Red::Migration.new(:reason("Unknown configured connection: $connection")).throw
        unless $db ~~ Red::Driver:D;
    my %client = database-client($db);
    my %environment = %*ENV;
    %environment{.key} = .value for %client<environment>.pairs;
    my $process = run |%client<command>, :env(%environment);
    if $process.exitcode == -1 {
        X::Red::Migration.new(:reason(
            "Unable to start {%client<command>[0]}; install the database client and check PATH")).throw;
    }
    exit $process.exitcode;
}

#| List tables in database
multi MAIN(
    "list-tables",
    Str  :$driver!,
    *%pars
) {
    my $*RED-DB = database($driver, |%pars);
    .say for list-tables :$driver, |%pars
}

#| Generate stub code to access models from database schema
multi MAIN(
    "print-stub",
    Str  :$schema-class,
    Str  :$driver!,
    *%pars
) {
    my $*RED-DB = database($driver, |%pars);
    say gen-stub-code :$schema-class, :$driver, |%pars
}


#| Generates migration plan to upgrade database schema
multi MAIN(
    "migration", "plan",
    Str :$model!,
    Str :$require = $model,
    Str :$driver!,
    *%pars
) {
    my $*RED-DB = database($driver, |%pars);
    migration-plan :$model, :$require, :$driver, |%pars
}

#| Generates models' code from database schema
multi MAIN(
    "generate-code",
    Str  :$path!    where { !.defined || .IO.d || $_ eq "-" },
    Str  :$from-sql where { !.defined || .IO.f || $_ eq "-" },
    Str  :$schema-class,
    Bool :$print-stub       = False,
    Bool :$no-relationships = False,
    #Bool :$stub-only,
    Str  :$driver!,
    *%pars
) {
    my $*RED-DB = database($driver, |%pars);
    generate-code
            :$path,
            :$from-sql,
            :$schema-class,
            :$print-stub,
            :$no-relationships,
            :$driver,
            |%pars
}

#| Prepare database
multi MAIN(
        "migration", "prepare-database",
        Bool :$populate,
        Str  :$models!,
        Str  :$driver!,
        *%pars
) {
    $GLOBAL::RED-DB = database $driver, |%pars;
    prepare-database :$populate, :$models, :$driver, |%pars
}

sub dyn-lib(@libs) {
	qq[use lib "{ $_ }"].EVAL for @libs
}

#| Show the development database plan, without applying it.
multi MAIN('migration', 'plan', Str :$config = 'red.rakuconf', Str :$connection = 'default') {
    .say for migration-manager($config).plan(:$connection)
}

#| Synchronize only the development database.
multi MAIN('migration', 'sync', Bool :$dev! where so *, Str :$config = 'red.rakuconf',
           Str :$connection = 'default', Bool :$allow-destructive = False) {
    say migration-manager($config).sync(:$connection, :$allow-destructive)
}

#| Publish prepared migration artifacts in distribution resources.
multi MAIN('migration', 'bundle', Str :$config = 'red.rakuconf', Str :$project = '.') {
    say migration-manager($config).bundle(:$project) ~ ' migrations bundled'
}

#| Import prepared SQL resources from installed runtime dependencies for local review.
multi MAIN('migration', 'import', Str :$config = 'red.rakuconf', Str :$project = '.') {
    say migration-manager($config).import(:$project) ~ ' migrations imported; review before applying'
}

#| Transfer all tables of an explicitly named dependency to local schema management.
multi MAIN('migration', 'adopt', Str :$dependency!, Str :$config = 'red.rakuconf',
           Str :$project = '.') {
    say migration-manager($config).adopt(:$dependency, :$project) ~ ' tables adopted'
}

#| Prepare versioned SQL and schema snapshots for all configured drivers.
multi MAIN('migration', 'prepare', Str $name, Str :$config = 'red.rakuconf',
           Bool :$allow-destructive = False) {
    my $path = migration-manager($config).prepare($name, :$allow-destructive);
    say $path // 'No schema changes'
}

#| Apply pending migrations to the selected connection, optionally stopping at a migration ID.
multi MAIN('migration', 'apply', Str :$config = 'red.rakuconf',
           Str :$connection = 'default', Str :$to) {
    say migration-manager($config).apply(:$connection, :$to)
}

sub confirm-table-removal(Str $command, Str $connection, Bool $force, @tables --> Bool) {
    say "Connection: $connection";
    say 'Tables to remove: ' ~ (@tables ?? @tables.join(', ') !! '(none)');
    return True if $force;
    my $answer = prompt "$command removes all data in these tables. Continue? [y/N] ";
    so ($answer.defined && $answer.trim.lc eq any(<y yes>))
}

#| Remove managed tables and all migration bookkeeping, preserving migration files.
multi MAIN('migration', 'drop-tables', Str :$config = 'red.rakuconf',
           Str :$connection = 'default', Bool :$force = False) {
    my $removed = migration-manager($config).drop-tables(:$connection,
        :confirm(-> @tables { confirm-table-removal('drop-tables', $connection, $force, @tables) }));
    say "$removed tables removed";
}

#| Remove managed tables and replay all prepared migrations, without applying fixtures.
multi MAIN('migration', 'reset', Str :$config = 'red.rakuconf',
           Str :$connection = 'default', Bool :$force = False) {
    my $applied = migration-manager($config).reset(:$connection,
        :confirm(-> @tables { confirm-table-removal('reset', $connection, $force, @tables) }));
    say "$applied migrations applied";
}

#| Reconcile a manually completed or reverted partial MySQL migration.
multi MAIN('migration', 'resolve', Str $id, Str :$as!, Str :$reason!,
           Str :$config = 'red.rakuconf', Str :$connection = 'default') {
    my %result = migration-manager($config).resolve($id, :$as, :$reason, :$connection);
    say "{%result<id>} resolved as {%result<state>}; verification: {%result<verification>}; data checked manually"
}

#| Revert the last applied migration using its down scripts.
multi MAIN('migration', 'rollback', Str :$config = 'red.rakuconf', Str :$connection = 'default') {
    say migration-manager($config).rollback(:$connection)
}

#| Show initialization, configured models and recorded migration states.
multi MAIN('migration', 'status', Str :$config = 'red.rakuconf', Str :$connection = 'default') {
    unless $config.IO.e {
        say 'Database migrations: Not initialized';
        say "Configuration: not found ({$config.IO.absolute})";
        say "Connection: $connection (not configured)";
        say 'Configured models: none (configuration missing)';
        return;
    }
    my %report = migration-manager($config).status-report(:$connection);
    say 'Database migrations: ' ~ (%report<initialized> ?? 'Initialized' !! 'Not initialized');
    say "Configuration: {$config.IO.absolute}";
    say "Connection: {%report<connection>} ({%report<driver>})";
    say "Migration directory: {%report<directory>}";
    say "Configured models: {%report<models>.elems}";
    for %report<models>.list -> %model {
        say "  {%model<name>} -> {%model<table>} (table "
            ~ (%model<table-exists> ?? 'present' !! 'missing') ~ ')';
        for %model<columns>.list -> %column {
            my @flags = %column<nullable> ?? 'nullable' !! 'required';
            @flags.push('primary key') if %column<primary-key>;
            @flags.push('automatic') if %column<auto-increment>;
            say "    {%column<name>}: {%column<raku-type>} [{@flags.join(', ')}]";
        }
    }
    say "Prepared migrations: {%report<migrations>.elems}";
    say "Recorded migrations: {%report<recorded>}";
    for %report<migrations>.list -> %migration {
        say "%migration<id> %migration<state> %migration<name>"
    }
    for (%report<drafts> // []).list -> %draft {
        say "%draft<id> draft %draft<name> (run red migration prepare)";
    }
}

sub migration-manager(Str $file --> Red::Migration::Manager) {
    Red::Migration::Manager.new(
        :config(Red::Migration::Configuration.load($file.IO)))
}

#| Validate migrations using fresh disposable databases, optionally running a command.
multi MAIN('migration', 'apply', Bool :$test! where so *, Str :$driver, Bool :$keep = False,
           Str :$config = 'red.rakuconf', *@command) {
    migration-manager($config).test(:$driver, :$keep, :@command)
}

#| Add the standard migration test without replacing project tests.
multi MAIN('migration', 'init-tests', Str :$config = 'red.rakuconf') {
    my $path = 't/migrations.rakutest'.IO;
    X::Red::Migration.new(:reason('t/migrations.rakutest already exists')).throw if $path.e;
    $path.parent.mkdir unless $path.parent.d;
    $path.spurt("use Test;\nuse Red::Migration::Test;\nmigration-ok " ~ $config.raku ~ ";\ndone-testing;\n");
    say $path
}

#| Generate missing SQL dialects across the entire migration history.
multi MAIN('migration', 'backfill', Str :$driver!, Str :$config = 'red.rakuconf') {
    say migration-manager($config).backfill(:$driver)
}

#| Rebase unpublished branches after an explicitly protected migration.
multi MAIN('migration', 'serialize', Str :$after!, Str :$order,
           :@check-connection = [], Str :$config = 'red.rakuconf') {
    say migration-manager($config).serialize(:$after,
        :order($order.defined ?? $order.split(',').Array !! []),
        :check-connections(@check-connection))
}
#| Create a synchronous migration draft with historical models and migration.raku.
multi MAIN('migration', 'create', Str $name, Str :$config = 'red.rakuconf') {
    say migration-manager($config).create($name)
}

#| List named fixtures without opening the database.
multi MAIN('fixture', 'list', Str :$config = 'red.rakuconf') {
    use Red::Fixture;
    fixture-command('list', :$config);
}

#| Save configured models as a named fixture.
multi MAIN('fixture', 'save', Str $name, Str :$config = 'red.rakuconf', Bool :$force = False) {
    use Red::Fixture;
    fixture-command('save', $name, :$config, :$force);
}

#| Apply a named fixture in a transaction.
multi MAIN('fixture', 'apply', Str $name, Str :$config = 'red.rakuconf') {
    use Red::Fixture;
    fixture-command('apply', $name, :$config);
}
