use Mozilla::LDAP::Conn;
use Mozilla::LDAP::Utils qw(normalizeDN);
use Mozilla::LDAP::API qw(:constant ldap_url_parse ldap_explode_dn);
use File::Basename;
use File::Copy;
use DSUpdate qw(isOffline);

# Upgrade DN format if needed.
# For each backend instance, 
#     run upgradednformat with -N (dryrun mode),
#     if it returns 0 (Upgrade candidates are found), 
#     recursively copy the instance dir to the work dir (dnupgrade)
#     run upgradednformat w/o -N against the DB in the work dir
#     if it went ok, replace the original instance dir with the work dir.
# Note: This script does nothing if the server is up.
sub runinst {
    my ($inf, $inst, $dseldif, $conn) = @_;

    my @errs;

    my $config = "cn=config";
    my $config_entry = $conn->search($config, "base", "(cn=*)");
    if (!$config_entry) {
        return ("error_no_configuration_entry", $!);
    }

    # Check if the server is up or not
    my $rc;
    ($rc, @errs) = isOffline($inf, $inst, $conn);
    if (!$rc) {
        return @errs;
    }
    my $mappingtree = "cn=mapping tree,cn=config";
    my $ldbmbase = "cn=ldbm database,cn=plugins,cn=config";

    my $backend_entry;
    my $mtentry = $conn->search($mappingtree, "onelevel", "(cn=*)", 0, @attr);
    if (!$mtentry) {
        return ();
    }

    my $db_config_entry = 
            $conn->search("cn=config,cn=ldbm database,cn=plugins,cn=config",
            "base", "(objectclass=*)");
    if (!$db_config_entry) {
        return ('error_finding_config_entry',
                'cn=config,cn=ldbm database,cn=plugins,cn=config',
                $conn->getErrorString());
    }
    # If subtree rename swich is not found in the config file,
    # set off to the switch and upgrade dn format using entrydn.
    my $switch = $db_config_entry->getValues('nsslapd-subtree-rename-switch');
    if ("" eq $switch) { 
        $db_config_entry->addValue('nsslapd-subtree-rename-switch', "off");
        $conn->update($db_config_entry);
    }

    # If a suffix in the mapping tree is doube-quoted and 
    # the cn value has only the double-quoted value, e.g.
    #   dn: cn="dc=example,dc=com",cn=mapping tree,cn=config
    #   cn: "dc=example,dc=com"
    # the following code adds non-quoted value:
    #   cn: dc=example,dc=com
    while ($mtentry) {
        my $numvals = $mtentry->size("cn");
        my $i;
        my $withquotes = -1;
        my $noquotes = -1;
        for ($i = 0; $i < $numvals; $i++) {
            if ($mtentry->{"cn"}[$i] =~ /^".*"$/) {
                $withquotes = $i;
            } else {
                $noquotes = $i;
            }
        }
        if ($withquotes >= 0 && $noquotes == -1) {
            # Has only cn: "<suffix>"
            # Adding cn: <suffix>
            my $stripped = $mtentry->{"cn"}[$withquotes];
            $stripped =~ s/^"(.*)"$/$1/;
            $mtentry->addValue("cn", $stripped);
            $conn->update($mtentry);
        }
        $mtentry = $conn->nextEntry();
    }

    my $ldifdir = $config_entry->{"nsslapd-ldifdir"}[0];
    my $instancedir = $config_entry->{"nsslapd-instancedir"}[0];
    my ($slapd, $serverID) = split(/-/, $instancedir, 2);
    my $upgradednformat = "/usr/bin/upgradednformat -Z $serverID";
    my $reindex = "/usr/bin/db2index -Z $serverID";

    # Scan through all of the backends to see if any of them
    # contain escape characters in the DNs.  If we find any
    # escapes, we need to run the conversion tool on that
    # backend.
    $backend_entry = $conn->search($ldbmbase, "onelevel", "(objectClass=nsBackendInstance)", 0, @attr);
    if (!$backend_entry) {
        return ("error_no_backend_entries", $!);
    }

    while ($backend_entry) {
        my $backend = $backend_entry->{"cn"}[0];
        my $dbinstdir = $backend_entry->{"nsslapd-directory"}[0];
        my $workdir = $dbinstdir . "/dnupgrade";
        my $dbdir = dirname($dbinstdir);
        my $pdbdir = dirname($dbdir);
        my $instname = basename($dbinstdir);
        my $dn_norm_sp_txt = $ldifdir . "/" . $instname . "_dn_norm_sp.txt";

        if ("$dbdir" eq "" || "$instname" eq "") {
            push @errs, ["error_invalid_dbinst_dir", $dbinstdir];
            return @errs;
        }

        # clean up db region files, which might contain the old pages
        if ( -d $dbdir  && -f $dbdir."/__db.001") {
            unlink <$dbdir/__db.*>;
        }

        my $escapes = 0;
        my $rc = 0;
        my $cmd = 0;
        if ((-e "$dbinstdir/id2entry.db") || (-e "$dbinstdir/id2entry.db4")) {
            # Check if any DNs contain escape characters with dbscan.
            # dryrun mode
            # return values:  0 -- need to upgrade dn format
            #                 1 -- no need to upgrade dn format
            #                -1 -- error
            $cmd = "$upgradednformat -n $backend -a $dbinstdir -N";
            $rc = system("$cmd");
            if ($rc & 127) {
                push @errs, [ 'error_running_command', $cmd, $rc, $! ];
                return @errs;
            }
            $escapes = $rc >> 8;
# $escapes == 0 ==> no need to do dn upgrade
# $escapes == 1 ==> need to do dn upgrade (both dn upgrade and spaces)
# $escapes == 2 ==> need to do dn upgrade (dn upgrade only)
# $escapes == 3 ==> need to do dn upgrade (dn upgrade spaces only)
            if ($escapes <= 0) {
                # already upgraded or an error occurred.
                # check ancestorid to see if it has not-sorted ID list or not.
                my $ancestorid = $dbinstdir . "/ancestorid.db4";
                if (!(-e "$ancestorid")) {
                    $ancestorid = $dbinstdir . "/ancestorid.db";
                }
                if (-e "$ancestorid") {
                    my $disorder = 0;
                    open(ANCESTOR, "/usr/bin/dbscan -f $ancestorid -r |");
                    while (<ANCESTOR>) {
                        if (!/^=[0-9]*/) {
                            chomp($_);
                            my @IDs = split(/ |	/, $_);
                            # print "ID count: $#IDs\n";
                            my $lasti = $#IDs;
                            for (my $i = 1; $i < $lasti; $i++) {
                                if ($IDs[$i] >= $IDs[$i + 1]) {
                                    $disorder = 1;
                                    last;
                                }
                            }
                            # print "Result: $disorder \n";
                            if ($disorder) {
                                last;
                            }
                        }
                    }
                    close(ANCESTOR);

                    # ancestorid index is in disorder; need to reindex it.
                    if ($disorder) {
                        print "The ancestorid index in $backend is in disorder; Reindexing $ancestorid.\n";
                        $cmd = "$reindex -n $backend -t ancestorid";
                        $rc = system("$cmd");
                        if ($rc & 127) {
                            push @errs, [ 'error_running_command', $cmd, $rc, $! ];
                            return @errs;
                        }
                    }
                }
            } else {
                # need to upgrade dn format
                $rc = 0;

                if (system("cd $pdbdir; tar cf - db/DBVERSION | (cd $dbinstdir; tar xf -)") ||
                    system("cd $pdbdir; tar cf - db/$instname/DBVERSION | (cd $dbinstdir; tar xf -)") ||
                    system("cd $pdbdir; tar cf - db/$instname/*.db* | (cd $dbinstdir; tar xf -)")) {
                    push @errs, [ "error_cant_backup_db", $backend, $! ];
                    return @errs;
                }
                my @stat = stat("$dbdir");
                my $mode = $stat[2];
                my $uid = $stat[4];
                my $gid = $stat[5];

                move("$dbinstdir/db", "$workdir");
                chmod($mode, $workdir);
                chown($uid, $gid, $workdir);

                @stat = stat("$dbinstdir");
                $mode = $stat[2];
                $uid = $stat[4];
                $gid = $stat[5];

                chmod($mode, "$workdir/$instname");
                chown($uid, $gid, "$workdir/$instname");

                my $do_dn_norm_sp = 0;
                if ((1 == $escapes) || (3 == $escapes)) {
                    # We are taking care of spaces in DN.
                    my $sorted = $dn_norm_sp_txt . ".sorted";
                    $cmd = "sort $dn_norm_sp_txt";
                    $rc = system("$cmd > $sorted");
                    if ($rc) {
                        debug(1, "Error: $cmd failed - output $sorted: $!\n");
                        push @errs, [ 'error_running_command', $cmd, $rc, $! ];
                        return @errs;
                    }
                    # Create a work file from $sorted.
                    my $workfile = $sorted . ".work";
                    # print "$sorted -> $workfile\n";

                    open(SORTEDFILE, "$sorted");
                    open(WORKFILE, "> $workfile");
                    my $prev_dn = "";
                    my $prev_id = 0;
                    my $new = 1;
                    for (my $line = <SORTEDFILE>; $line; $line = <SORTEDFILE>) {
                        chomp($line);
                        my ($dn, $id) = split(":", $line, 2);
                        if ($dn eq $prev_dn) {
                            if ($new == 1) {
                                print WORKFILE "$prev_id:$id";
                                $new = 0;
                                $do_dn_norm_sp = 1; # go ahead and fix it.
                            } else {
                                print WORKFILE " $id";
                            }
                        } else {
                            if (0 == $new) {
                                print WORKFILE "\n";
                                $new = 1;
                            }
                        }
                        $prev_dn = $dn;
                        $prev_id = $id;
                    }
                    close(WORKFILE);
                    close(SORTEDFILE);
                    unlink <$sorted>;
                    move("$dn_norm_sp_txt", "$dn_norm_sp_txt.orig");
                    if ($do_dn_norm_sp) {
                        move("$workfile", "$dn_norm_sp_txt");
                        # otherwise, we don't need $dn_norm_sp_txt
                    }
                } else {
                    unlink <$dn_norm_sp_txt>;
                }

                if ((1 == $escapes) || (2 == $escapes) || (3 == $escapes)) {
                    # call conversion tool here and get return status.
                    $cmd = "$upgradednformat -n $backend -a $workdir/$instname";
                    $rc = system("$cmd");
                    if ($rc & 127) {
                        push @errs, [ 'error_running_command', $cmd, $rc, $! ];
                        return @errs;
                    }
                    $escapes = $rc >> 8;
                }
                if ((0 == $rc) || (1 == $escapes) || (3 == $escapes)) {
                    # success
                    move("$dbinstdir", "$dbinstdir.orig");
                    move("$dbinstdir.orig/dnupgrade/$instname", "$dbinstdir");
                    copy("$dbinstdir.orig/dnupgrade/DBVERSION", "$dbdir");
                    if ((1 == $escapes) || (3 == $escapes)) {
                        $cmd = "$reindex -n $backend -t entryrdn";
                        $rc = system("$cmd");
                        if ($rc & 127) {
                            push @errs, [ 'error_running_command', $cmd, $rc, $! ];
                            return @errs;
                        }
                    }
                    my $dn_norm_sp_txt = $ldifdir . "/" . $instname . "_dn_norm_sp.txt";
                    my $conflict = $ldifdir . "/" . $instname . "_conflict.txt";
                    system("echo prinary entry ID: duplicated entry IDs > $conflict");
                    system("cat $dn_norm_sp_txt >> $conflict");
                    print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n";
                    print "Duplicated DN(s) were found and renamed.\n";
                    print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n";
                    print "Renamed entry IDs are listed in $conflict.\n";
                } else {
                    # Conversion failed. Cleanup and bail.
                    unlink <$dbinstdir/dnupgrade/$backend/*>;
                    rmdir("$dbinstdir/dnupgrade/$backend");
                    unlink <$dbinstdir/dnupgrade/*>;
                    rmdir("$dbinstdir/dnupgrade");
                    return ("error_cant_convert_db", $backend, $rc);
                }
            }
        }

        $backend_entry = $conn->nextEntry();
    }

    return ();
}
