### ----------------------------------------------------
### -- fez
### -- Licenses: Artistic-2.0
### -- Authors: 
### -- File: lib/Fez/CLI.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 2
 @keys.combinations.grep(*.elems) -> @combo {
      $rx   = '^\'usage/\'.*?\'' ~ @combo.join('\'<-[_]>*_\'') ~ '\'<-[_]>*';
      @usage = $?DISTRIBUTION.meta<resources>.grep({$_ ~~ rx/ <$rx> /});
      @max-usage = @usage if +@usage > +@max-usage;
    }
  }
  if +@max-usage > 1 {
    my @options = @max-usage.map({$_.=substr(6); $_ = S:g/'_'/ / given $_; });
    say HELP-HEADER ~ "\n";
    say "Did you mean any of the following?\n  fez {@options.join("\n  fez ")}\n";
  } elsif +@max-usage == 1 {
    say HELP-HEADER ~ "\n";
    say template-repl(%?RESOURCES{@max-usage[0]}.slurp);
  } else {
    MAIN(:help);
  }
}

multi MAIN('ref', Bool:D :d(:$dry-run) = False, :q(:$quiet) = False) is export {
  MAIN('refresh', :$dry-run, :$quiet);
}
multi MAIN('refresh', Bool:D :d(:$dry-run) = False, :q(:$quiet) = False) is export {
  my %findings;
  my $cwd = upcurse-meta();
  log(FATAL, 'could not find META6.json') unless $cwd;

  my $repo-cfg  = $cwd.add('.fez').f
               ?? (try {
                 CATCH { default { log(FATAL, 'error reading .fez: %s', $_); } };
                 from-j($cwd.add('.fez').slurp)
               })
               !! {};

  log(DEBUG, "scanning files in {$cwd.add('lib').relative}");
  my $ignorer = $cwd.add('.gitignore').e
             ?? parse(|$cwd.IO.add('.gitignore').IO.slurp.lines, '.git', :git-ignore)
             !! parse('**/.precomp', '**.swp', '.git');

  my @files = ls($cwd.add('lib'), -> $f {
    !$ignorer.match($f.relative($cwd)) && (
      $f.basename.ends-with('.rakumod') || $f.d || $f.basename.ends-with('.pm6')
    );
  });
  log(DEBUG, "manifest:\n  %s", @files.join("\n  "));
  %findings<modfiles> = @files;

  log(DEBUG, 'looking for modules/classes in those files');
  my %rsult = :use({}), :provides({}), :depends({}), :lib-files({});
  scan-files(@files.sort, sub (Str:D $fn, Str:D $fc) {
    %rsult<lib-files>{$fn}++;
    for $fc.lines -> $ln {
      if my $m = $ln ~~ m:g/^ \s* 'use' \s+ $<use-stmt>=(<-[\s;:]>+ % '::')+ <-[\n]>*?';' / {
        my $match-str = "{$m[0]<use-stmt>.join.Str}";
        if $match-str !~~ any('Test'|'NativeCall'|'nqp') && !$match-str.starts-with('v6.') {
          %rsult<use>{$match-str}.push: $fn;
          log(DEBUG, '[%s]: uses: %s', $fn, $match-str);
        }
      }
      if $m = $ln ~~ m:g/^ \s* ('unit'\s+)? ('module'|'role'|'package'|'class') \s+ $<mod-stmt>=(<-[\s:;]>+ % '::')+ <-[\n]>*? (';'|'{') / {
        if $m.Str ~~ m:g/(^\s* 'unit')|(\s+'is export'(\s+|';'|'{'))/ {
          my $match-str = $m[0]<mod-stmt>.join.Str.trim;
          %rsult<provides>{$match-str}.push: $fn;
          %findings<provides>.push: $fn;
          log(DEBUG, '[%s]: provides: %s', $fn, $match-str);
        } else {
          log(DEBUG, '[%s]: %s missing unit or is export, skipping', $fn, $m.Str);
        }
      }
      if $m = $ln ~~ m:g/^\s*(class|role|module)\s+$<cls-stmt>=(<-[\s:;]>+ % '::')+ <-[\n]>*? (';'|'{') / {
        my $cls = $m[0]<cls-stmt>.join.Str.trim;
        next if $cls ~~ any(|($repo-cfg<ignore-provides>//[]));
        %rsult<provides>{$cls}.push: $fn;
        %findings<provides>.push: $fn;
        log(DEBUG, '[%s]: provides: %s', $fn, $cls);
      }
    }
  });

  %rsult<use>.keys.map({ %rsult<depends>{$_}.push(|%rsult<use>{$_}) unless %rsult<provides>{$_}:exists; });
  %findings<use> = %rsult<use>;

  for %rsult.keys.sort -> $k {
    log(DEBUG,
      "%s:\n%s",
      $k,
      %rsult{$k}
        .keys
        .sort
        .map({ "  $_ => [{%rsult{$k}{$_}.join(", ")}]" })
        .join("\n"));
  }

  my @rsrcs = $cwd.add('resources').d
    ?? ls($cwd.add('resources'), -> $f { True })
         .grep({ $ignorer.rmatch($_) })
         .map({ S/^ 'resources' [\/|\\] // given $_; })
    !! ();

  my %meta = from-j($cwd.add('META6.json').slurp);
  %findings<meta>  = %(|%meta.clone);
  %meta<depends>   = %rsult<depends>.keys.sort.Array;
  %meta<provides>  = %rsult<provides>.keys.sort.map({$_ => %rsult<provides>{$_}.first}).hash;
  %meta<resources> = @rsrcs.sort.Array;

  %findings<new-meta> = %meta;
  %findings<ignorer>  = $ignorer;
  %findings<meta-dir> = $cwd;

  log(DEBUG, to-j(%meta));

  printf "%s\n", to-j(%meta) if $dry-run && !$quiet;
  $cwd.add('META6.json').spurt(to-j(%meta)) unless $dry-run;

  %findings;
}

multi MAIN('li', Str:D :s(:$set) = '') is export {
  MAIN('license', :$set);
}
multi MAIN('license', Str:D :s(:$set) = '') is export {
  log(FATAL,
      "License %s was not found, licenses available:\n  %s",
      $set,
      $?DISTRIBUTION.meta<resources>
        .grep(*.starts-with('licenses/'))
        .map(*.substr(9, *-4))
        .sort({ $^a.lc cmp $^b.lc })
        .join("\n  ")
  ) if !$?DISTRIBUTION.meta<resources>.grep(*.lc eq "licenses/{$set.lc}.txt").elems
    && $set ne '';

  my $cwd = upcurse-meta();
  my $clicense = '';
  $clicense = $cwd.add('LICENSE').slurp if $cwd.add('LICENSE').f;
  $clicense = $cwd.add('LICENSE.txt').slurp if $cwd.add('LICENSE.txt').f;

  log(WARN, 'No license found in repo') if $clicense eq '' && $set eq '';

  if $set eq '' {
    my $match = '';
    for $?DISTRIBUTION.meta<resources>.grep(*.starts-with: "licenses/") -> $rsrc {
      if $clicense eq %?RESOURCES{$rsrc}.slurp {
        $match = $rsrc;
      }
    }

    if $match eq '' {
      log(MSG, 'No license found');
    } else {
      log(MSG, 'Current license: %s', $match.substr(9, *-4));
    }

  } else {
    my $lkey    = $?DISTRIBUTION.meta<resources>.grep(*.lc eq "licenses/{$set.lc}.txt").first;
    log(DEBUG, 'found license key: %s', $lkey);
    my $LICENSE = %?RESOURCES{$lkey}.slurp;
    $cwd.add('LICENSE.txt').spurt: $LICENSE;
    log(DEBUG, 'copied %s to %s', %?RESOURCES{$lkey}.absolute, $cwd.add('LICENSE.txt').relative);

    log(DEBUG, 'updating license in meta');
    my $meta = from-j($cwd.add('META6.json').slurp);
    $meta<license> = $lkey.substr(9, *-4);
    $cwd.add('META6.json').spurt: to-j($meta);
    log(MSG, 'Updated repo license to: %s', $lkey.substr(9, *-4));
  }
}

multi MAIN('in', Str $module is copy = '', Str:D :l(:$license) = config-value('default-init-license')//'') is export {
  MAIN('init', $module, :$license);
}
multi MAIN('init', Str $module is copy = '', Str:D :l(:$license) = config-value('default-init-license')//'') is export {
  log(FATAL,
      "License %s was not found, licenses available:\n  %s",
      $license,
      $?DISTRIBUTION.meta<resources>
        .grep(*.starts-with('licenses/'))
        .map(*.substr(9, *-4))
        .sort({ $^a.lc cmp $^b.lc })
        .join("\n  ")
  ) if !$?DISTRIBUTION.meta<resources>.grep(*.lc eq "licenses/{$license.lc}.txt").elems
    && $license ne '';

  $module          = prompt-wrapper('>>= Module name? ') while ($module//'').chars == 0;
  my @module-parts = $module.split('::', :skip-empty);
  my $module-file  = @module-parts.pop ~ ".rakumod";
  my $module-path  = 'lib'.IO.add(|@module-parts, $module-file);
  my $dist-name    = S:g/':'/\-/ given $module;
  my $license-name = $?DISTRIBUTION.meta<resources>.grep(*.lc eq "licenses/{$license.lc}.txt").first;
  my $LICENSE      = $license
                  ?? %?RESOURCES{$license-name}.slurp
                  !! '';

  log(DEBUG, "module-parts:%s\nmodule-file:%s\nmodule-path:%s\n  dist-name:%s", @module-parts.join(', '), $module-file, $module-path, $dist-name);

  log(FATAL, "'%s' exists, will not proceed\n", $dist-name) if '.'.IO.add($dist-name).e;

  mkdir $dist-name;

  my $root := '.'.IO.add($dist-name, 'lib').IO;
  mkdir $root.absolute;
  while @module-parts.elems {
    $root := $root.add(@module-parts.shift);
    mkdir $root.absolute;
  }

  my $auth = '';
  my $un = config-value('un') // '';
  if $un ne ''  {
    log(DEBUG, "found auth {current-prefix}%s", $un);
    $auth = "{current-prefix}{$un}";
  } else {
    log(INFO, "no auth found for the {current-prefix.substr(0, *-1)} ecosystem, creating with empty auth str");
  }

  log(DEBUG, 'creating meta file');
  my $meta = {
    "name" => "$dist-name",
    "version" =>  "0.0.1",
    "auth" => "$auth",

    "description" => "A brand new and very nice module",

    "depends" => [],
    "build-depends" => [],
    "test-depends" => [],

    "resources" => [],

    "provides" => {
      "$module" => "$module-path"
    }
  };

  $meta<license> = $license-name.substr(9, *-4) if $license ne '';

  '.'.IO.add($dist-name, 'META6.json').IO.spurt: to-j($meta);

  log(DEBUG, 'creating empty unit module file');
  '.'.IO.add($dist-name, $module-path).IO.spurt: "unit module $module;";

  log(DEBUG, 'making test directory');
  mkdir '.'.IO.add($dist-name, 't');

  '.'.IO.add($dist-name, 't', '00-use.rakutest').spurt: qq:to/EOF/;
  use Test;

  plan 1;

  use-ok "$module";
  EOF

  if $license ne '' {
    '.'.IO.add($dist-name, 'LICENSE.txt').spurt: $LICENSE;
  }
}

multi MAIN('mod', Str:D $mod, Bool:D :c(:$class) = False) is export {
  MAIN('module', $mod, :$class);
}

multi MAIN('module', Str:D $mod, Bool:D :c(:$class) = False) is export {
  my $cwd = upcurse-meta();
  log(FATAL, 'could not find META6.json') unless $cwd;
  log(DEBUG, "found META6.json in {$cwd.relative}");
  my %meta = from-j($cwd.add('META6.json').slurp);
  if %meta<provides>{$mod}:exists {
    log(FATAL, '%s already exists in provides', $mod);
  }
  my @module-parts = $mod.split('::', :skip-empty);
  my $module-file  = @module-parts.pop ~ ".rakumod";
  my $module-path  = 'lib'.IO.add(|@module-parts, $module-file);

  my $root = $cwd.add('lib');
  while @module-parts.elems {
    $root := $root.add(@module-parts.shift);
    mkdir $root.absolute;
  }

  $module-path.spurt("unit {$class??'class'!!'module'} $mod;\n");
  %meta<provides>{$mod} = $module-path.relative($cwd);

  $cwd.add('META6.json').spurt(to-j(%meta));
}

multi MAIN('dep', Str:D $dist, Bool :b(:$build) = False, Bool :t(:$test) = False, Bool :r(:$remove) = False) is export {
  MAIN('depends', $dist, :$build, :$test, :$remove);
}
multi MAIN('depends', Str:D $dist, Bool :b(:$build) = False, Bool :t(:$test) = False, Bool :r(:$remove) = False) is export {
  my $cwd = upcurse-meta();
  log(FATAL, '-b(--build) and -t(--test) flags cannot be used together, choose one') if $test && $build;
  log(FATAL, 'could not find META6.json') unless $cwd;
  log(DEBUG, "found META6.json in {$cwd.relative}");

  log(DEBUG, 'inspecting meta file');
  my $dep-key = "{$build??'build-'!!$test??'test-'!!''}depends";
  my %meta = from-j($cwd.add('META6.json').slurp);


  if $remove {
    my $ex = %meta{$dep-key}.grep(* eq $dist);
    my $rc = 0;
    log(DEBUG, "looking for %s in %s (exists? %s)\nexists in meta? %s", $dist, $dep-key, $ex??'yes'!!'no');
    if $ex {
      %meta{$dep-key} = %meta{$dep-key}.grep(* ne $dist).sort;
      $cwd.add('META6.json').spurt: to-j(%meta);
      log(DEBUG, 'new %s: %s', $dep-key, %meta{$dep-key}.join(', '));
      log(MSG, '%s removed', $dist);
    } else {
      log(WARN, '"%s" not found in: "%s": [%s]', $dist, $dep-key, %meta{$dep-key}.sort.join(', '));
      $rc = 1;
    }

    exit $rc;
  }




  if (%meta{$dep-key}||[]).grep($dist).elems == 0 {
    log(DEBUG, "did not find dependency (%s) in %s", $dist, $dep-key);
    %meta{$dep-key} = [] unless %meta{$dep-key};
    %meta{$dep-key}.push($dist);
    $cwd.add('META6.json').spurt: to-j(%meta);
    log(MSG, "%s was not found in %s so it was added", $dist, $dep-key);
  } else {
    log(MSG, "%s already exists in %s", $dist, $dep-key);
  }
}

multi MAIN('cmd') is export {
  MAIN('command');
}
multi MAIN('command') is export {
  my $cwd = upcurse-meta();
  log(FATAL, 'could not find META6.json') unless $cwd;
  log(DEBUG, "found META6.json in {$cwd.relative}");

  my $dist-cfg = $cwd.add('.fez');

  $dist-cfg.spurt(to-j({:commands({
    :test(["zef","test","."]),
  })})) unless $dist-cfg.f;

  my $cfg = from-j($dist-cfg.slurp);

  my $m = max $cfg<commands>.keys.map(*.chars);

  for $cfg<commands>.keys.sort -> $k {
    log(MSG, "%-{$m}s: %s", $k, $cfg<commands>{$k});
  }
}

multi MAIN('r', Str:D $command, :t(:$timeout) is copy = 300) is export {
  MAIN('run', $command, :t($timeout));
}
multi MAIN('run', Str:D $command, :t(:$timeout) is copy = 300) is export {
  my $cwd = upcurse-meta();
  log(FATAL, 'could not find META6.json') unless $cwd;

  my $dist-cfg = $cwd.add('.fez');

  $dist-cfg.spurt(to-j({:commands({
    :test(["zef", "test", "."]),
  })})) unless $dist-cfg.f;

  my $cfg = from-j($dist-cfg.slurp);

  log(FATAL, '"%s" command not found\navailable commands: %s', $command, $cfg<commands>.keys.sort.join(', '))
    unless $cfg<commands>{$command};

  my @cmd = |$cfg<commands>{$command};

  my $proc = Proc::Async.new: |@cmd;

  $timeout = $timeout.defined ?? $timeout !! $cfg<command_timeout> || 300;
  react {
    whenever $proc.stdout.lines { $_ ~~ s:g/'%'/%%/; log( INFO, $_); };
    whenever $proc.stderr.lines { $_ ~~ s:g/'%'/%%/; log(ERROR, $_); };
    whenever $proc.start {
      exit 0;
    };
    whenever signal(SIGTERM).merge: signal(SIGINT) {
      once {
        log(INFO, 'Attempting to kill process...');
        try $proc.kill: SIGKILL;
      }
    };
    whenever Promise.in($timeout) {
      try $proc.kill: SIGKILL;
      log(FATAL, 'Process timed out');
    }
  };
}

multi MAIN('res', Str:D $path, Bool:D :r($remove) = False) is export {
  MAIN('resource', $path, :$remove);
}
multi MAIN('resource', Str:D $path is copy, Bool:D :r($remove) = False) is export {
  $path.=trim;
  if !$remove && $path ~~ m{'#'|'<'|'>'|'$'|'+'|'%'|'!'|'`'|'&'|'*'|'\''|'|'|'{'|'}'|'?'|'"'|'='|':'|' '|'@'|"\r"|"\n"} || $path.encode.decode('ascii', :replacement<->) ne $path {
    log(FATAL, '%s contains a poor choice of characters, please remove any #<>$+%%>!`&*\'|{}?"=: @', $path);
  }


  my $cwd = upcurse-meta();
  log(FATAL, 'could not find META6.json') unless $cwd;

  my $resource-dir =  $cwd.add('resources');

  if $remove {
    my $me = from-j($cwd.add('META6.json').slurp);
    my $fd = $resource-dir.add($path);
    my $ex = $me<resources>.grep({"resources/$_".IO.relative($cwd) eq $fd.relative($cwd)});
    my $rc = 0;
    log(DEBUG, "looking for %s (exists? %s)\nexists in meta? %s", $fd.relative($cwd), $fd.f??'yes'!!'no', $ex??'yes'!!'no');
    if $ex && $fd.f {
      log(DEBUG, 'exists in dir and META, removing');
      $fd.unlink;
      $me<resources> = $me<resources>.grep({"resources/$_".IO.relative($cwd) ne $fd.relative($cwd)}).sort;
      $cwd.add('META6.json').spurt: to-j($me);
      log(DEBUG, 'new resources: %s', $me<resources>.join(', '));
    } elsif $fd.f {
      log(WARN, 'File %s exists in resources/ but not in META6.json, file will remain in resources/ in case this was a mistake', $fd.relative($cwd));
      $rc = 1;
    } elsif $ex {
      log(WARN, 'File %s exists in META6.json but not in META6.json, will remove from META6.json', $fd.relative($cwd));
      $me<resources> = $me<resources>.grep({"resources/$_".IO.relative($cwd) ne $fd.relative($cwd)}).sort;
      $cwd.add('META6.json').spurt: to-j($me);
      log(DEBUG, 'new resources: %s', $me<resources>.join(', '));
    }

    exit $rc;
  }

  my &ensure-resource-in-meta = sub {
    log(DEBUG, 'ensuring resource is in meta: %s', $path);
    my %meta = from-j($cwd.add('META6.json').slurp);
    if (%meta<resources>||[]).grep($path).elems == 0 {
      log(MSG, 'Resource is not in META, adding it');
      %meta<resources> = [|%meta<resources>, $path];
      $cwd.add('META6.json').spurt: to-j(%meta);
    }
  };

  mkdir($resource-dir) unless $resource-dir.d;
  if $resource-dir.add($path).e {
    log(MSG, 'Resource exists - not creating any directories or files');
    ensure-resource-in-meta;
    exit 0;
  }

  if $path ~~ m/(^|'/'|'\\')'..'('/'|'\\'|$)/ {
    log(FATAL, "Cannot create resources outside of the project resources/ dir or with relative paths\nWhatever path is provided will automatically reside under {$resource-dir.absolute}");
  }

  my @parts = $path.IO.relative.split($*DISTRO.is-win??'\\'!!'/', :skip-empty);
  my $fname = @parts.pop;
  my $cdir := $resource-dir;

  log(DEBUG, 'resolving: %s', @parts.join('/'));
  while @parts.elems {
    $cdir = $cdir.add(@parts.shift);
    log(DEBUG, "making {$cdir.absolute}") unless $cdir.IO.d;
    log(DEBUG, "skip {$cdir.absolute}") if $cdir.IO.d;
    mkdir $cdir unless $cdir.IO.d;
  }

  log(DEBUG, 'creating resource:');
  $cdir.add($fname).spurt: '';
  ensure-resource-in-meta;
}



#multi MAIN(*@p, *%n) {
#  ## load extensions
#  my @l = |(user-config<extensions>//[]), |(env-config<extensions>//[]);
#  my %*USAGE;
#  for @l -> $ext {
#    CATCH {
#      default {
#        $*ERR.say: "=<< error loading requested extension: $ext";
#      }
#    }
#    require ::("$ext") <&MAIN>;
#    try {
#      %*USAGE = %*USAGE, | ( try { ::("{$ext}::EXPORT::DEFAULT::%usage"); } // {});
#      ::("{$ext}::EXPORT::DEFAULT::&MAIN").(|@p, |%n);
#      exit 0;
#    };
#  }
#  MAIN(:h);
#  exit 1;
#}


