/*
 * Copyright (C) 2004-2011 Centro de Computacao Cientifica e Software Livre
 * Departamento de Informatica - Universidade Federal do Parana - C3SL/UFPR
 *
 * This file is part of datasid
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 * USA.
 */
package br.ufpr.c3sl.datasid;

import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.math.BigInteger;

import java.sql.Timestamp;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Types;

import javax.naming.InitialContext;
import javax.sql.DataSource;
import javax.xml.bind.*;
import javax.xml.bind.util.ValidationEventCollector;
import javax.xml.bind.helpers.DefaultValidationEventHandler;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.transform.stream.StreamSource;

public class DataSID {
    // enum does not work as expected inside an axis web service
    // using simple constants instead
    private static final int LINUX = 0;
    private static final int WINDOWS = 1;

    private static final int ERROR = 0;
    private static final int WARNING = 1;
    private static final int INFO = 2;
    private static final int DEBUG = 3;

    private Properties prop;

    private File xml_inventory_schema;
    private File xml_net_usage_schema;

    private String linux_agent_version;
    private String linux_agent_update_link;

    private String windows_agent_version;
    private String windows_agent_update_link;

    public DataSID() throws IOException {
        try {
            this.prop = new Properties();
            this.prop.load(new FileInputStream("../conf/webservice.properties"));
        }
        catch (IOException e) {
            System.err.println("Failed to load webservice.properties.");
            throw e;
        }

        this.xml_inventory_schema = new File(this.prop.getProperty("xml_inventory_schema",
            "../conf/collected-data.xsd"));
        this.xml_net_usage_schema = new File(this.prop.getProperty("xml_net_usage_schema",
            "../conf/net-collected-data.xsd"));

        this.linux_agent_version = this.prop.getProperty("linux_agent_version", "1.0.0");
        this.linux_agent_update_link = this.prop.getProperty("linux_agent_update_link",
                    "http://bisimmcdev.c3sl.ufpr.br/download/datasid-1.0.0-update.run");

        this.windows_agent_version = this.prop.getProperty("windows_agent_version", "1.0.0");
        this.windows_agent_update_link = this.prop.getProperty("windows_agent_update_link",
                    "http://bisimmcdev.c3sl.ufpr.br/download/datasid-1.0.0-update.exe");
    }


    /**
     * Returns the name of the log level
     *
     * @author                  Eduardo Luis Buratti
     * @param       level       Log level
     */
    private static String logLevelName(int level) {
        if (level == ERROR) return "ERROR";
        else if (level == WARNING) return "WARNING";
        else if (level == INFO) return "INFO";
        else if (level == DEBUG) return "DEBUG";
        else return "NULL";
    }

    /**
     * Write messages to the log
     *
     * @author                  Eduardo Luis Buratti
     * @param       level       Log level
     * @param       msg         Message
     */
    private static void log(int level, String msg) {
        System.out.println(getTimestamp() + " [DataSidWS][" + logLevelName(level) + "]: " + msg);
    }

    /**
     * Returns a string representing the current date
     *
     * @author                  Eduardo Luis Buratti
     * @return                  String
     */
    private static String getDate() {
        Calendar date = Calendar.getInstance(); // gets current date

        int year = date.get(Calendar.YEAR);
        int month = date.get(Calendar.MONTH);
        int day = date.get(Calendar.DATE);

        /* NOTE: Calendar.MONTH field value is 0-based. e.g: 0 is January */
        month++;

        return year + "-" + month + "-" + day;
    }

    /**
     * Returns current timestamp
     *
     * @author                  Eduardo Luis Buratti
     * @return                  String
     */
    private static String getTimestamp() {
        // gets the current time
        java.util.Date date = new java.util.Date();
        long milisecs = date.getTime();

        // generates timestamp string
        Timestamp timestamp = new Timestamp(milisecs);
        return timestamp.toString();
    }

