package PMG::API2::ACMEPlugin;

use strict;
use warnings;

use Storable qw(dclone);

use PVE::ACME::Challenge;
use PVE::ACME::DNSChallenge;
use PVE::ACME::StandAlone;
use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
use PVE::Tools qw(extract_param);

use base qw(PVE::RESTHandler);

my $inotify_file_id = 'pmg-acme-plugins-config.conf';
my $config_filename = '/etc/pmg/acme/plugins.conf';
my $lockfile = "/var/lock/pmg-acme-plugins-config.lck";

PVE::ACME::DNSChallenge->register();
PVE::ACME::StandAlone->register();
PVE::ACME::Challenge->init();

PVE::JSONSchema::register_standard_option(
    'pmg-acme-pluginid',
    {
        type => 'string',
        format => 'pve-configid',
        description => 'Unique identifier for ACME plugin instance.',
    },
);

sub read_pmg_acme_challenge_config {
    my ($filename, $fh) = @_;
    my $raw = defined($fh) ? do { local $/ = undef; <$fh> } : '';
    return PVE::ACME::Challenge->parse_config($filename, $raw);
}

sub write_pmg_acme_challenge_config {
    my ($filename, $fh, $cfg) = @_;
    my $raw = PVE::ACME::Challenge->write_config($filename, $cfg);
    PVE::Tools::safe_print($filename, $fh, $raw);
}

PVE::INotify::register_file(
    $inotify_file_id,
    $config_filename,
    \&read_pmg_acme_challenge_config,
    \&write_pmg_acme_challenge_config,
    undef,
    always_call_parser => 1,
);

sub lock_config {
    my ($code) = @_;
    my $p = PVE::Tools::lock_file($lockfile, undef, $code);
    die $@ if $@;
    return $p;
}

sub load_config {
    # auto-adds the standalone plugin if no config is there for backwards
    # compatibility, so ALWAYS call the cfs registered parser
    return PVE::INotify::read_file($inotify_file_id);
}

sub write_config {
    my ($self) = @_;
    return PVE::INotify::write_file($inotify_file_id, $self);
}

my $plugin_type_enum = PVE::ACME::Challenge->lookup_types();

my $modify_cfg_for_api = sub {
    my ($cfg, $pluginid) = @_;

    die "ACME plugin '$pluginid' not defined\n" if !defined($cfg->{ids}->{$pluginid});

    my $plugin_cfg = dclone($cfg->{ids}->{$pluginid});
    $plugin_cfg->{plugin} = $pluginid;
    $plugin_cfg->{digest} = $cfg->{digest};

    return $plugin_cfg;
};

__PACKAGE__->register_method({
    name => 'index',
    path => '',
    method => 'GET',
    permissions => { check => ['admin'] },
    description => "ACME plugin index.",
    protected => 1,
    parameters => {
        additionalProperties => 0,
        properties => {
            type => {
                description => "Only list ACME plugins of a specific type",
                type => 'string',
                enum => $plugin_type_enum,
                optional => 1,
            },
        },
    },
    returns => {
        type => 'array',
        items => {
            type => "object",
            properties => {
                plugin => get_standard_option('pmg-acme-pluginid'),
            },
        },
        links => [{ rel => 'child', href => "{plugin}" }],
    },
    code => sub {
        my ($param) = @_;

        my $cfg = load_config();

        my $res = [];
        foreach my $pluginid (keys %{ $cfg->{ids} }) {
            my $plugin_cfg = $modify_cfg_for_api->($cfg, $pluginid);
            next if $param->{type} && $param->{type} ne $plugin_cfg->{type};
            push @$res, $plugin_cfg;
        }

        return $res;
    },
});

__PACKAGE__->register_method({
    name => 'get_plugin_config',
    path => '{id}',
    method => 'GET',
    description => "Get ACME plugin configuration.",
    permissions => { check => ['admin'] },
    protected => 1,
    parameters => {
        additionalProperties => 0,
        properties => {
            id => get_standard_option('pmg-acme-pluginid'),
        },
    },
    returns => {
        type => 'object',
    },
    code => sub {
        my ($param) = @_;

        my $cfg = load_config();
        return $modify_cfg_for_api->($cfg, $param->{id});
    },
});

__PACKAGE__->register_method({
    name => 'add_plugin',
    path => '',
    method => 'POST',
    description => "Add ACME plugin configuration.",
    permissions => { check => ['admin'] },
    protected => 1,
    parameters => PVE::ACME::Challenge->createSchema(),
    returns => {
        type => "null",
    },
    code => sub {
        my ($param) = @_;

        my $id = extract_param($param, 'id');
        my $type = extract_param($param, 'type');

        lock_config(sub {
            my $cfg = load_config();
            die "ACME plugin ID '$id' already exists\n" if defined($cfg->{ids}->{$id});

            my $plugin = PVE::ACME::Challenge->lookup($type);
            my $opts = $plugin->check_config($id, $param, 1, 1);

            $cfg->{ids}->{$id} = $opts;
            $cfg->{ids}->{$id}->{type} = $type;

            write_config($cfg);
        });
        die "$@" if $@;

        return undef;
    },
});

__PACKAGE__->register_method({
    name => 'update_plugin',
    path => '{id}',
    method => 'PUT',
    description => "Update ACME plugin configuration.",
    permissions => { check => ['admin'] },
    protected => 1,
    parameters => PVE::ACME::Challenge->updateSchema(),
    returns => {
        type => "null",
    },
    code => sub {
        my ($param) = @_;

        my $id = extract_param($param, 'id');
        my $delete = extract_param($param, 'delete');
        my $digest = extract_param($param, 'digest');

        lock_config(sub {
            my $cfg = load_config();
            PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
            my $plugin_cfg = $cfg->{ids}->{$id};
            die "ACME plugin ID '$id' does not exist\n" if !$plugin_cfg;

            my $type = $plugin_cfg->{type};
            my $plugin = PVE::ACME::Challenge->lookup($type);

            if (defined($delete)) {
                my $schema = $plugin->private();
                my $options = $schema->{options}->{$type};
                for my $k (PVE::Tools::split_list($delete)) {
                    my $d = $options->{$k} || die "no such option '$k'\n";
                    die "unable to delete required option '$k'\n" if !$d->{optional};

                    delete $cfg->{ids}->{$id}->{$k};
                }
            }

            my $opts = $plugin->check_config($id, $param, 0, 1);
            for my $k (keys %$opts) {
                $plugin_cfg->{$k} = $opts->{$k};
            }

            write_config($cfg);
        });
        die "$@" if $@;

        return undef;
    },
});

__PACKAGE__->register_method({
    name => 'delete_plugin',
    path => '{id}',
    method => 'DELETE',
    description => "Delete ACME plugin configuration.",
    permissions => { check => ['admin'] },
    protected => 1,
    parameters => {
        additionalProperties => 0,
        properties => {
            id => get_standard_option('pmg-acme-pluginid'),
        },
    },
    returns => {
        type => "null",
    },
    code => sub {
        my ($param) = @_;

        my $id = extract_param($param, 'id');

        lock_config(sub {
            my $cfg = load_config();

            delete $cfg->{ids}->{$id};

            write_config($cfg);
        });
        die "$@" if $@;

        return undef;
    },
});

1;
