package org.ff4j.audit.repository;

/*-
 * #%L
 * ff4j-core
 * %%
 * Copyright (C) 2013 - 2023 FF4J
 * %%
 * 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.
 * #L%
 */


import static org.ff4j.audit.EventConstants.ACTION_CHECK_OK;
import static org.ff4j.store.JdbcStoreConstants.*;

import static org.ff4j.utils.JdbcUtils.closeConnection;
import static org.ff4j.utils.JdbcUtils.closeResultSet;
import static org.ff4j.utils.JdbcUtils.closeStatement;
import static org.ff4j.utils.JdbcUtils.executeUpdate;
import static org.ff4j.utils.JdbcUtils.isTableExist;
import static org.ff4j.utils.JdbcUtils.rollback;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.sql.DataSource;

import org.ff4j.audit.Event;
import org.ff4j.audit.EventQueryDefinition;
import org.ff4j.audit.EventSeries;
import org.ff4j.audit.MutableHitCount;
import org.ff4j.audit.chart.TimeSeriesChart;
import org.ff4j.exception.AuditAccessException;
import org.ff4j.exception.FeatureAccessException;
import org.ff4j.store.JdbcEventMapper;
import org.ff4j.store.JdbcQueryBuilder;
import org.ff4j.utils.MappingUtil;
import org.ff4j.utils.Util;

/**
 * Implementation of in memory {@link EventRepository} with limited events.
 * 
 * @author Cedrick Lunven (@clunven)
 */
public class JdbcEventRepository extends AbstractEventRepository {
    
    /** Error message 1. */
    public static final String CANNOT_READ_AUDITTABLE =  "Cannot read audit table from DB";

	/** error message. */
    public static final String CANNOT_BUILD_PIE_CHART_FROM_REPOSITORY = "Cannot build PieChart from repository, ";
    
    /** Access to storage. */
    private DataSource dataSource;
    
    /** Query builder. */
    private JdbcQueryBuilder queryBuilder;
    
    /** Mapper to read from SQL result. */
    private static final JdbcEventMapper EVENT_MAPPER = new JdbcEventMapper();
    
    /**
     * Default Constructor
     */
    public JdbcEventRepository() {}
    
    /**
     * Constructor from DataSource.
     * 
     * @param jdbcDS
     *            native jdbc datasource
     */
    public JdbcEventRepository(DataSource jdbcDS) {
        this.dataSource = jdbcDS;
    }
    
    /** {@inheritDoc} */
    @Override
    public void createSchema() {
        DataSource       ds = getDataSource();
        JdbcQueryBuilder qb = getQueryBuilder();
        if (!isTableExist(ds, qb.getTableNameAudit())) {
            executeUpdate(ds, qb.sqlCreateTableAudit());
        }
    }
    
    /** {@inheritDoc} */
    // FIXME Stop dynamic query !
    @Override
    public boolean saveEvent(Event evt) {
        Util.assertEvent(evt);
        
        Connection        sqlConn = null;
        PreparedStatement stmt = null;
        try {
            // Get collection from Pool
            sqlConn = dataSource.getConnection();
           
            // Open TX Bloc
            sqlConn.setAutoCommit(false);
            int idx = 9;
            Map < Integer, String > statementParams = new HashMap<Integer, String>();

            StringBuilder sb = new StringBuilder("INSERT INTO "
                    + getQueryBuilder().getSchemaPattern() + getQueryBuilder().getTableNameAudit()
            		+ "(EVT_UUID,EVT_TIME,EVT_TYPE,EVT_NAME,EVT_ACTION,EVT_HOSTNAME,EVT_SOURCE,EVT_DURATION");
            if (Util.hasLength(evt.getUser())) {
                sb.append(", EVT_USER");
                statementParams.put(idx, evt.getUser());
                idx++;
            }
            if (Util.hasLength(evt.getValue())) {
                sb.append(", EVT_VALUE");
                statementParams.put(idx, evt.getValue());
                idx++;
            }
            if (!evt.getCustomKeys().isEmpty()) {
                sb.append(", EVT_KEYS");
                statementParams.put(idx, MappingUtil.fromMap(evt.getCustomKeys()));
                idx++;
            }
            
            sb.append(") VALUES (?");
            for(int offset = 1; offset < idx-1;offset++) {
                sb.append(",?");
            }
            sb.append(")");
            
            stmt = sqlConn.prepareStatement(sb.toString());
            stmt.setString(1, evt.getUuid());
            stmt.setTimestamp(2, new java.sql.Timestamp(evt.getTimestamp()));
            stmt.setString(3, evt.getType());
            stmt.setString(4, evt.getName());
            stmt.setString(5, evt.getAction());
            stmt.setString(6, evt.getHostName());
            stmt.setString(7, evt.getSource());
            stmt.setLong(8, evt.getDuration());
            for (int id = 9;id < idx;id++) {
                stmt.setString(id, statementParams.get(id));
            }
            
            // Execute Query
            stmt.executeUpdate();
            
            // Commit TX
            sqlConn.commit();

        } catch(Exception exc) {
            rollback(sqlConn);
            throw new AuditAccessException("Cannot insert event into DB (" + exc.getClass() + ") "+ exc.getCause(), exc);
        } finally {
           closeStatement(stmt);
           closeConnection(sqlConn);
        }
        return true;
    }
    
