package org.postgresql.ers;

import java.util.*;
import java.sql.*;

/**
 * @author (c) 2003 Frank Thompson, Afilias Limited.
 * @author (c) 2000 Vadim Mikheev, PostgreSQL Inc. (original eRServer.pm)
**/

public final class ReplicationSetup
{
    private static final int TGT_MASTER = 0;
    private static final int TGT_SLAVE = 1;

    private static final int CMD_INIT = 0;
    private static final int CMD_DESTROY = 1;
    private static final int CMD_FLUSH = 2;

    public static void init_master_sysfunctions(PGConnection conn, String ers_libdir) throws Exception
    {
        ResultSet rs = null;
        Statement stmt = null;

        try
        {
            conn.begin();
            
            stmt = conn.createStatement();
            stmt.executeUpdate("set transaction isolation level serializable");
            
            rs = stmt.executeQuery("select proname from pg_proc where proname = '_rserv_log_'");
            if (rs.next()) return;

            stmt.executeUpdate("create function _rserv_log_() RETURNS opaque AS '"+ ers_libdir + "/erserver.so' LANGUAGE 'c'");
            stmt.executeUpdate("create function _rserv_sync_(int4) RETURNS int4 AS '"+ ers_libdir + "/erserver.so' LANGUAGE 'c'");
            stmt.executeUpdate("create function _rserv_debug_(int4) RETURNS int4 AS '"+ ers_libdir + "/erserver.so' LANGUAGE 'c'");
            stmt.executeUpdate("create function _rserv_lock_altid_(int4) RETURNS int4 AS '"+ ers_libdir + "/erserver.so' LANGUAGE 'c'");
            stmt.executeUpdate("create function _rserv_unlock_altid_(int4) RETURNS int4 AS '"+ ers_libdir + "/erserver.so' LANGUAGE 'c'");
            stmt.executeUpdate("create function _pte_get_snapshot_() RETURNS text AS '"+ ers_libdir + "/pte.so' LANGUAGE 'c'");
            stmt.executeUpdate("create function _pte_set_snapshot_(text) RETURNS int4 AS '"+ ers_libdir + "/pte.so' LANGUAGE 'c'");
            
            conn.commit();
        }
        catch(Exception xcp)
        {
            Log.log(xcp);
            conn.rollback();
            throw xcp;
        }
        finally 
        {
            DBUtils.close(stmt,rs);
        }
    }

    public static void init_master_systables(PGConnection conn) throws Exception
    {
        ResultSet rs = null;
        Statement stmt = null;

        try
        {
            conn.begin();
            
            stmt = conn.createStatement();
            
            rs = stmt.executeQuery("select relname from pg_class where relname = '_rserv_tables_'");

            if (!rs.next())
            {
                stmt.executeUpdate("create table _rserv_servers_ (server serial, host text, post int4, dbase text)");
                stmt.executeUpdate("create table _rserv_tables_  (tname name, cname name, reloid oid, key int4)");
                stmt.executeUpdate("create table _rserv_log_1_ (reloid oid, logid int4, logtime timestamp, deleted int4, key text)");
                stmt.executeUpdate("create index _rserv_log_1_indx_rel_key_ on _rserv_log_1_ (reloid, key)");
                stmt.executeUpdate("create index _rserv_log_1_indx_rel_id_ on _rserv_log_1_ (reloid, logid)");
                stmt.executeUpdate("create table _rserv_log_2_ (reloid oid, logid int4, logtime timestamp, deleted int4, key text)");
                stmt.executeUpdate("create index _rserv_log_2_indx_rel_key_ on _rserv_log_2_ (reloid, key)");
                stmt.executeUpdate("create index _rserv_log_2_indx_rel_id_ on _rserv_log_2_ (reloid, logid)");
                stmt.executeUpdate("create table _rserv_sync_ (server int4, syncid int4, synctime timestamp, status int4, minid int4, maxid int4, active text)");
                stmt.executeUpdate("create index _rserv_sync_indx_srv_id_ on _rserv_sync_ (server, syncid)");
                stmt.executeUpdate("create sequence _rserv_sync_seq_");
                stmt.executeUpdate("create sequence _rserv_slave_seq_");
                stmt.executeUpdate("create sequence _rserv_active_log_id_ minvalue 1 maxvalue 2");
                stmt.executeQuery("select setval('_rserv_active_log_id_', 1)");
                stmt.executeUpdate("create sequence _rserv_old_log_status_ minvalue 0 maxvalue 2");
                stmt.executeQuery("select setval('_rserv_old_log_status_', 0)");
            }

            conn.commit();
        }
        catch(Exception xcp)
        {
            Log.log(xcp);
            conn.rollback();
            throw xcp;
        }
        finally 
        {
            DBUtils.close(stmt,rs);
        }
    }

