#!/usr/bin/env raku

use v6.d;
BEGIN %*ENV<ORM_LOG_FILE> //= 'log/error.log' if 'log'.IO.d;
use ORM::ActiveRecord::Schema::Migrate;
use ORM::ActiveRecord::Schema::WorkerDbs;
use ORM::ActiveRecord::Schema::Generator;
use ORM::ActiveRecord::Schema::DbTasks;
use ORM::ActiveRecord::Support::Runtime;
use ORM::ActiveRecord::Support::Version;

# Subcommands:
#   createdb         create the configured database(s) — all for a multi-db
#                    config, or just the one. Does not migrate.
#   migrate          run migrations against the configured database(s).
#   check            report whether every expected database exists and is
#                    migrated; exit non-zero if not (run no migrations).
#   up[:N] down[:N]  migrate up / down (optionally N steps).
#   reset [--yes]    drop every table (add --quiet to silence the listing).
# Add --parallel to createdb/migrate/check/reset to target the test
# environment's parallel databases. The worker count comes from config (the
# env's `parallel` key) unless `--parallel=N` gives it explicitly. A parallel
# reset is non-interactive, so it requires --yes.
sub MAIN(*@args, Bool :$parallel = False, Bool :$version = False) {
  if $version {
    say "ar (ORM::ActiveRecord) {ORM::ActiveRecord::Support::Version::version()}";
    return;
  }

  # Raku binds `--parallel` to the named arg only when it precedes the
  # subcommand; accept it (and the `--parallel=N` count form) in either
  # position by parsing @args directly.
  my $par = $parallel;
  my Int $count;
  my @cmd;
  for @args -> $a {
    if    $a eq '--parallel'              { $par = True }
    elsif $a ~~ /^ '--parallel=' (\d+) $/ { $par = True; $count = +$0 }
    else                                  { @cmd.push: $a }
  }

  given @cmd[0] // 'migrate' {
    when 'generate' | 'g' { exit run-generate(@cmd[1 .. *]) }
    when 'destroy'  | 'd' { exit run-destroy(@cmd[1 .. *]) }
    when 'console'   { run-console }
    when 'runner'    { exit run-runner(@cmd[1 .. *]) }
    when 'dbconsole' { run-dbconsole }
    when 'notes'     { run-notes }
    when 'stats'     { run-stats }
    when /^ 'db:' / { exit run-db-task(@cmd) }
    when 'createdb' { create-test-databases(parallel => $par, :$count) }
    when 'migrate'  { migrate-test-databases(parallel => $par, :$count) }
    when 'check'    { exit check-command(parallel => $par, :$count) }
    when 'reset'    {
      if $par {
        my $yes = @cmd.first({ $_ eq '--yes' || $_ eq '-y' }).defined
                  || (%*ENV<AR_ASSUME_YES> // '') eq '1';
        unless $yes {
          $*ERR.say: 'reset --parallel drops every worker test database; pass --yes to confirm.';
          exit 2;
        }
        reset-test-databases(parallel => True, :$count);
      } else {
        Migrate.new(args => @cmd).run;
      }
    }
    default         { Migrate.new(args => @cmd).run }
  }
}

# Pre-flight: print one line per problem and exit 1 if any database is missing
# or un-migrated, else confirm readiness and exit 0.
sub check-command(Bool :$parallel, Int :$count --> Int) {
  my @problems = check-test-databases(:$parallel, :$count);
  return 0 unless @problems;

  $*ERR.say: "Databases not ready:";
  $*ERR.say: "  - $_" for @problems;
  $*ERR.say: "Run `ar createdb"
    ~ ($parallel ?? ' --parallel' !! '') ~ "` and `ar migrate"
    ~ ($parallel ?? ' --parallel' !! '') ~ "` first.";
  1;
}

sub announce(Str:D $verb, @paths) {
  say "      $verb  $_" for @paths;
}