    /** {@inheritDoc} */
    @Override
    public Event getEventByUUID(String uuid, Long timestamp) {
        Util.assertHasLength(uuid);
        Connection          sqlConn = null;
        PreparedStatement   ps = null;
        ResultSet           rs = null;
        try {
            sqlConn = getDataSource().getConnection();
            ps = sqlConn.prepareStatement(getQueryBuilder().getEventByUuidQuery());
            ps.setString(1, uuid);
            rs = ps.executeQuery();
            if (rs.next()) {
               return EVENT_MAPPER.mapEvent(rs);
            }
            return null;
        } catch (SQLException sqlEX) {
            throw new IllegalStateException("CANNOT_READ_AUDITTABLE", sqlEX);
        } finally {
            closeResultSet(rs);
            closeStatement(ps);
            closeConnection(sqlConn);
        }
    }
     
    /** {@inheritDoc} */
    @Override
    public void purgeAuditTrail(EventQueryDefinition qDef) {
        Util.assertNotNull(qDef);
        Connection          sqlConn = null;
        PreparedStatement   ps = null;
        ResultSet           rs = null;
        try {
           sqlConn = getDataSource().getConnection();
           ps = sqlConn.prepareStatement(getQueryBuilder().getPurgeAuditTrailQuery(qDef));
           ps.setTimestamp(1, new java.sql.Timestamp(qDef.getFrom()));
           ps.setTimestamp(2, new java.sql.Timestamp(qDef.getTo()));
           ps.executeUpdate();
        } catch (SQLException sqlEX) {
            throw new IllegalStateException("CANNOT_READ_AUDITTABLE", sqlEX);
        } finally {
            closeResultSet(rs);
            closeStatement(ps);
            closeConnection(sqlConn);
        }
    }
    
    /** {@inheritDoc} */
    @Override
    public void purgeFeatureUsage(EventQueryDefinition qDef) {
        Util.assertNotNull(qDef);
        // Enforce remove "checks"
        qDef.getActionFilters().add(ACTION_CHECK_OK);
        
        Connection          sqlConn = null;
        PreparedStatement   ps = null;
        ResultSet           rs = null;
        try {
           sqlConn = getDataSource().getConnection();
            ps = sqlConn.prepareStatement(getQueryBuilder().getPurgeFeatureUsageQuery(qDef));
            ps.setTimestamp(1, new java.sql.Timestamp(qDef.getFrom()));
            ps.setTimestamp(2, new java.sql.Timestamp(qDef.getTo()));
            ps.executeUpdate();
        } catch (SQLException sqlEX) {
            throw new IllegalStateException("CANNOT_READ_AUDITTABLE", sqlEX);
        } finally {
            closeResultSet(rs);
            closeStatement(ps);
            closeConnection(sqlConn);
        }
    }
    
    /** {@inheritDoc} */
    private EventSeries searchEvents(String sqlQuery, long from, long to) {
        Connection          sqlConn = null;
        PreparedStatement   ps = null;
        ResultSet           rs = null;
        EventSeries         es = new EventSeries();
        try {
            sqlConn = getDataSource().getConnection();
            ps = sqlConn.prepareStatement(sqlQuery);
            ps.setTimestamp(1, new Timestamp(from));
            ps.setTimestamp(2, new Timestamp(to));
            rs = ps.executeQuery();
            while (rs.next()) {
                es.add(EVENT_MAPPER.mapEvent(rs));
            }
        } catch (SQLException sqlEX) {
            throw new IllegalStateException("CANNOT_READ_AUDITTABLE", sqlEX);
        } finally {
            closeResultSet(rs);
            closeStatement(ps);
            closeConnection(sqlConn);
        }
        return es;
    }
    