    /**
     * Return a string with current agent version.
     *
     * @author                  Eduardo Luis Buratti
     * @param       OS          Integer of OS Type
     * @return                  String
     */
    public String getAgentVersion(int OS) {
        switch(OS){
            case LINUX:
                return this.linux_agent_version;
            case WINDOWS:
                return this.windows_agent_version;
            default:
                return "ERROR: invalid OS";
        }

    }

    /**
     * Return a string that contains a link to download the newest version of
     * agent.
     *
     * @author                  Eduardo Luis Buratti
     * @param       OS          Integer of OS Type
     * @return                  String
     */
    public String getUpdateLink(int OS)
    {
        switch(OS){
            case LINUX:
                return this.linux_agent_update_link;
            case WINDOWS:
                return this.windows_agent_update_link;
            default:
                return "ERROR: invalid OS";
        }
    }

    /**
     * Receive an XML string which has the inventory data to be parsed and
     * inserted into database. Return "Success" string if insertion operation
     * is successful. Any other errors will throw exceptions.
     *
     * @author                 Eduardo Luis Buratti
     * @param     xmlData      XML string of inventory
     * @return                 String
     */
    public String setInventory(String xmlData) {
        try {
            InitialContext cxt = new InitialContext();
            DataSource ds = (DataSource) cxt.lookup("java:/comp/env/jdbc/simmc");
            if (ds == null)
                throw new Exception("Data source not found!");

            Connection con = ds.getConnection();
            if (con == null)
                throw new Exception("Failed to get a database connection!");

            SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
            Schema schema = factory.newSchema(this.xml_inventory_schema);

            JAXBContext context = JAXBContext.newInstance(CollectedData.class);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            unmarshaller.setSchema(schema);

            // Strip spaces in the beginning of the xml
            xmlData = xmlData.replaceAll("^\\s+", "");

            // transform the xml string into a InputStream
            InputStream is = new ByteArrayInputStream(xmlData.getBytes());

            // Decode the XML into a Java Object
            JAXBElement<CollectedData> element = (JAXBElement<CollectedData>) unmarshaller.unmarshal(new StreamSource(is), CollectedData.class);
            CollectedData collected = element.getValue();
            // contact_date = current date
            Calendar cal = Calendar.getInstance();
            java.sql.Date contactDate = new java.sql.Date(cal.getTimeInMillis());

            PreparedStatement st = createInventoryStatement(con, collected, contactDate);
            st.executeUpdate();

            List<User> users = collected.getUserHistory().getUser();
            int idpoint = collected.getPointInfo().getIdpoint().intValue();
            List<Interface> interfaces = collected.getInterfaces().getInterface();
            org.postgresql.util.PGobject macaddr = new org.postgresql.util.PGobject();
            macaddr.setType("macaddr");
            macaddr.setValue(interfaces.get(0).getMacAddress());

            for(User user: users) {
                st = createUserHistoryStatement(con, contactDate, idpoint,
                    macaddr, user.getName(), user.getLogin(), user.getLogout());
                st.executeUpdate();
            }
            con.close();

            log(INFO, "setInventory(idpoint=" + collected.getPointInfo().getIdpoint() +
                        ", macaddr=" + interfaces.get(0).getMacAddress() + ")");

            return "Success";
        } catch (Exception e) {
            log(ERROR, e.getMessage() + " " + xmlData);
            e.printStackTrace();
            return "ERROR: " + e.getMessage();
        }
    }

