package PVE::Network::SDN::Ipams::PVEPlugin;

use strict;
use warnings;
use PVE::INotify;
use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_register_file cfs_lock_file);
use PVE::Tools;
use JSON;
use NetAddr::IP qw(:lower);

use Net::IP;
use Digest::SHA;

use base('PVE::Network::SDN::Ipams::Plugin');


my $ipamdb_file = "sdn/pve-ipam-state.json";
my $ipamdb_file_legacy = "priv/ipam.db";

PVE::Cluster::cfs_register_file(
    $ipamdb_file,
    sub {
	my ($filename, $data) = @_;
	if (defined($data)) {
	    return PVE::Network::SDN::Ipams::PVEPlugin->parse_config($filename, $data);
	} else {
	    # TODO: remove legacy state file handling with PVE 9+ after ensuring all call sites got
	    # switched over.
	    return cfs_read_file($ipamdb_file_legacy);
	}
    },
    sub {
	my ($filename, $data) = @_;
	# TODO: remove below with PVE 9+, add a pve8to9 check to allow doing so.
	if (-e $ipamdb_file_legacy && -e $ipamdb_file) {
	    # only clean-up if we succeeded to write the new path at least once
	    unlink $ipamdb_file_legacy or $!{ENOENT} or warn "failed to unlink legacy IPAM DB - $!\n";
	}
	return PVE::Network::SDN::Ipams::PVEPlugin->write_config($filename, $data);
    },
);

PVE::Cluster::cfs_register_file(
    $ipamdb_file_legacy,
    sub { PVE::Network::SDN::Ipams::PVEPlugin->parse_config(@_); },
    undef, # no writer for legacy file, all must go to the new file.
);

sub type {
    return 'pve';
}

sub properties {
}

sub options {
}

# Plugin implementation

sub add_subnet {
    my ($class, $plugin_config, $subnetid, $subnet) = @_;

    my $cidr = $subnet->{cidr};
    my $zone = $subnet->{zone};
    my $gateway = $subnet->{gateway};


    cfs_lock_file($ipamdb_file, undef, sub {
	my $db = {};
	$db = read_db();

	$db->{zones}->{$zone} = {} if !$db->{zones}->{$zone};
	my $zonedb = $db->{zones}->{$zone};

	if(!$zonedb->{subnets}->{$cidr}) {
	    #create subnet
	    $zonedb->{subnets}->{$cidr}->{ips} = {};
	    write_db($db);
	}
    });
    die "$@" if $@;
}

sub update_subnet {
    my ($class, $plugin_config, $subnetid, $subnet, $old_subnet, $noerr) = @_;
    # we don't need to do anything on update
}

sub only_gateway_remains {
    my ($ips) = @_;

    if (keys %{$ips} == 1 &&
	(values %{$ips})[0]->{gateway} == 1) {
	return 1;
    }
    return 0;
};

sub del_subnet {
    my ($class, $plugin_config, $subnetid, $subnet) = @_;

    my $cidr = $subnet->{cidr};
    my $zone = $subnet->{zone};

    cfs_lock_file($ipamdb_file, undef, sub {

	my $db = read_db();

	my $dbzone = $db->{zones}->{$zone};
	die "zone '$zone' doesn't exist in IPAM DB\n" if !$dbzone;
	my $dbsubnet = $dbzone->{subnets}->{$cidr};
	die "subnet '$cidr' doesn't exist in IPAM DB\n" if !$dbsubnet;

	my $ips = $dbsubnet->{ips};

	if (keys %{$ips} > 0 && !only_gateway_remains($ips)) {
	    die "cannot delete subnet '$cidr', not empty\n";
	}

	delete $dbzone->{subnets}->{$cidr};

	write_db($db);
    });
    die "$@" if $@;

}

sub add_ip {
    my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway) = @_;

    my $cidr = $subnet->{cidr};
    my $zone = $subnet->{zone};

    cfs_lock_file($ipamdb_file, undef, sub {

	my $db = read_db();
	my $dbzone = $db->{zones}->{$zone};
	die "zone '$zone' doesn't exist in IPAM DB\n" if !$dbzone;
	my $dbsubnet = $dbzone->{subnets}->{$cidr};
	die "subnet '$cidr' doesn't exist in IPAM DB\n" if !$dbsubnet;

	die "IP '$ip' already exist\n" if (!$is_gateway && defined($dbsubnet->{ips}->{$ip})) || ($is_gateway && defined($dbsubnet->{ips}->{$ip}) && !defined($dbsubnet->{ips}->{$ip}->{gateway}));

        my $data = {};
	if ($is_gateway) {
	    $data->{gateway} = 1;
	} else {
	    $data->{vmid} = $vmid if $vmid;
	    $data->{hostname} = $hostname if $hostname;
	    $data->{mac} = $mac if $mac;
	}

	$dbsubnet->{ips}->{$ip} = $data;

	write_db($db);
    });
    die "$@" if $@;
}

sub update_ip {
    my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway) = @_;
    return;
}