    public static void link_master_tables(PGConnection master_su_conn, PGConnection master_conn, String user, Collection exclude, Collection include) throws Exception
    {
        ResultSet rs1 = null;
        ResultSet rs2 = null;
        Statement stmt1 = null;
        Statement stmt2 = null;

        try
        {
            master_conn.begin();

            stmt1 = master_su_conn.createStatement();
            stmt2 = master_conn.createStatement();

            rs1 = stmt1.executeQuery("select relname from pg_class pgc, pg_shadow pgs where pgc.relkind = 'r' and pgc.relowner = pgs.usesysid and pgs.usename = '" + user + "'");

            stmt2.executeUpdate("set constraints all deferred");

            while (rs1.next())
            {
                String relname = DBUtils.getString(rs1,1);
                String short_relname = toShortTable(relname);

                if (relname.startsWith("_rserv") || relname.startsWith("pg_") || exclude.contains(relname) || (include.size() > 0 && !include.contains(relname))) continue;

                Log.debug("PROCESSING: relname="+relname+", short-relname="+short_relname);
                
                rs2 = stmt2.executeQuery("select pga.attname from pg_class pgc, pg_attribute pga where pgc.relname = '" + relname + "' and pgc.oid = pga.attrelid and pga.attname = '_rserv_ts'");

                boolean found = rs2.next();
                rs2.close(); rs2 = null;
                if (found) continue;

                Log.debug("ADDING-REPLICATION-HOOKS");

                stmt2.executeUpdate("lock " + relname + " in exclusive mode");
                stmt2.executeUpdate("create sequence _ers_" + short_relname + "_seq");
                stmt2.executeUpdate("alter table " + relname + " add column _rserv_ts integer");
                stmt2.executeUpdate("alter table " + relname + " alter column _rserv_ts set default nextval('_ers_" + short_relname + "_seq')");
                stmt2.executeUpdate("update " + relname + " set _rserv_ts = nextval('_ers_" + short_relname + "_seq') where _rserv_ts is null");
                stmt2.executeUpdate("alter table " + relname + " add constraint _ers_" + short_relname + "_nn check (_rserv_ts is not null)");
                stmt2.executeUpdate("create unique index _ers_" + short_relname + "_idx on " + relname + "(_rserv_ts)");

                rs2 = stmt2.executeQuery("select pgc.oid, pga.attnum from pg_class pgc, pg_attribute pga where pgc.relname = '" + relname + "' and pgc.oid = pga.attrelid and pga.attname = '_rserv_ts'");

                if (!rs2.next()) throw new SQLException("ERR: _rserv_ts fields not found for master table:" + relname);

                int oid = DBUtils.getInt(rs2,1);
                int attnum = DBUtils.getInt(rs2,2);

                stmt2.executeUpdate("create trigger _rserv_trigger_t_ after insert or update or delete on " + relname + " for each row execute procedure _rserv_log_('" + attnum + "')");
                stmt2.executeUpdate("insert into _rserv_tables_(tname, cname, reloid, key) values ('" + relname + "','_rserv_ts'," + oid + "," + attnum + ")");
                rs2.close(); rs2 = null;
            }

            master_conn.commit();
        }
        catch(Exception xcp)
        {
            Log.log(xcp);
            master_conn.rollback();
            throw xcp;
        }
        finally 
        {
            DBUtils.close(stmt1,rs1);
            DBUtils.close(stmt2,rs2);
        }
    }

    public static void flush_master_tables(PGConnection master_su_conn, PGConnection master_conn, String user, Collection exclude, Collection include) throws Exception
    {
        ResultSet rs1 = null;
        ResultSet rs2 = null;
        Statement stmt1 = null;
        Statement stmt2 = null;

        try
        {
            master_conn.begin();

            stmt1 = master_su_conn.createStatement();
            stmt2 = master_conn.createStatement();

            rs1 = stmt1.executeQuery("select relname from pg_class pgc, pg_shadow pgs where pgc.relkind = 'r' and pgc.relowner = pgs.usesysid and pgs.usename = '" + user + "'");

            stmt2.executeUpdate("set constraints all deferred");

            while (rs1.next())
            {
                String relname = DBUtils.getString(rs1,1);
                String short_relname = toShortTable(relname);

                if (relname.startsWith("_rserv") || relname.startsWith("pg_") || exclude.contains(relname) || (include.size() > 0 && !include.contains(relname))) continue;

                Log.debug("PROCESSING: relname="+relname+", short-relname="+short_relname);
                
                stmt2.executeUpdate("delete from " + relname);
            }

            master_conn.commit();
        }
        catch(Exception xcp)
        {
            Log.log(xcp);
            master_conn.rollback();
            throw xcp;
        }
        finally 
        {
            DBUtils.close(stmt1,rs1);
            DBUtils.close(stmt2,rs2);
        }
    }