    private PreparedStatement createInventoryStatement(Connection con, CollectedData collectedData, java.sql.Date contactDate) throws SQLException, ParseException {
        final String query = "INSERT INTO telecenter_inventory " +
            "(contact_date, machine_type, id_point, macaddr, agent_version, " +
            " os_type, os_distro, os_kernel, " +
            " processor, memory, " +
            " disk1_model, disk1_size, disk1_used, " +
            " disk2_model, disk2_size, disk2_used, " +
            " extra_hds, mirror_timestamp, amount_users " +
            ") VALUES " +
            "(?, ?, ?, ?, ?, " +
            " ?, ?, ?, " +
            " ?, ?, " +
            " ?, ?, ?, " +
            " ?, ?, ?, " +
            " ?, ?, ? " +
            " );";

        PreparedStatement st = con.prepareStatement(query);

        List<Interface> interfaces = collectedData.getInterfaces().getInterface();

        Inventory inventory = collectedData.getInventory();
        List<Disk> disks = inventory.getDisks().getDisk();
        PointInfo pointInfo = collectedData.getPointInfo();

        // contact_date
        st.setDate(1, contactDate);

        // machine_type
        if(collectedData.getMachineType().compareTo("client") == 0)
            st.setInt(2, 0);
        else
            st.setInt(2, 1);

        // idpoint
        st.setInt(3, pointInfo.getIdpoint().intValue());

        // macaddr
        org.postgresql.util.PGobject macaddr = new org.postgresql.util.PGobject();
        macaddr.setType("macaddr");
        macaddr.setValue(interfaces.get(0).getMacAddress());
        st.setObject(4, macaddr);

        // versao
        st.setString(5, collectedData.getAgentVersion());

        // os_type
        st.setString(6, inventory.getOs());

        // os_distro
        st.setString(7, inventory.getDistro());

        // os_kernel
        st.setString(8, inventory.getKernel());

        // processor
        st.setString(9, inventory.getProcessor());

        // memory
        st.setInt(10, inventory.getMemory().intValue());

        // disk1_model
        st.setString(11, disks.get(0).getModel());

        // disk1_size
        st.setInt(12, disks.get(0).getSize().intValue());

        // disk1_used
        st.setInt(13, disks.get(0).getUsed().intValue());

        if (disks.size() > 1) {
            // disk2_model
            st.setString(14, disks.get(1).getModel());

            // disk2_size
            st.setInt(15, disks.get(1).getSize().intValue());

            // disk2_used
            st.setInt(16, disks.get(1).getUsed().intValue());
        }
        else {
            // disk2_model
            st.setNull(14, Types.VARCHAR);

            // disk2_size
            st.setNull(15, Types.INTEGER);

            // disk2_used
            st.setNull(16, Types.INTEGER);
        }

        // extra_hds
        st.setInt(17, (disks.size() > 2) ? (disks.size() - 2) : 0);


        // mirrors_timestamp
        if(collectedData.getMirrorsTimestamp().compareTo(" ") == 0)
            st.setTimestamp(18, null);
        else {
            SimpleDateFormat dt = new SimpleDateFormat("EEE MMM dd hh:mm:ss zzz yyyy", Locale.US);
            Date ts = dt.parse(collectedData.getMirrorsTimestamp());
            st.setTimestamp(18, new java.sql.Timestamp(ts.getTime()));
        }

        // user_count
        st.setInt(19, pointInfo.getUserCount().intValue());

        return st;
    }

    private PreparedStatement createUserHistoryStatement(Connection con, java.sql.Date contactDate,
            int id_point, Object macaddr, String name, String login, String logout) throws SQLException {
        final String query = "INSERT INTO telecenter_user " +
            "(contact_date, id_point, macaddr, name, login, logout) VALUES " +
            "(?, ?, ?, ?, ?, ?);";

        PreparedStatement st = con.prepareStatement(query);

        st.setDate(1, contactDate);

        st.setInt(2, id_point);

        st.setObject(3, macaddr);

        st.setString(4, name);

        try {
            st.setTime(5, java.sql.Time.valueOf(login));
        } catch (Exception e) {
            st.setTime(5, null);
        }

        try {
            st.setTime(6, java.sql.Time.valueOf(logout));
        } catch (Exception e) {
            st.setTime(6, null);
        }

        return st;
    }

