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;
