package com.mzh.library.dao.impl;

import com.mzh.library.dao.BookDao;
import com.mzh.library.entity.Book;
import com.mzh.library.entity.BookCategory;
import com.mzh.library.entity.BookSearchCriteria;
import com.mzh.library.entity.BookStatus;
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 JdbcBookDao implements BookDao {
    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_ALL_CATEGORIES = ""
        + "SELECT id, name, description "
        + "FROM book_categories "
        + "ORDER BY name";

    private static final String FIND_CATEGORY_BY_ID = ""
        + "SELECT id, name, description "
        + "FROM book_categories "
        + "WHERE id = ?";

    private static final String FIND_CATEGORY_BY_NAME = ""
        + "SELECT id, name, description "
        + "FROM book_categories "
        + "WHERE name = ?";

    private static final String CREATE_CATEGORY = ""
        + "INSERT INTO book_categories (name, description) "
        + "VALUES (?, ?)";

    private static final String UPDATE_CATEGORY = ""
        + "UPDATE book_categories "
        + "SET name = ?, description = ? "
        + "WHERE id = ?";

    private static final String DELETE_CATEGORY = "DELETE FROM book_categories WHERE id = ?";

    private static final String COUNT_BOOKS_BY_CATEGORY = ""
        + "SELECT COUNT(*) "
        + "FROM books "
        + "WHERE category_id = ?";

    private static final String FIND_BY_ID = "SELECT " + BOOK_COLUMNS + BOOK_FROM + "WHERE b.id = ?";

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

    private static final String CREATE = ""
        + "INSERT INTO books "
        + "(book_identifier, title, author, category_id, total_copies, available_copies, status) "
        + "VALUES (?, ?, ?, ?, ?, ?, ?)";

    private static final String UPDATE = ""
        + "UPDATE books "
        + "SET book_identifier = ?, title = ?, author = ?, category_id = ?, total_copies = ?, "
        + "available_copies = ?, status = ? "
        + "WHERE id = ?";

    private static final String DELETE = "DELETE FROM books WHERE id = ?";

    @Override
    public List<BookCategory> findAllCategories() {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(FIND_ALL_CATEGORIES);
             ResultSet resultSet = statement.executeQuery()) {
            List<BookCategory> categories = new ArrayList<>();
            while (resultSet.next()) {
                categories.add(mapCategory(resultSet));
            }
            return categories;
        } catch (SQLException ex) {
            throw new DaoException("Unable to load book categories", ex);
        }
    }

    @Override
    public Optional<BookCategory> findCategoryById(long id) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(FIND_CATEGORY_BY_ID)) {
            statement.setLong(1, id);
            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? Optional.of(mapCategory(resultSet)) : Optional.empty();
            }
        } catch (SQLException ex) {
            throw new DaoException("Unable to load book category by id", ex);
        }
    }

    @Override
    public Optional<BookCategory> findCategoryByName(String name) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(FIND_CATEGORY_BY_NAME)) {
            statement.setString(1, name);
            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? Optional.of(mapCategory(resultSet)) : Optional.empty();
            }
        } catch (SQLException ex) {
            throw new DaoException("Unable to load book category by name", ex);
        }
    }

    @Override
    public long createCategory(BookCategory category) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(CREATE_CATEGORY, Statement.RETURN_GENERATED_KEYS)) {
            bindCategory(statement, category);
            statement.executeUpdate();

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

    @Override
    public boolean updateCategory(BookCategory category) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(UPDATE_CATEGORY)) {
            bindCategory(statement, category);
            statement.setLong(3, category.getId());
            return statement.executeUpdate() == 1;
        } catch (SQLException ex) {
            throw new DaoException("Unable to update book category", ex);
        }
    }

    @Override
    public boolean deleteCategory(long id) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(DELETE_CATEGORY)) {
            statement.setLong(1, id);
            return statement.executeUpdate() == 1;
        } catch (SQLException ex) {
            throw new DaoException("Unable to delete book category", ex);
        }
    }

    @Override
    public int countBooksByCategoryId(long categoryId) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(COUNT_BOOKS_BY_CATEGORY)) {
            statement.setLong(1, categoryId);
            try (ResultSet resultSet = statement.executeQuery()) {
                return resultSet.next() ? resultSet.getInt(1) : 0;
            }
        } catch (SQLException ex) {
            throw new DaoException("Unable to count books by category", ex);
        }
    }

    @Override
    public List<Book> search(BookSearchCriteria criteria) {
        List<Object> parameters = new ArrayList<>();
        StringBuilder sql = new StringBuilder("SELECT ")
            .append(BOOK_COLUMNS)
            .append(BOOK_FROM)
            .append("WHERE 1 = 1 ");

        appendLike(sql, parameters, "b.book_identifier", criteria.getIdentifier());
        appendLike(sql, parameters, "b.title", criteria.getTitle());
        appendLike(sql, parameters, "b.author", criteria.getAuthor());
        if (criteria.getCategoryId() != null) {
            sql.append("AND b.category_id = ? ");
            parameters.add(criteria.getCategoryId());
        }
        sql.append("ORDER BY b.title, b.author, b.book_identifier");

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

    @Override
    public Optional<Book> 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(mapBook(resultSet)) : Optional.empty();
            }
        } catch (SQLException | IllegalArgumentException ex) {
            throw new DaoException("Unable to load book by id", ex);
        }
    }

    @Override
    public Optional<Book> 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(mapBook(resultSet)) : Optional.empty();
            }
        } catch (SQLException | IllegalArgumentException ex) {
            throw new DaoException("Unable to load book by identifier", ex);
        }
    }

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

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

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

    @Override
    public boolean delete(long id) {
        try (Connection connection = JdbcUtil.getConnection();
             PreparedStatement statement = connection.prepareStatement(DELETE)) {
            statement.setLong(1, id);
            return statement.executeUpdate() == 1;
        } catch (SQLException ex) {
            throw new DaoException("Unable to delete book", 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 bind(PreparedStatement statement, List<Object> parameters) throws SQLException {
        for (int i = 0; i < parameters.size(); i++) {
            Object value = parameters.get(i);
            if (value instanceof Long) {
                statement.setLong(i + 1, (Long) value);
            } else {
                statement.setString(i + 1, value.toString());
            }
        }
    }

    private void bindBook(PreparedStatement statement, Book book) throws SQLException {
        statement.setString(1, book.getIdentifier());
        statement.setString(2, book.getTitle());
        statement.setString(3, book.getAuthor());
        statement.setLong(4, book.getCategoryId());
        statement.setInt(5, book.getTotalCopies());
        statement.setInt(6, book.getAvailableCopies());
        statement.setString(7, book.getStatus().getCode());
    }

    private void bindCategory(PreparedStatement statement, BookCategory category) throws SQLException {
        statement.setString(1, category.getName());
        statement.setString(2, category.getDescription());
    }

    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 BookCategory mapCategory(ResultSet resultSet) throws SQLException {
        BookCategory category = new BookCategory();
        category.setId(resultSet.getLong("id"));
        category.setName(resultSet.getString("name"));
        category.setDescription(resultSet.getString("description"));
        return category;
    }

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