sub add_next_freeip {
    my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $vmid, $noerr) = @_;

    my $cidr = $subnet->{cidr};
    my $network = $subnet->{network};
    my $zone = $subnet->{zone};
    my $mask = $subnet->{mask};
    my $freeip = undef;

    cfs_lock_file($ipamdb_file, undef, sub {

	my $db = read_db();
	my $dbzone = $db->{zones}->{$zone};
	die "zone '$zone' doesn't exist in IPAM DB\n" if !$dbzone;
	my $dbsubnet = $dbzone->{subnets}->{$cidr};
	die "subnet '$cidr' doesn't exist in IPAM DB" if !$dbsubnet;

	if (Net::IP::ip_is_ipv4($network) && $mask == 32) {
	    die "cannot find free IP in subnet '$cidr'\n" if defined($dbsubnet->{ips}->{$network});
	    $freeip = $network;
	} else {
	    my $iplist = NetAddr::IP->new($cidr);
	    my $lastip = $iplist->last()->canon();
	    $iplist++ if Net::IP::ip_is_ipv4($network); #skip network address for ipv4
	    while(1) {
		my $ip = $iplist->canon();
		if (defined($dbsubnet->{ips}->{$ip})) {
		    last if $ip eq $lastip;
		    $iplist++;
		    next;
		} 
		$freeip = $ip;
		last;
	    }
	}

	die "can't find free ip in subnet '$cidr'\n" if !$freeip;

	$dbsubnet->{ips}->{$freeip} = {
	    mac => $mac,
	    hostname => $hostname,
	    vmid => $vmid
	};

	write_db($db);
    });
    die "$@" if $@;

    return $freeip;
}

sub add_range_next_freeip {
    my ($class, $plugin_config, $subnet, $range, $data, $noerr) = @_;

    my $cidr = $subnet->{cidr};
    my $zone = $subnet->{zone};

    cfs_lock_file($ipamdb_file, undef, sub {
	my $db = read_db();

	my $dbzone = $db->{zones}->{$zone};
	die "zone '$zone' doesn't exist in IPAM DB\n" if !$dbzone;

	my $dbsubnet = $dbzone->{subnets}->{$cidr};
	die "subnet '$cidr' doesn't exist in IPAM DB\n" if !$dbsubnet;

	my $ip = new Net::IP ("$range->{'start-address'} - $range->{'end-address'}")
	    or die "Invalid IP address(es) in Range!\n";
	my $mac = $data->{mac};

	do {
	    my $ip_address = $ip->version() == 6 ? $ip->short() : $ip->ip();
	    if (!$dbsubnet->{ips}->{$ip_address}) {
		$dbsubnet->{ips}->{$ip_address} = $data;
		write_db($db);

		return $ip_address;
	    }
	} while (++$ip);

	die "No free IP left in Range $range->{'start-address'}:$range->{'end-address'}}\n";
    });
}

sub del_ip {
    my ($class, $plugin_config, $subnetid, $subnet, $ip) = @_;

    my $cidr = $subnet->{cidr};
    my $zone = $subnet->{zone};

    cfs_lock_file($ipamdb_file, undef, sub {

	my $db = read_db();
	die "zone $zone don't exist in ipam db" if !$db->{zones}->{$zone};
	my $dbzone = $db->{zones}->{$zone};
	die "subnet $cidr don't exist in ipam db" if !$dbzone->{subnets}->{$cidr};
	my $dbsubnet = $dbzone->{subnets}->{$cidr};

	die "IP '$ip' does not exist in IPAM DB\n" if !defined($dbsubnet->{ips}->{$ip});
	delete $dbsubnet->{ips}->{$ip};
	write_db($db);
    });
    die "$@" if $@;
}

sub get_ips_from_mac {
    my ($class, $plugin_config, $mac, $zoneid) = @_;

    #just in case, as this should already be cached in local macs.db

    my $ip4 = undef;
    my $ip6 = undef;

    my $db = read_db();
    die "zone $zoneid don't exist in ipam db" if !$db->{zones}->{$zoneid};
    my $dbzone = $db->{zones}->{$zoneid};
    my $subnets = $dbzone->{subnets};

    for my $subnet ( keys %$subnets) {
	next if Net::IP::ip_is_ipv4($subnet) && $ip4;
	next if $ip6;
	my $ips = $subnets->{$subnet}->{ips};
	for my $ip (keys %$ips) {
	    my $ipobject = $ips->{$ip};
	    if ($ipobject->{mac} && $ipobject->{mac} eq $mac) {
		if (Net::IP::ip_is_ipv4($ip)) {
		    $ip4 = $ip;
		} else {
		    $ip6 = $ip;
		}
	    }
	}
	last if $ip4 && $ip6;
    }
    return ($ip4, $ip6);
}

#helpers

sub read_db {
    my $db = cfs_read_file($ipamdb_file);
    return $db;
}

sub write_db {
    my ($cfg) = @_;

    my $json = to_json($cfg);
    cfs_write_file($ipamdb_file, $json);
}

sub write_config {
    my ($class, $filename, $cfg) = @_;

    return $cfg;
}

sub parse_config {
    my ($class, $filename, $raw) = @_;

    $raw = '{}' if !defined($raw) ||$raw eq '';
    my $cfg = from_json($raw);

    return $cfg;
}

1;
