package com.mzh.library.dao.impl;

import com.mzh.library.dao.ReaderDao;
import com.mzh.library.entity.Reader;
import com.mzh.library.entity.ReaderSearchCriteria;
import com.mzh.library.entity.ReaderStatus;
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;
import java.util.Optional;

public class JdbcReaderDao implements ReaderDao {
    private static final String READER_COLUMNS = ""
        + "r.id, r.reader_identifier, r.user_id, u.username, r.full_name, r.phone, r.email, "
        + "r.status, r.max_borrow_count, r.created_at, r.updated_at ";

    private static final String READER_FROM = ""
        + "FROM readers r "
        + "LEFT JOIN users u ON u.id = r.user_id ";

    private static final String FIND_BY_ID = "SELECT " + READER_COLUMNS + READER_FROM + "WHERE r.id = ?";

    private static final String FIND_BY_IDENTIFIER = "SELECT " + READER_COLUMNS + READER_FROM
        + "WHERE r.reader_identifier = ?";

    private static final String FIND_BY_USER_ID = "SELECT " + READER_COLUMNS + READER_FROM
        + "WHERE r.user_id = ?";

    private static final String CREATE = ""
        + "INSERT INTO readers "
        + "(reader_identifier, user_id, full_name, phone, email, status, max_borrow_count) "
        + "VALUES (?, ?, ?, ?, ?, ?, ?)";

    private static final String UPDATE = ""
        + "UPDATE readers "
        + "SET reader_identifier = ?, user_id = ?, full_name = ?, phone = ?, email = ?, "
        + "status = ?, max_borrow_count = ? "
        + "WHERE id = ?";

    private static final String DEACTIVATE = "UPDATE readers SET status = ? WHERE id = ?";

    @Override
    public List<Reader> search(ReaderSearchCriteria criteria) {
        List<Object> parameters = new ArrayList<>();
        StringBuilder sql = new StringBuilder("SELECT ")
            .append(READER_COLUMNS)
            .append(READER_FROM)
            .append("WHERE 1 = 1 ");

        appendLike(sql, parameters, "r.reader_identifier", criteria.getIdentifier());
        appendLike(sql, parameters, "r.full_name", criteria.getName());
        appendContact(sql, parameters, criteria.getContact());
        if (criteria.getStatusCode() != null && !criteria.getStatusCode().isEmpty()) {
            sql.append("AND r.status = ? ");
            parameters.add(criteria.getStatusCode());
        }
        sql.append("ORDER BY r.full_name, r.reader_identifier");

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

    @Override
    public Optional<Reader> findById(long id) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(FIND_BY_ID)) {
            statement.setLong(1, id);
            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? Optional.of(mapReader(resultSet)) : Optional.empty();
            }
        } catch (SQLException | IllegalArgumentException ex) {
            throw new DaoException("Unable to load reader by id", ex);
        }
    }

    @Override
    public Optional<Reader> findByIdentifier(String identifier) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(FIND_BY_IDENTIFIER)) {
            statement.setString(1, identifier);
            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? Optional.of(mapReader(resultSet)) : Optional.empty();
            }
        } catch (SQLException | IllegalArgumentException ex) {
            throw new DaoException("Unable to load reader by identifier", ex);
        }
    }

    @Override
    public Optional<Reader> findByUserId(long userId) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(FIND_BY_USER_ID)) {
            statement.setLong(1, userId);
            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? Optional.of(mapReader(resultSet)) : Optional.empty();
            }
        } catch (SQLException | IllegalArgumentException ex) {
            throw new DaoException("Unable to load reader by user id", ex);
        }
    }

    @Override
    public long create(Reader reader) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(CREATE, Statement.RETURN_GENERATED_KEYS)) {
            bindReader(statement, reader);
            statement.executeUpdate();

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

    @Override
    public boolean update(Reader reader) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(UPDATE)) {
            bindReader(statement, reader);
            statement.setLong(8, reader.getId());
            return statement.executeUpdate() == 1;
        } catch (SQLException ex) {
            throw new DaoException("Unable to update reader", ex);
        }
    }

    @Override
    public boolean deactivate(long id) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(DEACTIVATE)) {
            statement.setString(1, ReaderStatus.INACTIVE.getCode());
            statement.setLong(2, id);
            return statement.executeUpdate() == 1;
        } catch (SQLException ex) {
            throw new DaoException("Unable to deactivate reader", ex);
        }
    }

    private void appendLike(StringBuilder sql, List<Object> parameters, String column, String value) {
        if (value == null || value.trim().isEmpty()) {
            return;
        }
        sql.append("AND ").append(column).append(" LIKE ? ");
        parameters.add("%" + value.trim() + "%");
    }

    private void appendContact(StringBuilder sql, List<Object> parameters, String value) {
        if (value == null || value.trim().isEmpty()) {
            return;
        }
        sql.append("AND (r.phone LIKE ? OR r.email LIKE ?) ");
        String filter = "%" + value.trim() + "%";
        parameters.add(filter);
        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);
            statement.setString(i + 1, value.toString());
        }
    }

    private void bindReader(PreparedStatement statement, Reader reader) throws SQLException {
        statement.setString(1, reader.getIdentifier());
        if (reader.getUserId() == null) {
            statement.setNull(2, Types.BIGINT);
        } else {
            statement.setLong(2, reader.getUserId());
        }
        statement.setString(3, reader.getFullName());
        statement.setString(4, reader.getPhone());
        statement.setString(5, reader.getEmail());
        statement.setString(6, reader.getStatus().getCode());
        statement.setInt(7, reader.getMaxBorrowCount());
    }

    private Reader mapReader(ResultSet resultSet) throws SQLException {
        Reader reader = new Reader();
        reader.setId(resultSet.getLong("id"));
        reader.setIdentifier(resultSet.getString("reader_identifier"));
        long userId = resultSet.getLong("user_id");
        reader.setUserId(resultSet.wasNull() ? null : userId);
        reader.setUsername(resultSet.getString("username"));
        reader.setFullName(resultSet.getString("full_name"));
        reader.setPhone(resultSet.getString("phone"));
        reader.setEmail(resultSet.getString("email"));
        reader.setStatus(ReaderStatus.fromCode(resultSet.getString("status")));
        reader.setMaxBorrowCount(resultSet.getInt("max_borrow_count"));
        reader.setCreatedAt(toLocalDateTime(resultSet.getTimestamp("created_at")));
        reader.setUpdatedAt(toLocalDateTime(resultSet.getTimestamp("updated_at")));
        return reader;
    }

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