sub run-generate(@args --> Int) {
  my $kind = @args[0];
  my @rest = @args[1 .. *].grep(*.defined);
  my $gen  = Generator.new;

  given $kind {
    when 'model' {
      return missing('generate model NAME [field:type ...]') unless @rest.elems;
      announce('create', $gen.generate-model(@rest[0], @rest[1 .. *].grep(*.defined)));
    }
    when 'migration' {
      return missing('generate migration NAME [field:type ...]') unless @rest.elems;
      announce('create', $gen.generate-migration(@rest[0], @rest[1 .. *].grep(*.defined)));
    }
    when 'scope' {
      return missing('generate scope MODEL NAME [field:value ...]') unless @rest.elems >= 2;
      announce('create', $gen.generate-scope(@rest[0], @rest[1], @rest[2 .. *].grep(*.defined)));
    }
    when 'validator' {
      return missing('generate validator NAME') unless @rest.elems;
      announce('create', $gen.generate-validator(@rest[0]));
    }
    default {
      $*ERR.say: "ar generate: unknown type '{$kind // ''}' (model | migration | scope | validator)";
      return 2;
    }
  }

  0;
}

sub run-destroy(@args --> Int) {
  my $kind = @args[0];
  my @rest = @args[1 .. *].grep(*.defined);
  my $gen  = Generator.new;

  given $kind {
    when 'model' {
      return missing('destroy model NAME') unless @rest.elems;
      announce('remove', $gen.destroy-model(@rest[0]));
    }
    when 'migration' {
      return missing('destroy migration NAME') unless @rest.elems;
      announce('remove', $gen.destroy-migration(@rest[0]));
    }
    when 'scope' {
      return missing('destroy scope MODEL NAME') unless @rest.elems >= 2;
      announce('remove', $gen.destroy-scope(@rest[0], @rest[1]));
    }
    when 'validator' {
      return missing('destroy validator NAME') unless @rest.elems;
      announce('remove', $gen.destroy-validator(@rest[0]));
    }
    default {
      $*ERR.say: "ar destroy: unknown type '{$kind // ''}' (model | migration | scope | validator)";
      return 2;
    }
  }

  0;
}

sub missing(Str:D $usage --> Int) {
  $*ERR.say: "ar: missing arguments - usage: ar $usage";
  2;
}

sub run-console {
  exit run(|Runtime.new.console-command).exitcode;
}

sub run-runner(@args --> Int) {
  my $target = @args[0];
  return missing('runner FILE | "CODE"') unless $target.defined;

  my $runtime = Runtime.new;
  $target.IO.e ?? $runtime.run-script($target) !! $runtime.run-code($target);
  0;
}

sub run-dbconsole {
  my %command = Runtime.new.dbconsole-command;
  %*ENV{.key} = .value for %command<env>.pairs;
  exit run(|%command<argv>).exitcode;
}

sub run-notes {
  my @roots = <lib bin app db t>.grep(*.IO.e);
  my @notes = Runtime.new.scan-notes(@roots);

  unless @notes.elems {
    say 'No annotations found.';
    return;
  }

  for @notes -> %note {
    say sprintf('%-8s %s:%d  %s', %note<tag>, %note<file>, %note<line>, %note<text>);
  }
}

sub run-stats {
  my %stats = Runtime.new.compute-stats;
  say "Files:      %stats<files>";
  say "Lines:      %stats<lines>";
  say "Code lines: %stats<code>";
  say "Models:     %stats<models>";
  say "Migrations: %stats<migrations>";
}

