#!/opt/bin/perl

=head1 NAME

anyevent-mcp - initialize MCP configuration and manage users and permissions

=head1 SYNOPSIS

   anyevent-mcp init
   anyevent-mcp manage-rules fs-default
   anyevent-mcp create-user chatgpt
   anyevent-mcp revoke-user chatgpt
   anyevent-mcp edit-rules-file /path/to/rules.ini

=head1 DESCRIPTION

C<anyevent-mcp> sets up the shared AnyEvent::MCP configuration and manages
permission rules saved by its servers. Use it to review permissions you have
allowed or denied permanently, remove a rule, or change the order in which
rules are checked.

C<manage-rules> opens a graphical rule manager; C<edit-rules> opens the
rule file in your text editor. Both take a rule profile name. For example,
C<anyevent-mcp manage-rules fs-default> manages the default filesystem
server's rules. The corresponding C<*-file> commands accept a pathname
when an application stores its rules elsewhere.

C<init> creates the private global configuration and its master secret,
from which servers can derive their connection credentials. You normally
do not need to run it: a server creates a missing secret when it loads its
configuration. Use C<init> when you want to create the configuration
explicitly; it refuses to overwrite an existing file.

See L<AnyEvent::MCP::Intro> for setting up and connecting to the servers,
L<AnyEvent::MCP/GLOBAL CONFIGURATION> for the configuration file format,
and L<AnyEvent::MCP::Server::Permissions/RULE FILES> for permission rules.

=head1 COMMANDS

=over

=item init

Create the private global configuration and master secret. Unlike ordinary
configuration loading, this fails rather than overwriting an existing file.
C<--config FILE> selects a different configuration file for this command.

=item manage-rules NAME

Open the YAD rule deletion and reordering interface for the named profile.
For a filesystem server started with C<--name code>, use profile C<fs-code>.

=item edit-rules NAME

Open the named profile's rule file in a text editor.

=item manage-rules-file $file

Open the rule manager for the explicit pathname C<$file>.

=item edit-rules-file $file

Open C<$file> in a text editor. Use these explicit-file commands for
applications whose rule files do not use the named-profile convention.

=item create-user USER

Enable a new named user; rotate a revoked user at the next generation.
Use --program or --instance for a scoped override.

=item revoke-user USER

Disable a user, preserving the generation for later rotation. In a narrower
scope, a negative definition also blocks inherited credentials. Implicit
global:default is revocable. Restart affected servers after changes; these
commands do not print secrets. Use server --print-url[=USER] for URLs.

=back

Named profiles use
C<anyevent-mcp/rules/NAME.ini> in the user configuration directory.
Editing uses C<$VISUAL>, C<$EDITOR>, C<sensible-editor> or C<vi>.
For server URLs, use each server's C<--print-url> option.

=head1 OPTIONS

=over

=item --config FILE

Use FILE instead of default global configuration
(C<$XDG_CONFIG_HOME/anyevent-mcp/global.ini>, or
C<~/.config/anyevent-mcp/global.ini>). Relevant to C<init> and user
management; rule management uses its own profile or pathname.

=item --program NAME

Select C<[program NAME]> for user management.

=item --instance NAME

Select C<[instance NAME]> for user management. Cannot combine with --program.

=item --help

Print command synopsis, available commands and configuration defaults,
then exit without running a command.

=back

=cut

use common::sense;

use Getopt::Long ();

use AnyEvent::MCP ();
use AnyEvent::MCP::Config ();
use AnyEvent::MCP::File ();

sub usage {
   print <<'EOF';
Usage: anyevent-mcp [OPTIONS] COMMAND [ARGUMENTS]

   manage-rules NAME       delete/reorder rules in YAD
   edit-rules NAME         edit rules with VISUAL, EDITOR, sensible-editor, vi
   manage-rules-file FILE  manage an explicit rules file
   edit-rules-file FILE    edit an explicit rules file
   init                    create a private configuration and master secret
   create-user USER        enable or rotate a managed user
   revoke-user USER        revoke a managed user

Options (before COMMAND):
   --config FILE           use a different global configuration file
   --program NAME          select [program NAME] for user management
   --instance NAME         select [instance NAME] for user management
   --help                  show this help

User definitions inherit from [global] through [program NAME] to
[instance NAME]; narrower scopes override wider ones. Without --program
or --instance, user commands modify [global]. Revoked users remain
disabled until recreated with a new token generation. The implicit
global:default user is enabled initially.

Examples:
   anyevent-mcp create-user chatgpt
   anyevent-mcp --program mcp-fs create-user backup
   anyevent-mcp --instance private revoke-user alice
   anyevent-mcp revoke-user chatgpt
   mcp-multibroker --print-urls        # list all enabled user URLs
   mcp-multibroker --print-url=chatgpt # show just chatgpt's URL

Server changes take effect after restart. The selected connect_user
(default: default) authenticates auxiliary browser/Vim/reverse connections.

Rule profiles are stored under $XDG_CONFIG_HOME/anyevent-mcp, falling back
to ~/.config/anyevent-mcp/rules (e.g. manage-rules fs-default).

Configuration defaults to ~/.config/anyevent-mcp/global.ini, or the
corresponding location under XDG_CONFIG_HOME. Use --help for this text.

A missing master secret is generated and saved automatically when a server
loads its configuration. Configuration format: see AnyEvent::MCP.
EOF
}