    public static void destroy_master_systables(PGConnection conn) throws Exception
    {
        ResultSet rs = null;
        Statement stmt = null;

        try
        {
            stmt = conn.createStatement();

            rs = stmt.executeQuery("select relname from pg_class where relname = '_rserv_tables_'");
            if (!rs.next()) return;

            //select pgc.relname from pg_class pgc, pg_attribute pga where pgc.relkind = 'r' and  pgc.oid = pga.attrelid and pga.attname = '_rserv_ts'

            stmt.executeUpdate("drop table _rserv_servers_");
            stmt.executeUpdate("drop table _rserv_tables_");
            stmt.executeUpdate("drop table _rserv_log_1_");
            stmt.executeUpdate("drop table _rserv_log_2_");
            stmt.executeUpdate("drop table _rserv_sync_");
            stmt.executeUpdate("drop sequence _rserv_sync_seq_");
            stmt.executeUpdate("drop sequence _rserv_slave_seq_");
            stmt.executeUpdate("drop sequence _rserv_active_log_id_");
            stmt.executeUpdate("drop sequence _rserv_old_log_status_");
        }
        finally 
        {
            DBUtils.close(stmt,rs);
        }
    }

    public static void destroy_master_sysfunctions(PGConnection conn) throws Exception
    {
        ResultSet rs = null;
        Statement stmt = null;

        try
        {
            stmt = conn.createStatement();
            stmt.executeUpdate("set transaction isolation level serializable");

            rs = stmt.executeQuery("select proname from pg_proc where proname = '_rserv_log_'");
            if (!rs.next()) return;

            stmt.executeUpdate("drop function _rserv_log_()");
            stmt.executeUpdate("drop function _rserv_sync_(int4)");
            stmt.executeUpdate("drop function _rserv_debug_(int4)");
            stmt.executeUpdate("drop function _rserv_lock_altid_(int4)");
            stmt.executeUpdate("drop function _rserv_unlock_altid_(int4)");
            stmt.executeUpdate("drop function _pte_get_snapshot_()");
            stmt.executeUpdate("drop function _pte_set_snapshot_(text)");
        }
        finally 
        {
            DBUtils.close(stmt,rs);
        }
    }

    public static void init_slave_systables(PGConnection conn) throws Exception
    {
        ResultSet rs = null;
        Statement stmt = null;

        try
        {
            conn.begin();

            stmt = conn.createStatement();
            stmt.executeUpdate("set transaction isolation level serializable");

            rs = stmt.executeQuery("select relname from pg_class where relname = '_rserv_slave_tables_'");

            if (!rs.next())
            {
                stmt.executeUpdate("create table _rserv_slave_tables_ (tname name, cname name, reloid oid, key int4)");
                stmt.executeUpdate("create table _rserv_slave_sync_ (syncid int4, synctime timestamp)");
            }

            conn.commit();
        }
        catch(Exception xcp)
        {
            Log.log(xcp);
            conn.rollback();
            throw xcp;
        }
        finally 
        {
            DBUtils.close(stmt,rs);
        }
    }