sub run-db-task(@cmd --> Int) {
  my $task = @cmd[0].substr(3);    # strip the leading 'db:'

  my %opts;
  for @cmd[1 .. *] -> $arg {
    %opts{$0.Str.uc} = $1.Str if $arg ~~ /^ (<-[=]>+) '=' (.*) $/;
  }

  my $tasks = DbTasks.new;

  given $task {
    when 'create'  { $tasks.create }
    when 'drop'    { $tasks.drop }
    when 'reset'   { $tasks.reset }
    when 'setup'   { $tasks.setup }
    when 'seed'    { $tasks.seed }
    when 'prepare' { $tasks.prepare }
    when 'version' { $tasks.version }
    when 'rollback' { $tasks.rollback(step => (%opts<STEP> // 1).Int) }
    when 'migrate' {
      %opts<VERSION>.defined ?? $tasks.migrate-to(%opts<VERSION>) !! $tasks.migrate;
    }
    when 'migrate:up' {
      return missing('db:migrate:up VERSION=NNN') unless %opts<VERSION>.defined;
      $tasks.migrate-up(%opts<VERSION>);
    }
    when 'migrate:down' {
      return missing('db:migrate:down VERSION=NNN') unless %opts<VERSION>.defined;
      $tasks.migrate-down(%opts<VERSION>);
    }
    when 'migrate:status' { $tasks.status }
    when 'migrate:redo'   { $tasks.redo(step => (%opts<STEP> // 1).Int) }
    when 'schema:dump'        { say '      create  ' ~ $tasks.schema-dump }
    when 'schema:load'        { $tasks.schema-load }
    when 'structure:dump'     { say '      create  ' ~ $tasks.structure-dump }
    when 'schema:cache:dump'  { say '      create  ' ~ $tasks.schema-cache-dump }
    when 'schema:cache:clear' { $tasks.schema-cache-clear }
    when 'abort_if_pending_migrations' | 'abort-if-pending-migrations' {
      return $tasks.abort-if-pending;
    }
    when 'test:prepare' | 'test:load_schema' | 'test:load-schema' { $tasks.test-prepare }
    default {
      $*ERR.say: "ar: unknown task 'db:$task'";
      return 2;
    }
  }

  0;
}

sub USAGE {
  print q:to/HELP/;
  ar — ORM::ActiveRecord migration & database tool

  Usage:
    ar                     migrate the configured database(s)
    ar migrate             migrate the configured database(s)
    ar createdb            create the configured database(s) (no migrate)
    ar check               report whether the database(s) exist and are migrated
    ar up[:N] | down[:N]   migrate up / down (optionally N steps)
    ar reset [--yes]       drop every table (use --yes to skip the prompt)

    ar generate model NAME [field:type ...]      model + create-table migration
    ar generate migration NAME [field:type ...]  a migration (Create/Add/Remove inferred)
    ar generate scope MODEL NAME [field:value]   insert a scope into a model
    ar generate validator NAME                   a custom validator class
    ar destroy TYPE NAME                          remove what `generate` created
    (field types: string, integer, datetime, references, ... ; modifiers :uniq :index)

    ar db:create | db:drop | db:reset | db:setup  create / drop / reset / set up the database
    ar db:seed | db:prepare                       run db/seeds.raku / create+migrate+seed-if-new
    ar db:migrate [VERSION=NNN]                    migrate up (or to a target version)
    ar db:migrate:up VERSION=NNN                   run one migration's up
    ar db:migrate:down VERSION=NNN                 run one migration's down
    ar db:migrate:status                           list each migration as up / down
    ar db:migrate:redo [STEP=N]                    roll back then re-apply N migrations
    ar db:rollback [STEP=N]                        roll back the last N migrations
    ar db:version                                  print the current schema version
    ar db:abort_if_pending_migrations             exit non-zero if migrations are pending
    ar db:test:prepare                             create + migrate the test database
    ar db:schema:dump | db:schema:load            dump / load db/schema.raku
    ar db:structure:dump                           dump raw DDL to db/structure.sql
    ar db:schema:cache:dump | :clear              dump / remove db/schema_cache.yml

    ar console                                     a Raku REPL with the project on the include path
    ar runner FILE | "CODE"                        run a script or inline code against the app
    ar dbconsole                                   open the database client with the app credentials
    ar notes                                       list TODO / FIXME / OPTIMIZE / HACK / XXX annotations
    ar stats                                       count files, lines, models, and migrations

    ar createdb --parallel       create the test env's parallel databases
    ar migrate  --parallel       migrate the test env's parallel databases
    ar check    --parallel       check the test env's parallel databases
    ar reset    --parallel --yes drop every table in the parallel databases
    (the worker count comes from config: the test env's `parallel` key)

    ar --version           print the installed version
    ar --help              show this help
  HELP
}