sub change_user {
   my ($file, $section, $user, $command) = @_;

   my $lock = AnyEvent::MCP::File::lock $file;
   open my $fh, "<:raw", $file or die "$file: $!\n";
   local $/;
   my $body = <$fh>;
   close $fh or die "$file: $!\n";

   my @lines = split /(?<=\n)/, $body;
   my ($current, $found, $position, $value);
   my $key = "user.$user";

   for my $i (0 .. $#lines) {
      if ($lines[$i] =~ /^\s*\[([^\]\r\n]+)\]\s*(?:[;#][^\r\n]*)?\r?\n?\z/) {
         $current = $1;
         $position = $i + 1 if $current eq $section;
      } elsif ($current eq $section && $lines[$i] =~ /^\s*\Q$key\E\s*=\s*([^\r\n;#]*)(?:[;#][^\r\n]*)?\r?\n?\z/) {
         die "Duplicate $key in [$section]\n" if defined $found;
         ($found, $value) = ($i, $1);
         $value =~ s/\s+\z//;
      }
   }

   my $generation;
   if (defined $found) {
      $value =~ /\A-?[1-9][0-9]*\z/ && length $value < 18
         or die "Invalid $key generation '$value'\n";
      $generation = 0 + $value;
   }

   if ($command eq "create-user") {
      die "$key already enabled in [$section]\n"
         if defined $generation && $generation > 0 || !defined $generation && $section eq "global" && $user eq "default";
      $generation = defined $generation ? -$generation + 1 : 1;
   } else {
      die "$key already revoked in [$section]\n" if defined $generation && $generation < 0;
      if (!defined $generation) {
         ($section ne "global" || $user eq "default")
            or die "$key is not enabled\n";
         $generation = -1;
      } else {
         $generation = -$generation;
      }
   }

   if (defined $found) {
      $lines[$found] = "$key = $generation\n";
   } elsif (defined $position) {
      splice @lines, $position, 0, "$key = $generation\n";
   } else {
      push @lines, "\n[$section]\n$key = $generation\n";
   }
   AnyEvent::MCP::File::replace $file, join "", @lines;
   close $lock;
   print "$section: $user generation " . abs ($generation)
      . ($generation > 0 ? " enabled\n" : " revoked\n");
}

Getopt::Long::Configure ("no_auto_abbrev", "no_ignore_case", "require_order");
Getopt::Long::GetOptions ("config=s" => \my $file, "program=s" => \my $program,
                         "instance=s" => \my $instance, "help" => \my $help)
   or exit 2;

if ($help) {
   usage;
   exit 0;
}

my $command = shift @ARGV;
unless (defined $command) {
   usage;
   exit 2;
}

@ARGV = map AnyEvent::MCP::utf8dec $_, @ARGV;

eval {
   if ($command =~ /\A(?:manage-rules|edit-rules)(?:-file)?\z/ && @ARGV == 1) {
      my $path = $ARGV[0];
      if ($command !~ /-file\z/) {
         $path =~ /\A[A-Za-z0-9][A-Za-z0-9_.-]*\z/
            or die "Invalid permission rule profile name\n";
         my $config_home = $ENV{XDG_CONFIG_HOME} || ($ENV{HOME} || die "HOME is not set\n") . "/.config";
         $config_home =~ m{\A/} or die "XDG_CONFIG_HOME must be an absolute path\n";
         $path = "$config_home/anyevent-mcp/rules/$path.ini";
      }
      my $action = $command =~ /\Amanage-/ ? "--manage-rules" : "--edit-rules";
      exec "anyevent-mcp-askpermission", $action, AnyEvent::MCP::utf8enc $path;
      die "Cannot start permission rule tool: $!\n";
   } elsif ($command eq "init" && !@ARGV) {
      my $config = AnyEvent::MCP::Config->init (defined $file ? (file => $file) : ());
      print "Created $config->{file}\n";
   } elsif ($command =~ /\A(?:create-user|revoke-user)\z/ && @ARGV == 1) {
      !defined $program || !defined $instance or die "--program and --instance are exclusive\n";
      my $user = AnyEvent::MCP::Config::server_name ($ARGV[0]);
      my $scope = defined $program ? "program " . AnyEvent::MCP::Config::server_name ($program)
                : defined $instance ? "instance " . AnyEvent::MCP::Config::server_name ($instance)
                : "global";
      my $config_file = $file // AnyEvent::MCP::Config::_file ();
      AnyEvent::MCP::Config->new (file => $config_file);
      change_user $config_file, $scope, $user, $command;
   } else {
      die "Invalid command or arguments; use --help\n";
   }
   1
} or do {
   print STDERR $@;
   exit 1;
};