    /** {@inheritDoc} */
    private  Map<String, MutableHitCount> computeHitCount(String sqlQuery, String columnName, long from, long to) {
        Connection          sqlConn = null;
        PreparedStatement   ps = null;
        ResultSet           rs = null;
        Map<String, MutableHitCount>  hitCount = new HashMap<String, MutableHitCount>();
        try {
            // Returns features
            sqlConn = dataSource.getConnection();
            ps = sqlConn.prepareStatement(sqlQuery);
            ps.setTimestamp(1, new Timestamp(from));
            ps.setTimestamp(2, new Timestamp(to));
            rs = ps.executeQuery();
            while (rs.next()) {
                hitCount.put(rs.getString(columnName), new MutableHitCount(rs.getInt("NB")));
            } 
        } catch (SQLException sqlEX) {
            throw new FeatureAccessException(CANNOT_BUILD_PIE_CHART_FROM_REPOSITORY, sqlEX);
        } finally {
            closeResultSet(rs);
            closeStatement(ps);
            closeConnection(sqlConn);
        }
        return hitCount;
    }
    
    /** {@inheritDoc} */
    @Override
    public EventSeries getAuditTrail(EventQueryDefinition qDef) {
        return searchEvents(getQueryBuilder().getSelectAuditTrailQuery(qDef), qDef.getFrom(), qDef.getTo());
    }

    /** {@inheritDoc} */
    @Override
    public EventSeries searchFeatureUsageEvents(EventQueryDefinition qDef) {
        return searchEvents(getQueryBuilder().getSelectFeatureUsageQuery(qDef), qDef.getFrom(), qDef.getTo());
    }
        
    /** {@inheritDoc} */
    @Override
    public Map<String, MutableHitCount> getFeatureUsageHitCount(EventQueryDefinition query) {
        return computeHitCount(getQueryBuilder().getFeaturesHitCount(), COL_EVENT_NAME, query.getFrom(), query.getTo());
    }
    
    /** {@inheritDoc} */
    @Override
    public Map<String, MutableHitCount> getHostHitCount(EventQueryDefinition query) {
        return computeHitCount(getQueryBuilder().getHostHitCount(), COL_EVENT_HOSTNAME, query.getFrom(), query.getTo());
    }

    /** {@inheritDoc} */
    @Override
    public Map<String, MutableHitCount> getUserHitCount(EventQueryDefinition query) {
        return computeHitCount(getQueryBuilder().getUserHitCount(), COL_EVENT_USER, query.getFrom(), query.getTo());
    }

    /** {@inheritDoc} */
    @Override
    public Map<String, MutableHitCount> getSourceHitCount(EventQueryDefinition query) {
        return computeHitCount(getQueryBuilder().getSourceHitCount(), COL_EVENT_SOURCE, query.getFrom(), query.getTo());
    }

    /** {@inheritDoc} */
    @Override
    public TimeSeriesChart getFeatureUsageHistory(EventQueryDefinition query, TimeUnit units) {
        // Create the interval depending on units
        TimeSeriesChart tsc = new TimeSeriesChart(query.getFrom(), query.getTo(), units);
        // Search All events
        Iterator<Event> iterEvent = searchFeatureUsageEvents(query).iterator();
        // Dispatch events into time slots
        while (iterEvent.hasNext()) {
            tsc.addEvent(iterEvent.next());
        }
        return tsc;
    }
   
    /**
     * Getter accessor for attribute 'dataSource'.
     *
     * @return current value of 'dataSource'
     */
    public DataSource getDataSource() {
        return dataSource;
    }

    /**
     * Setter accessor for attribute 'dataSource'.
     * 
     * @param dataSource
     *            new value for 'dataSource '
     */
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    /**
	 * @return the queryBuilder
	 */
	public JdbcQueryBuilder getQueryBuilder() {
		if (queryBuilder == null) {
			queryBuilder = new JdbcQueryBuilder();
		}
		return queryBuilder;
	}

	/**
	 * @param queryBuilder the queryBuilder to set
	 */
	public void setQueryBuilder(JdbcQueryBuilder queryBuilder) {
		this.queryBuilder = queryBuilder;
	}
}
