package PVE::API2::ACMEPlugin;

use strict;
use warnings;

use MIME::Base64;
use Storable qw(dclone);

use PVE::ACME::Challenge;
use PVE::ACME::DNSChallenge;
use PVE::ACME::StandAlone;
use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_register_file cfs_lock_file);
use PVE::JSONSchema qw(register_standard_option get_standard_option);
use PVE::Tools qw(extract_param);

use base qw(PVE::RESTHandler);

my $plugin_config_file = "priv/acme/plugins.cfg";

cfs_register_file(
    $plugin_config_file,
    sub { PVE::ACME::Challenge->parse_config(@_); },
    sub { PVE::ACME::Challenge->write_config(@_); },
);

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

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

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 => ['perm', '/', ['Sys.Modify']],
    },
    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('pve-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 => ['perm', '/', ['Sys.Modify']],
    },
    protected => 1,
    parameters => {
        additionalProperties => 0,
        properties => {
            id => get_standard_option('pve-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 => ['perm', '/', ['Sys.Modify']],
    },
    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');

        cfs_lock_file(
            $plugin_config_file,
            undef,
            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;

                cfs_write_file($plugin_config_file, $cfg);
            },
        );
        die "$@" if $@;

        return undef;
    },
});

__PACKAGE__->register_method({
    name => 'update_plugin',
    path => '{id}',
    method => 'PUT',
    description => "Update ACME plugin configuration.",
    permissions => {
        check => ['perm', '/', ['Sys.Modify']],
    },
    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');

        cfs_lock_file(
            $plugin_config_file,
            undef,
            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 (sort keys %$opts) {
                    $plugin_cfg->{$k} = $opts->{$k};
                }

                cfs_write_file($plugin_config_file, $cfg);
            },
        );
        die "$@" if $@;

        return undef;
    },
});

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

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

        cfs_lock_file(
            $plugin_config_file,
            undef,
            sub {
                my $cfg = load_config();

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

                cfs_write_file($plugin_config_file, $cfg);
            },
        );
        die "$@" if $@;

        return undef;
    },
});

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

1;
