package com.mzh.library.dao.impl;

import com.mzh.library.dao.BorrowRecordDao;
import com.mzh.library.entity.Book;
import com.mzh.library.entity.BookStatus;
import com.mzh.library.entity.BorrowRecord;
import com.mzh.library.entity.BorrowRecordSearchCriteria;
import com.mzh.library.entity.BorrowRecordStatus;
import com.mzh.library.entity.Reader;
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.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JdbcBorrowRecordDao implements BorrowRecordDao {
    private static final String RECORD_COLUMNS = ""
        + "br.id, br.reader_id, r.reader_identifier, r.full_name AS reader_name, "
        + "br.book_id, b.book_identifier, b.title AS book_title, "
        + "br.borrowed_at, br.due_at, br.returned_at, br.renewal_count, br.status, "
        + "br.created_at, br.updated_at ";

    private static final String RECORD_FROM = ""
        + "FROM borrow_records br "
        + "JOIN readers r ON r.id = br.reader_id "
        + "JOIN books b ON b.id = br.book_id ";

    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 BOOK_COLUMNS = ""
        + "b.id, b.book_identifier, b.title, b.author, b.category_id, c.name AS category_name, "
        + "b.total_copies, b.available_copies, b.status, b.created_at, b.updated_at ";

    private static final String BOOK_FROM = ""
        + "FROM books b "
        + "JOIN book_categories c ON c.id = b.category_id ";

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

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

    private static final String FIND_BOOK_BY_IDENTIFIER_FOR_UPDATE = "SELECT " + BOOK_COLUMNS + BOOK_FROM
        + "WHERE b.book_identifier = ? FOR UPDATE";

    private static final String FIND_RECORD_BY_ID_FOR_UPDATE = "SELECT " + RECORD_COLUMNS + RECORD_FROM
        + "WHERE br.id = ? FOR UPDATE";

    private static final String COUNT_ACTIVE_BY_READER_ID = ""
        + "SELECT COUNT(*) "
        + "FROM borrow_records "
        + "WHERE reader_id = ? AND status = ? AND returned_at IS NULL";

    private static final String CREATE = ""
        + "INSERT INTO borrow_records "
        + "(reader_id, book_id, borrowed_at, due_at, renewal_count, status) "
        + "VALUES (?, ?, ?, ?, ?, ?)";

    private static final String DECREMENT_AVAILABLE = ""
        + "UPDATE books "
        + "SET available_copies = available_copies - 1 "
        + "WHERE id = ? AND available_copies > 0";

    private static final String INCREMENT_AVAILABLE = ""
        + "UPDATE books "
        + "SET available_copies = LEAST(available_copies + 1, total_copies) "
        + "WHERE id = ?";

    private static final String MARK_RETURNED = ""
        + "UPDATE borrow_records "
        + "SET status = ?, returned_at = ? "
        + "WHERE id = ? AND status = ? AND returned_at IS NULL";

    private static final String RENEW = ""
        + "UPDATE borrow_records "
        + "SET due_at = ?, renewal_count = renewal_count + 1 "
        + "WHERE id = ? AND status = ? AND returned_at IS NULL";

    @Override
    public List<BorrowRecord> search(BorrowRecordSearchCriteria criteria) {
        BorrowRecordSearchCriteria normalized = criteria == null ? new BorrowRecordSearchCriteria() : criteria;
        List<Object> parameters = new ArrayList<>();
        StringBuilder sql = new StringBuilder("SELECT ")
            .append(RECORD_COLUMNS)
            .append(RECORD_FROM)
            .append("WHERE 1 = 1 ");

        appendLike(sql, parameters, "r.reader_identifier", normalized.getReaderIdentifier());
        appendLike(sql, parameters, "b.book_identifier", normalized.getBookIdentifier());
        appendStatus(sql, parameters, normalized);
        appendOrder(sql);

        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(sql.toString())) {
            bind(statement, parameters);
            try (ResultSet resultSet = statement.executeQuery()) {
                return mapRecords(resultSet);
            }
        } catch (SQLException | IllegalArgumentException ex) {
            throw new DaoException("Unable to search borrow records", ex);
        }
    }

    @Override
    public List<BorrowRecord> findByReaderId(long readerId) {
        StringBuilder sql = new StringBuilder("SELECT ")
            .append(RECORD_COLUMNS)
            .append(RECORD_FROM)
            .append("WHERE br.reader_id = ? ");
        appendOrder(sql);

        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(sql.toString())) {
            statement.setLong(1, readerId);
            try (ResultSet resultSet = statement.executeQuery()) {
                return mapRecords(resultSet);
            }
        } catch (SQLException | IllegalArgumentException ex) {
            throw new DaoException("Unable to load reader borrow records", ex);
        }
    }

    @Override
    public Optional<Reader> findReaderByUserId(long userId) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(FIND_READER_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 for borrowing", ex);
        }
    }

    @Override
    public Optional<Reader> findReaderByIdentifierForUpdate(Connection connection, String identifier) {
        try (PreparedStatement statement = connection.prepareStatement(FIND_READER_BY_IDENTIFIER_FOR_UPDATE)) {
            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 lock reader for borrowing", ex);
        }
    }

    @Override
    public Optional<Book> findBookByIdentifierForUpdate(Connection connection, String identifier) {
        try (PreparedStatement statement = connection.prepareStatement(FIND_BOOK_BY_IDENTIFIER_FOR_UPDATE)) {
            statement.setString(1, identifier);
            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? Optional.of(mapBook(resultSet)) : Optional.empty();
            }
        } catch (SQLException | IllegalArgumentException ex) {
            throw new DaoException("Unable to lock book for borrowing", ex);
        }
    }

    @Override
    public Optional<BorrowRecord> findByIdForUpdate(Connection connection, long id) {
        try (PreparedStatement statement = connection.prepareStatement(FIND_RECORD_BY_ID_FOR_UPDATE)) {
            statement.setLong(1, id);
            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? Optional.of(mapRecord(resultSet)) : Optional.empty();
            }
        } catch (SQLException | IllegalArgumentException ex) {
            throw new DaoException("Unable to lock borrow record", ex);
        }
    }

    @Override
    public int countActiveByReaderId(Connection connection, long readerId) {
        try (PreparedStatement statement = connection.prepareStatement(COUNT_ACTIVE_BY_READER_ID)) {
            statement.setLong(1, readerId);
            statement.setString(2, BorrowRecordStatus.ACTIVE.getCode());
            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? resultSet.getInt(1) : 0;
            }
        } catch (SQLException ex) {
            throw new DaoException("Unable to count active borrow records", ex);
        }
    }

    @Override
    public long create(Connection connection, BorrowRecord record) {
        try (PreparedStatement statement = connection.prepareStatement(CREATE, Statement.RETURN_GENERATED_KEYS)) {
            statement.setLong(1, record.getReaderId());
            statement.setLong(2, record.getBookId());
            statement.setTimestamp(3, Timestamp.valueOf(record.getBorrowedAt()));
            statement.setTimestamp(4, Timestamp.valueOf(record.getDueAt()));
            statement.setInt(5, record.getRenewalCount());
            statement.setString(6, record.getStatus().getCode());
            statement.executeUpdate();

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

    @Override
    public boolean decrementAvailableCopies(Connection connection, long bookId) {
        try (PreparedStatement statement = connection.prepareStatement(DECREMENT_AVAILABLE)) {
            statement.setLong(1, bookId);
            return statement.executeUpdate() == 1;
        } catch (SQLException ex) {
            throw new DaoException("Unable to decrement available book copies", ex);
        }
    }

    @Override
    public boolean incrementAvailableCopies(Connection connection, long bookId) {
        try (PreparedStatement statement = connection.prepareStatement(INCREMENT_AVAILABLE)) {
            statement.setLong(1, bookId);
            statement.executeUpdate();
            return true;
        } catch (SQLException ex) {
            throw new DaoException("Unable to increment available book copies", ex);
        }
    }

    @Override
    public boolean markReturned(Connection connection, long id, LocalDateTime returnedAt) {
        try (PreparedStatement statement = connection.prepareStatement(MARK_RETURNED)) {
            statement.setString(1, BorrowRecordStatus.RETURNED.getCode());
            statement.setTimestamp(2, Timestamp.valueOf(returnedAt));
            statement.setLong(3, id);
            statement.setString(4, BorrowRecordStatus.ACTIVE.getCode());
            return statement.executeUpdate() == 1;
        } catch (SQLException ex) {
            throw new DaoException("Unable to mark borrow record returned", ex);
        }
    }

    @Override
    public boolean renew(Connection connection, long id, LocalDateTime dueAt) {
        try (PreparedStatement statement = connection.prepareStatement(RENEW)) {
            statement.setTimestamp(1, Timestamp.valueOf(dueAt));
            statement.setLong(2, id);
            statement.setString(3, BorrowRecordStatus.ACTIVE.getCode());
            return statement.executeUpdate() == 1;
        } catch (SQLException ex) {
            throw new DaoException("Unable to renew borrow record", 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 appendStatus(StringBuilder sql, List<Object> parameters, BorrowRecordSearchCriteria criteria) {
        String statusCode = criteria.getStatusCode();
        if (statusCode == null || statusCode.isEmpty()) {
            return;
        }
        if (criteria.isOverdueOnly()) {
            sql.append("AND br.status = ? AND br.returned_at IS NULL AND br.due_at < CURRENT_TIMESTAMP ");
            parameters.add(BorrowRecordStatus.ACTIVE.getCode());
            return;
        }
        sql.append("AND br.status = ? ");
        parameters.add(statusCode);
    }

    private void appendOrder(StringBuilder sql) {
        sql.append("ORDER BY ")
            .append("CASE ")
            .append("WHEN br.status = 'active' AND br.returned_at IS NULL AND br.due_at < CURRENT_TIMESTAMP THEN 0 ")
            .append("WHEN br.status = 'active' THEN 1 ELSE 2 END, ")
            .append("br.due_at, br.borrowed_at DESC");
    }

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

    private List<BorrowRecord> mapRecords(ResultSet resultSet) throws SQLException {
        List<BorrowRecord> records = new ArrayList<>();
        while (resultSet.next()) {
            records.add(mapRecord(resultSet));
        }
        return records;
    }

    private BorrowRecord mapRecord(ResultSet resultSet) throws SQLException {
        BorrowRecord record = new BorrowRecord();
        record.setId(resultSet.getLong("id"));
        record.setReaderId(resultSet.getLong("reader_id"));
        record.setReaderIdentifier(resultSet.getString("reader_identifier"));
        record.setReaderName(resultSet.getString("reader_name"));
        record.setBookId(resultSet.getLong("book_id"));
        record.setBookIdentifier(resultSet.getString("book_identifier"));
        record.setBookTitle(resultSet.getString("book_title"));
        record.setBorrowedAt(toLocalDateTime(resultSet.getTimestamp("borrowed_at")));
        record.setDueAt(toLocalDateTime(resultSet.getTimestamp("due_at")));
        record.setReturnedAt(toLocalDateTime(resultSet.getTimestamp("returned_at")));
        record.setRenewalCount(resultSet.getInt("renewal_count"));
        record.setStatus(BorrowRecordStatus.fromCode(resultSet.getString("status")));
        record.setCreatedAt(toLocalDateTime(resultSet.getTimestamp("created_at")));
        record.setUpdatedAt(toLocalDateTime(resultSet.getTimestamp("updated_at")));
        return record;
    }

    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 Book mapBook(ResultSet resultSet) throws SQLException {
        Book book = new Book();
        book.setId(resultSet.getLong("id"));
        book.setIdentifier(resultSet.getString("book_identifier"));
        book.setTitle(resultSet.getString("title"));
        book.setAuthor(resultSet.getString("author"));
        book.setCategoryId(resultSet.getLong("category_id"));
        book.setCategoryName(resultSet.getString("category_name"));
        book.setTotalCopies(resultSet.getInt("total_copies"));
        book.setAvailableCopies(resultSet.getInt("available_copies"));
        book.setStatus(BookStatus.fromCode(resultSet.getString("status")));
        book.setCreatedAt(toLocalDateTime(resultSet.getTimestamp("created_at")));
        book.setUpdatedAt(toLocalDateTime(resultSet.getTimestamp("updated_at")));
        return book;
    }

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