/*
 *   Copyright 2013 Brian Rosenberger (Brutex Network)
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

package net.brutex.xservices.ws.impl;

import static org.quartz.TriggerBuilder.newTrigger;

import java.io.StringWriter;
import java.math.BigInteger;
import java.sql.*;
import java.time.Instant;
import java.util.*;
import java.util.Date;
import java.util.concurrent.atomic.AtomicLong;
import jakarta.annotation.Resource;
import jakarta.jws.WebService;
import jakarta.servlet.ServletContext;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBElement;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.ws.WebServiceContext;
import jakarta.xml.ws.handler.MessageContext;
import lombok.extern.slf4j.Slf4j;
import net.brutex.xservices.types.*;
import net.brutex.xservices.types.alfevent.ALFEventResponseType;
import net.brutex.xservices.types.alfevent.ALFEventType;
import net.brutex.xservices.types.alfevent.ObjectFactory;
import net.brutex.xservices.types.ant.FileSetResource;
import net.brutex.xservices.util.ALFEventFilter;
import net.brutex.xservices.util.EventEmitter;
import net.brutex.xservices.util.EventmanagerConfiguration;
import net.brutex.xservices.util.RunTask;
import net.brutex.xservices.ws.MiscService;
import net.brutex.xservices.ws.XServicesFault;
import org.apache.tools.ant.taskdefs.HostInfo;
import org.apache.tools.ant.taskdefs.Sleep;
import org.apache.tools.ant.taskdefs.email.EmailTask;
import org.h2.jdbcx.JdbcConnectionPool;
import org.quartz.*;

/**
 * Implements the web service
 *
 * @author Brian Rosenberger, bru@brutex.de
 */
