#!/usr/bin/env raku

use v6.d;
BEGIN %*ENV<ORM_LOG_FILE> //= 'log/error.log' if 'log'.IO.d;
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;

sub show-version {
  say "active-record (ORM::ActiveRecord) {ORM::ActiveRecord::Support::Version::version()}";
}

# Every database operation lives under the `db:` namespace. With no arguments
# the tool runs `db:migrate` against the configured database(s). `--version`
# and `--help` are accepted as aliases of the `version` and `help` commands.
#
# Add --parallel to db:create / db:migrate / db:check / db:reset to target the
# test environment's parallel worker databases. The worker count comes from
# config (the env's `parallel` key) unless `--parallel=N` gives it explicitly.
# A parallel reset drops every worker table and is non-interactive, so it
# requires --yes.
sub MAIN(*@args, Bool :$parallel = False, Bool :$version = False) {
  if $version { show-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 }
  }

  @cmd.push: 'db:migrate' unless @cmd.elems;

  given @cmd[0] {
    when 'version' { show-version }
    when 'help'    { USAGE() }
    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, parallel => $par, :$count) }
    default {
      $*ERR.say: "active-record: unknown command '{@cmd[0]}' (see --help)";
      exit 2;
    }
  }
}

# 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 db:create"
    ~ ($parallel ?? ' --parallel' !! '') ~ "` and `ar db: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: "active-record: missing arguments - usage: active-record $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, Bool :$parallel = False, Int :$count --> 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'  {
      $parallel ?? create-test-databases(:parallel, :$count) !! $tasks.create;
    }
    when 'drop'    { $tasks.drop }
    when 'check'   { return check-command(:$parallel, :$count) }
    when 'reset'   {
      if $parallel {
        my $yes = @cmd.first({ $_ eq '--yes' || $_ eq '-y' }).defined
                  || (%*ENV<AR_ASSUME_YES> // '') eq '1';
        unless $yes {
          $*ERR.say: 'db:reset --parallel drops every worker test database; pass --yes to confirm.';
          return 2;
        }
        reset-test-databases(:parallel, :$count);
      } else {
        $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' {
      if $parallel {
        migrate-test-databases(:parallel, :$count);
      } else {
        %opts<VERSION>.defined ?? $tasks.migrate-to(%opts<VERSION>) !! $tasks.migrate;
        try $tasks.schema-dump-after-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: "active-record: unknown task 'db:$task'";
      return 2;
    }
  }

  0;
}

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

  Usage:
    active-record                     migrate the configured database(s)

    active-record generate model NAME [field:type ...]       model + create-table migration
    active-record generate migration NAME [field:type ...]   a migration (Create/Add/Remove inferred)
    active-record generate scope MODEL NAME [field:value]    insert a scope into a model
    active-record generate validator NAME                    a custom validator class
    active-record destroy TYPE NAME                          remove what `generate` created

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

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

    active-record db:create  --parallel        create the test env's parallel databases
    active-record db:migrate --parallel        migrate the test env's parallel databases
    active-record db:check   --parallel        check the test env's parallel databases
    active-record db:reset   --parallel --yes  drop every table in the parallel databases

    active-record version             print the installed version
    active-record help                show this help
  HELP
}
