package com.mzh.library.dao.impl;

import com.mzh.library.dao.SystemLogDao;
import com.mzh.library.entity.SystemLog;
import com.mzh.library.entity.SystemLogSearchCriteria;
import com.mzh.library.exception.DaoException;
import com.mzh.library.util.JdbcUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

public class JdbcSystemLogDao implements SystemLogDao {
    private static final String LOG_COLUMNS = ""
        + "sl.id, sl.operator_id, u.username AS operator_username, "
        + "u.display_name AS operator_display_name, sl.operator_role, sl.operation_type, "
        + "sl.target_table, sl.target_id, sl.result_status, sl.message, sl.request_ip, sl.created_at ";

    private static final String LOG_FROM = ""
        + "FROM system_logs sl "
        + "LEFT JOIN users u ON u.id = sl.operator_id ";

    private static final String OPERATION_TYPES = ""
        + "SELECT DISTINCT operation_type "
        + "FROM system_logs "
        + "ORDER BY operation_type";

    private static final String CREATE = ""
        + "INSERT INTO system_logs "
        + "(operator_id, operator_role, operation_type, target_table, target_id, result_status, message, request_ip) "
        + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";

    @Override
    public List<SystemLog> search(SystemLogSearchCriteria criteria) {
        SystemLogSearchCriteria normalized = criteria == null ? new SystemLogSearchCriteria() : criteria;
        List<Object> parameters = new ArrayList<>();
        StringBuilder sql = new StringBuilder("SELECT ")
            .append(LOG_COLUMNS)
            .append(LOG_FROM)
            .append("WHERE 1 = 1 ");

        if (!normalized.getOperationType().isEmpty()) {
            sql.append("AND sl.operation_type = ? ");
            parameters.add(normalized.getOperationType());
        }
        appendKeyword(sql, parameters, normalized.getKeyword());
        if (normalized.getCreatedFrom() != null) {
            sql.append("AND sl.created_at >= ? ");
            parameters.add(Timestamp.valueOf(normalized.getCreatedFrom().atStartOfDay()));
        }
        if (normalized.getCreatedTo() != null) {
            sql.append("AND sl.created_at < ? ");
            parameters.add(Timestamp.valueOf(normalized.getCreatedTo().plusDays(1).atStartOfDay()));
        }
        sql.append("ORDER BY sl.created_at DESC, sl.id DESC");

        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(sql.toString())) {
            bind(statement, parameters);
            try (ResultSet resultSet = statement.executeQuery()) {
                List<SystemLog> logs = new ArrayList<>();
                while (resultSet.next()) {
                    logs.add(mapLog(resultSet));
                }
                return logs;
            }
        } catch (SQLException ex) {
            throw new DaoException("Unable to search system logs", ex);
        }
    }

    @Override
    public List<String> findOperationTypes() {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(OPERATION_TYPES);
             ResultSet resultSet = statement.executeQuery()) {
            List<String> operationTypes = new ArrayList<>();
            while (resultSet.next()) {
                operationTypes.add(resultSet.getString("operation_type"));
            }
            return operationTypes;
        } catch (SQLException ex) {
            throw new DaoException("Unable to load system log operation types", ex);
        }
    }

    @Override
    public long create(Connection connection, SystemLog log) {
        try (PreparedStatement statement = connection.prepareStatement(CREATE, Statement.RETURN_GENERATED_KEYS)) {
            if (log.getOperatorId() == null) {
                statement.setNull(1, Types.BIGINT);
            } else {
                statement.setLong(1, log.getOperatorId());
            }
            statement.setString(2, log.getOperatorRole());
            statement.setString(3, log.getOperationType());
            statement.setString(4, log.getTargetTable());
            statement.setString(5, log.getTargetId());
            statement.setString(6, log.getResultStatus());
            statement.setString(7, log.getMessage());
            statement.setString(8, log.getRequestIp());
            statement.executeUpdate();

            try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
                if (generatedKeys.next()) {
                    return generatedKeys.getLong(1);
                }
            }
            throw new DaoException("Unable to read generated system log id", null);
        } catch (SQLException ex) {
            throw new DaoException("Unable to create system log", ex);
        }
    }

    private void appendKeyword(StringBuilder sql, List<Object> parameters, String value) {
        if (value == null || value.trim().isEmpty()) {
            return;
        }
        String filter = "%" + value.trim() + "%";
        sql.append("AND (sl.operation_type LIKE ? OR sl.target_table LIKE ? OR sl.target_id LIKE ? ")
            .append("OR sl.message LIKE ? OR sl.request_ip LIKE ? OR u.username LIKE ? OR u.display_name LIKE ?) ");
        for (int i = 0; i < 7; i++) {
            parameters.add(filter);
        }
    }

    private void bind(PreparedStatement statement, List<Object> parameters) throws SQLException {
        for (int i = 0; i < parameters.size(); i++) {
            Object value = parameters.get(i);
            if (value instanceof Timestamp) {
                statement.setTimestamp(i + 1, (Timestamp) value);
            } else {
                statement.setString(i + 1, value.toString());
            }
        }
    }

    private SystemLog mapLog(ResultSet resultSet) throws SQLException {
        SystemLog log = new SystemLog();
        log.setId(resultSet.getLong("id"));
        long operatorId = resultSet.getLong("operator_id");
        log.setOperatorId(resultSet.wasNull() ? null : operatorId);
        log.setOperatorUsername(resultSet.getString("operator_username"));
        log.setOperatorDisplayName(resultSet.getString("operator_display_name"));
        log.setOperatorRole(resultSet.getString("operator_role"));
        log.setOperationType(resultSet.getString("operation_type"));
        log.setTargetTable(resultSet.getString("target_table"));
        log.setTargetId(resultSet.getString("target_id"));
        log.setResultStatus(resultSet.getString("result_status"));
        log.setMessage(resultSet.getString("message"));
        log.setRequestIp(resultSet.getString("request_ip"));
        log.setCreatedAt(toLocalDateTime(resultSet.getTimestamp("created_at")));
        return log;
    }

    private LocalDateTime toLocalDateTime(Timestamp timestamp) {
        return timestamp == null ? null : timestamp.toLocalDateTime();
    }
}