@Slf4j
@WebService(targetNamespace = "http://ws.xservices.brutex.net", endpointInterface = "net.brutex.xservices.ws.MiscService", serviceName = "MiscService")
public class MiscServiceImpl
        implements MiscService {

    final Object serializedDbAccess = new Object();
    @Resource
    private WebServiceContext context;

    public MiscServiceImpl() throws SchedulerException {
    }

    public HostinfoType getHostinfo(String hostname) {
        HostInfo info = new HostInfo();
        info.setTaskName("HostInfo");
        RunTask runner = new RunTask(info);
        info.setHost(hostname);

        ReturnCode ret = runner.postTask();
        HostinfoType infotype = new HostinfoType(
                ret.getProperty("NAME"),
                ret.getProperty("DOMAIN"),
                ret.getProperty("ADDR4"),
                ret.getProperty("ADDR6"));
        return infotype;
    }

    public ReturnCode getInfo() {
        ReturnCode r = new ReturnCode();
        r.returnCode = 0;

        Properties props = System.getProperties();

        Enumeration e = props.propertyNames();
        while (e.hasMoreElements()) {
            String propName = (String) e.nextElement();

            String propValue = (String) props.get(propName);
            r.stdOut = (r.stdOut + propName + ": " + propValue + "\n");
        }

        return r;
    }

    public ReturnCode sendMailSimple(HostConnection mailhost, String from, String tolist, String subject, String message) {
        return sendMail(from, from, tolist, "", "", subject, message,
                "text/plain", null, mailhost.hostname, mailhost.port,
                mailhost.user, mailhost.password, "utf-8", false, false);
    }

    public ReturnCode sendMailSimpleWithAttachment(HostConnection mailhost, String from, String tolist, String subject, String message, FileSetResource res) {
        return sendMail(from, from, tolist, "", "", subject, message,
                "text/plain", res, mailhost.hostname, mailhost.port,
                mailhost.user, mailhost.password, "utf-8", false, false);
    }

    public ReturnCode sendMail(HostConnection mailhost, String from, String tolist, String cclist, String bcclist, String subject, MailMimeType mimetype, String charset, String message, FileSetResource res, boolean ssl, boolean tls) {
        return sendMail(from, from, tolist, cclist, bcclist, subject, message,
                mimetype.value(), res, mailhost.hostname, mailhost.port,
                mailhost.user, mailhost.password, charset, tls, ssl);
    }

    public ReturnCode sleep(int minutes, int seconds) {
        return sleep(0, minutes, seconds, 0);
    }

    public String generateUUID() {
        return UUID.randomUUID().toString();
    }

    private ReturnCode sendMail(String from, String replyto, String tolist, String cclist, String bcclist, String subject, String message, String messagemimetype, FileSetResource attachments, String mailhost, int mailport, String user, String password, String charset, boolean tls, boolean ssl) {
        EmailTask mail = new EmailTask();
        mail.setTaskName("Mail");
        RunTask runner = new RunTask(mail);
        mail.setFrom(from);
        mail.setReplyTo(replyto);
        mail.setToList(tolist);
        mail.setCcList(cclist);
        mail.setBccList(bcclist);
        mail.setSubject(subject);
        mail.setMessage(message);
        mail.setMessageMimeType(messagemimetype);
        if (attachments != null) {
            mail.addFileset(attachments.getAntResource(mail.getProject()));
        }
        mail.setMailhost(mailhost);
        mail.setMailport(mailport);
        mail.setUser(user);
        mail.setPassword(password);
        mail.setCharset(charset);
        mail.setSSL(ssl);
        mail.setEnableStartTLS(tls);
        return runner.postTask();
    }

    private ReturnCode sleep(int hours, int minutes, int seconds, int milliseconds) {
        Sleep sleep = new Sleep();
        sleep.setTaskName("Sleep");
        RunTask runner = new RunTask(sleep);
        sleep.setHours(hours);
        sleep.setMinutes(minutes);
        sleep.setSeconds(seconds);
        sleep.setMilliseconds(milliseconds);
        return runner.postTask();
    }

    public RuntimeInfoType getMemory() {
        return new RuntimeInfoType();
    }


    @Override
    public Boolean unlock(String id, String objectId) throws XServicesFault {
        MessageContext cont = context.getMessageContext();
        final ServletContext servletContext =
                (ServletContext) cont.get(MessageContext.SERVLET_CONTEXT);
        final JdbcConnectionPool pool = (JdbcConnectionPool) servletContext.getAttribute("mdbConnection");
        try(Connection con = pool.getConnection()) {
            PreparedStatement prep = con.prepareStatement("DELETE FROM brutex.tbl_lock where btx_id = ? AND btx_obj_id = ?");
            prep.setString(1, id);
            prep.setString(2, objectId);
            prep.execute();
            if(prep.getUpdateCount()>0) return Boolean.TRUE;
        } catch (SQLException e) {
            throw new XServicesFault(e);
        }
        return Boolean.FALSE;
    }
    final Object lockObj = new Object();
    @Override
    public BigInteger lock(String id, String objectId, BigInteger validity) throws XServicesFault {

        MessageContext cont = context.getMessageContext();
        final ServletContext servletContext =
                (ServletContext) cont.get(MessageContext.SERVLET_CONTEXT);

        if(validity == null || validity.longValue()<0) validity = BigInteger.valueOf(60000*60*12);

        final JdbcConnectionPool pool = (JdbcConnectionPool) servletContext.getAttribute("mdbConnection");
        final long ts = Instant.now().toEpochMilli();
        final long validUntil = ts + validity.longValue();

        long rows = 0L;

    try (Connection con = pool.getConnection()) {
        PreparedStatement prep =
                con.prepareStatement(
                        "SELECT btx_id from brutex.tbl_lock "
                                + " where btx_obj_id = ? "
                                + " AND btx_validity >= ? ORDER BY btx_seq asc");
        PreparedStatement prep2 = con.prepareStatement(
                        "INSERT INTO brutex.tbl_lock values (NEXT VALUE FOR brutex.btx_sequence1, ?, ?, ?, ?)");
        prep.setString(1, objectId);
        prep.setLong(2, ts);

        prep2.setString(1, id);
        prep2.setString(2, objectId);
        prep2.setLong(3, ts);
        prep2.setLong(4, validUntil);

        ResultSet rs;
        synchronized (lockObj) {
            rs = prep.executeQuery();
            if (rs.next()) {
              /* lock already set and first in queue */
              String lock_id = rs.getString(1);
              if (lock_id.equals(id)) {
                /* return directly from here */
                return BigInteger.ZERO;
              }
              rows = 1;
              while (rs.next()) {
                /* count but not self. Using DB COUNT(*) might be faster */
                if (!rs.getString(1).equals(id)) rows++;
              }
            }
            /* lock not found, need to insert */
            prep2.execute();
        }

    } catch (SQLException e) {
        log.error("Error during 'lock' operation. SQL error: {}", e.getMessage());
            throw new XServicesFault(e);
    }
        return BigInteger.valueOf(rows);
    }

    @Override
    public ALFEventResponseType mergeALFEvent(ALFEventType event) throws XServicesFault {
        final Instant d = Instant.now();
        final long ts = d.toEpochMilli();

        //Get Parameters from the Servlet Context
        MessageContext cont = context.getMessageContext();
        final ServletContext servletContext =
                (ServletContext) cont.get(MessageContext.SERVLET_CONTEXT);
        final EventmanagerConfiguration conf = (EventmanagerConfiguration) servletContext
                .getAttribute(EventmanagerConfiguration.KEY);


        final JdbcConnectionPool pool = (JdbcConnectionPool) servletContext.getAttribute("mdbConnection");
        final JdbcConnectionPool fpool = (JdbcConnectionPool) servletContext.getAttribute("fdbConnection");
        final AtomicLong egres_counter = (AtomicLong) servletContext.getAttribute("egres_counter");
        final AtomicLong ingres_counter = (AtomicLong) servletContext.getAttribute("ingres_counter");
        final Scheduler scheduler = (Scheduler) servletContext.getAttribute("scheduler");

        log.trace("Read dbConnection from servlet context: {}", pool);

        //System.out.println("Step 2: " + ChronoUnit.MILLIS.between(Instant.now(), d));
        final ObjectFactory of = new ObjectFactory();

        final String objectId = event.getBase().getObjectId();
        final String eventId = event.getBase().getEventId();
        final String objectType = event.getBase().getObjectType();
        final String eventType = event.getBase().getEventType();
        log.debug("Event id '{}', type '{}' received for object '{}' with object_id '{}'.",
                eventId, eventType, objectType, objectId);

        final String mergeStatememt = "SELECT btx_id FROM OLD TABLE (MERGE INTO brutex.tbl_events " +
                "KEY (btx_event_type, btx_obj_type, btx_obj_id) " +
                "VALUES (?,?,?,?,?,?))";

        final String insertAll = "INSERT INTO brutex.tbl_events_all VALUES (?,?,?,?,?,?,?)";

    try (Connection con = pool.getConnection()) {

            List<ALFEventFilter> filter = conf.getAlf_filter();
            for(ALFEventFilter f : filter) {
                event = f.filter(event);
            }

            Marshaller m = JAXBContext.newInstance(ALFEventType.class).createMarshaller();
            //m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
            m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
            m.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
            JAXBElement<ALFEventType> e = of.createALFEventNoticeDoc(event);
            StringWriter sw = new StringWriter();
            m.marshal(e, sw);
            StringBuilder sb = new StringBuilder();

            sb.append(" <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" " +
                    "xmlns:ns=\"http://www.eclipse.org/alf/schema/EventBase/1\">\n");
            sb.append("<soapenv:Body>\n");

            sb.append(sw);

            sb.append("</soapenv:Body>");
            sb.append("</soapenv:Envelope>");

            con.setAutoCommit(false);

            PreparedStatement prep = con.prepareStatement(mergeStatememt);
            prep.setString(1, eventType);
            prep.setString(2, eventId);
            prep.setString(3, objectType);
            prep.setString(4, objectId);
            prep.setLong(5, ts);
            //prep.setClob(6, new StringReader(sb.toString()));
            ResultSet r = null;
            prep.setString(6, sb.toString());

            /* We need to synchronize MERGE INTO statements, as it does not seem
            like H2 is able to execute them properly in a heavy multi-threaded
            environment resulting those statements to insert with the same key
            twice (and then throw KEY violation errors. */

            synchronized (serializedDbAccess) {
                r = prep.executeQuery();
                con.commit();
            }

            String supersed_id = null;
            if (r.next()) {
                supersed_id = r.getString(1);
                log.trace("Event '{}' superseded event '{}'", eventId, supersed_id);
            } else {
                log.trace("Event '{}' inserted into outgoing queue.", eventId);
            }


            /* Write all into permanent storage */
            //Connection fcon = fpool.getConnection();
            prep = con.prepareStatement(insertAll);
            prep.setString(1, eventType);
            prep.setString(2, eventId);
            prep.setString(3, objectType);
            prep.setString(4, objectId);
            prep.setLong(5, ts);
            prep.setString(6, supersed_id);
            prep.setString(7,  sb.toString());
            prep.execute();

            con.commit();
            //con.close();

            ingres_counter.incrementAndGet();

            if (conf.isEmitterActive()) {
                synchronized (scheduler) {
                    if (!scheduler.checkExists(JobKey.jobKey("ALFEmitter"))) {
                        JobDetail job2 = JobBuilder.newJob(EventEmitter.class)
                                .withIdentity("ALFEmitter")
                                .build();
                        job2.getJobDataMap().put("mdbConnection", pool);
                        job2.getJobDataMap().put("fdbConnection", fpool);
                        job2.getJobDataMap().put("run_key", ts);
                        job2.getJobDataMap().put("egres_counter", egres_counter);
                        job2.getJobDataMap().put("ingres_counter", ingres_counter);

                        job2.getJobDataMap().put(EventmanagerConfiguration.KEY, conf);

                        SimpleTrigger t = (SimpleTrigger) newTrigger()
                                .withIdentity("ALFEmitter").startAt(Date.from(d.plusSeconds(conf.getInterval())))
                                .build();

                        scheduler.scheduleJob(job2, t);
                    }
                }
            }
        } catch (JAXBException | SQLException | SchedulerException e) {
            log.error(e.getMessage());
            throw new XServicesFault(e);
        }
        return of.createALFEventResponseType();
    }
}