    /**
     * Receive an XML string which has the inventory data to be parsed and
     * inserted into database. Return "Success" string if insertion operation
     * is successful. Any other errors will throw exceptions.
     *
     * @author                 Eduardo Luis Buratti
     * @param     xmlData      XML string of inventory
     * @return                 String
     */
    public String setNetUsage(String xmlData)
    {
        try {
            InitialContext cxt = new InitialContext();
            DataSource ds = (DataSource) cxt.lookup("java:/comp/env/jdbc/simmc");
            if (ds == null)
                throw new Exception("Data source not found!");

            Connection con = ds.getConnection();
            if (con == null)
                throw new Exception("Failed to get a database connection!");

            SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
            Schema schema = factory.newSchema(this.xml_net_usage_schema);

            JAXBContext context = JAXBContext.newInstance(NetCollectedData.class);
            Unmarshaller unmarshaller = context.createUnmarshaller();
            unmarshaller.setSchema(schema);

            // Strip spaces in the beginning of the xml
            xmlData = xmlData.replaceAll("^\\s+", "");

            // transform the xml string into a InputStream
            InputStream is = new ByteArrayInputStream(xmlData.getBytes());

            // Decode the XML into a Java Object
            JAXBElement<NetCollectedData> element = (JAXBElement<NetCollectedData>) unmarshaller.unmarshal(new StreamSource(is), NetCollectedData.class);
            NetCollectedData netCollectedData = element.getValue();

            List<Interface> interfaces = netCollectedData.getInterfaces().getInterface();

            // contact_date = current date
            Calendar cal = Calendar.getInstance();
            java.sql.Date contactDate = new java.sql.Date(cal.getTimeInMillis());
            // macaddr
            org.postgresql.util.PGobject macaddr = new org.postgresql.util.PGobject();
            macaddr.setType("macaddr");
            macaddr.setValue(interfaces.get(0).getMacAddress());

            List<NetUse> netUses = netCollectedData.getBandwidthUsage().getNetuse();

            for(NetUse netUse : netUses) {
                PreparedStatement st = createNetUsageStatement(con, contactDate,
                 netCollectedData.getIdPoint(), (Object)macaddr,
                 netUse.getDate(), netUse.getTime(), netUse.getRx().getRxBytes(),
                 netUse.getRx().getRxPackets(), netUse.getTx().getTxBytes(),
                 netUse.getTx().getTxPackets());
                st.executeUpdate();
            }
            con.close();

            log(DEBUG, "setNetUsage(idpoint=" + netCollectedData.getIdPoint() +
                        ", macaddr=" + interfaces.get(0).getMacAddress() + ")");
            return "Success";
        } catch (Exception e) {
            log(ERROR, e.getMessage() + " " + xmlData);
            e.printStackTrace();
            return "ERROR: " + e.getMessage();
        }
    }

    private PreparedStatement createNetUsageStatement(Connection con, java.sql.Date contactDate,
            BigInteger idpoint, Object macaddr, String collect_date, String collect_time, long down_bytes,
            BigInteger down_packages, long up_bytes, BigInteger up_packages) throws SQLException {
        final String query = "INSERT INTO net_usage " +
            "(contact_date, id_point, macaddr, collect_date, collect_time, " +
            "down_bytes, down_packages, up_bytes, up_packages, ip, city_code) VALUES " +
            "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);";

        PreparedStatement st = con.prepareStatement(query);

        st.setDate(1, contactDate);

        st.setInt(2, idpoint.intValue());

        st.setObject(3, macaddr);

        st.setDate(4, java.sql.Date.valueOf(collect_date));

        st.setTime(5, java.sql.Time.valueOf(collect_time));

        st.setLong(6, down_bytes);

        st.setInt(7, down_packages.intValue());

        st.setLong(8, up_bytes);

        st.setInt(9, up_packages.intValue());

        st.setObject(10, null);

        st.setString(11, null);

        return st;
    }
}