    public static void link_slave_tables(PGConnection master_su_conn, String user, Collection exclude, Collection include, PGConnection slave_conn) throws Exception
    {
        ResultSet rs1 = null;
        ResultSet rs2 = null;
        Statement stmt1 = null;
        Statement stmt2 = null;

        try
        {
            slave_conn.begin();
            
            stmt1 = master_su_conn.createStatement();
            stmt2 = slave_conn.createStatement();
            
            rs1 = stmt1.executeQuery("select relname from pg_class pgc, pg_shadow pgs where pgc.relkind = 'r' and pgc.relowner = pgs.usesysid and pgs.usename = '" + user + "'");

            while (rs1.next())
            {
                String relname = DBUtils.getString(rs1,1);
                String short_relname = toShortTable(relname);

                if (relname.startsWith("_rserv") || relname.startsWith("pg_") || exclude.contains(relname) || (include.size() > 0 && !include.contains(relname))) continue;

                Log.debug("PROCESSING: relname="+relname+", short-relname="+short_relname);

                rs2 = stmt2.executeQuery("select pga.attname from pg_class pgc, pg_attribute pga where pgc.relname = '" + relname + "' and pgc.oid = pga.attrelid and pga.attname = '_rserv_ts'");

                boolean found = rs2.next();
                rs2.close(); rs2 = null;
                if (found) continue;

                Log.debug("ADDING-REPLICATION-HOOKS");

                stmt2.executeUpdate("lock " + relname + " in exclusive mode");
                stmt2.executeUpdate("create sequence _ers_" + short_relname + "_seq");
                stmt2.executeUpdate("alter table " + relname + " add column _rserv_ts integer");
                stmt2.executeUpdate("alter table " + relname + " alter column _rserv_ts set default nextval('_ers_" + short_relname + "_seq');");
                stmt2.executeUpdate("update " + relname + " set _rserv_ts = nextval('_ers_" + short_relname + "_seq') where _rserv_ts is null");
                stmt2.executeUpdate("alter table " + relname + " add constraint _rserv_ts_nn check (_rserv_ts is not null)");
                stmt2.executeUpdate("create unique index _ers_" + short_relname + "_idx on " + relname + "(_rserv_ts)");

                rs2 = stmt2.executeQuery("select pgc.oid, pga.attnum from pg_class pgc, pg_attribute pga where pgc.relname = '" + relname + "' and pgc.oid = pga.attrelid and pga.attname = '_rserv_ts'");

                if (!rs2.next()) throw new SQLException("ERR: _rserv_ts fields not found for slave table:" + relname);

                int oid = DBUtils.getInt(rs2,1);
                int attnum = DBUtils.getInt(rs2,2);
                stmt2.executeUpdate("insert into _rserv_slave_tables_ (tname, cname, reloid, key) values ('" + relname + "','_rserv_ts'," + oid + "," + attnum + ")"); 

                rs2.close(); rs2 = null;
            }

            slave_conn.commit();
        }
        catch(Exception xcp)
        {
            Log.log(xcp);
            slave_conn.rollback();
            throw xcp;
        }
        finally 
        {
            DBUtils.close(stmt1,rs1);
            DBUtils.close(stmt2,rs2);
        }
    }

    public static void flush_slave_tables(PGConnection master_su_conn, String user, Collection exclude, Collection include, PGConnection slave_conn) throws Exception
    {
        ResultSet rs1 = null;
        ResultSet rs2 = null;
        Statement stmt1 = null;
        Statement stmt2 = null;

        try
        {
            slave_conn.begin();
            
            stmt1 = master_su_conn.createStatement();
            stmt2 = slave_conn.createStatement();
            
            stmt2.executeUpdate("set constraints all deferred");

            rs1 = stmt1.executeQuery("select relname from pg_class pgc, pg_shadow pgs where pgc.relkind = 'r' and pgc.relowner = pgs.usesysid and pgs.usename = '" + user + "'");

            while (rs1.next())
            {
                String relname = DBUtils.getString(rs1,1);
                String short_relname = toShortTable(relname);

                if (relname.startsWith("_rserv") || relname.startsWith("pg_") || exclude.contains(relname) || (include.size() > 0 && !include.contains(relname))) continue;

                Log.debug("PROCESSING: relname="+relname+", short-relname="+short_relname);

                stmt2.executeUpdate("delete from " + relname);
            }

            slave_conn.commit();
        }
        catch(Exception xcp)
        {
            Log.log(xcp);
            slave_conn.rollback();
            throw xcp;
        }
        finally 
        {
            DBUtils.close(stmt1,rs1);
            DBUtils.close(stmt2,rs2);
        }
    }

    public static void destroy_slave_systables(PGConnection conn) throws Exception
    {
        ResultSet rs = null;
        Statement stmt = null;

        try
        {
            stmt = conn.createStatement();
            stmt.executeUpdate("set transaction isolation level serializable");

            rs = stmt.executeQuery("select relname from pg_class where relname = '_rserv_slave_tables_'");
            if (!rs.next()) return;

            stmt.executeUpdate("drop table _rserv_slave_tables_");
            stmt.executeUpdate("drop table _rserv_slave_sync_");
        }
        finally 
        {
            DBUtils.close(stmt,rs);
        }
    }

    private static String toShortTable(String table)
    {
        int index1 = 0;
        int index2 = 0;

        StringBuffer buf = new StringBuffer();

        while ((index2 = table.indexOf('_',index1)) >= 0)
        {
            buf.append(table.substring(index1,Math.min(index2,index1+4)));
            buf.append('_');
            index1 = index2+1;
        }

        index2 = table.length();
        buf.append(table.substring(index1,Math.min(index2,index1+4)));

        return buf.toString();
    }

    public static void main(String[] args)
    {
        PGConnectionPool master_dbpool = null;
        PGConnectionPool master_su_dbpool = null;
        PGConnectionPool slave_dbpool = null;

        PGConnection master_conn = null;
        PGConnection master_su_conn = null;
        PGConnection slave_conn = null;

        try
        {
            if (args.length != 3)
            {
                System.err.println("ReplicationSetup <instance> <master|slave#> <init|flush>");
                System.exit(1);
            }

            String instance = args[0].toLowerCase().trim();
            String target = args[1].toLowerCase().trim();
            String command = args[2].toLowerCase().trim();

            SysProperties.instance__ = instance;

            int tgt = -1;
            if ("master".equals(target)) tgt = TGT_MASTER;
            else if (target.startsWith("slave")) tgt = TGT_SLAVE;

            int cmd = -1;
            if ("init".equals(command)) cmd = CMD_INIT;
            else if ("destroy".equals(command)) cmd = CMD_DESTROY;
            else if ("flush".equals(command)) cmd = CMD_FLUSH;

            int slave = -1;

            if (tgt == TGT_SLAVE) try { slave = Integer.parseInt(target.substring(5)); } catch(Exception xcp) {}
            
            if (cmd == -1 || tgt == -1 || (tgt == TGT_SLAVE && (slave < 1 || slave > 99)))
            {
                System.err.println("ReplicationSetup <instance> <master|slave#> <init|flush>");
                System.exit(1);
            }

            Collection exclude = SysProperties.getListProperty("pgsql.ers.master.tables.exclude");
            Collection include = SysProperties.getListProperty("pgsql.ers.master.tables.include");

            System.err.println("ReplicationSetup\n\tinstance: " + instance + "\n\ttarget: " + target + "\n\tcommand: " + command + "\n\tmaster-tables-exclude: " + exclude + "\n\tmaster-tables-include: " + include + (tgt == TGT_SLAVE ? ("\n\tslave: " + slave) : ""));

            master_su_dbpool = new PGConnectionPool("pgsql.ers.master.dbpool",true);
            master_dbpool = new PGConnectionPool("pgsql.ers.master.dbpool");

            switch(tgt)
            {
            case TGT_MASTER:
                master_su_conn = master_su_dbpool.getConnection(false);
                master_conn = master_dbpool.getConnection(false);
                
                switch(cmd)
                {
                case CMD_INIT:
                    init_master_sysfunctions(master_su_conn,SysProperties.getProperty("pgsql.ers.libdir"));
                    init_master_systables(master_conn);
                    link_master_tables(master_su_conn,master_conn,master_dbpool.getUser(),exclude,include);
                    break;
                
                case CMD_FLUSH:
                    flush_master_tables(master_su_conn,master_conn,master_dbpool.getUser(),exclude,include);
                    break;

                case CMD_DESTROY:
                    //unlink_master_tables(master_conn);
                    //destroy_master_sysfunctions(master_su_conn);
                    //destroy_master_systables(master_conn);
                    break;
                }
                break;
            }
            
            switch(tgt)
            {
            case TGT_SLAVE:
                master_su_conn = master_su_dbpool.getConnection(false);

                slave_dbpool = new PGConnectionPool("pgsql.ers." + target + ".dbpool");
                slave_conn = slave_dbpool.getConnection(false);

                switch(cmd)
                {
                case CMD_INIT:
                    init_slave_systables(slave_conn);
                    link_slave_tables(master_su_conn,master_dbpool.getUser(),exclude,include,slave_conn);
                    break;

                case CMD_FLUSH:
                    flush_slave_tables(master_su_conn,master_dbpool.getUser(),exclude,include,slave_conn);
                    break;

                case CMD_DESTROY:
                    //unlink_slave_tables(slave_conn[j]);
                    //destroy_slave_systables(slave_conn[j]);
                    break;
                }
            }

            System.err.println("Done.");
        }
        catch(Throwable xcp)
        {
            Log.log(xcp);
            System.err.println("Failed.");
        }
        finally
        {
            if (master_dbpool != null) master_dbpool.destroy();
            if (master_su_dbpool != null) master_su_dbpool.destroy();
            if (slave_dbpool != null) slave_dbpool.destroy();
        }
    }
}
