package com.mzh.library.service;

import com.mzh.library.dao.BookDao;
import com.mzh.library.entity.AuthenticatedUser;
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.entity.Permission;
import com.mzh.library.entity.Role;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.impl.BookServiceImpl;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

public final class BookServiceCheck {
    private static final String UNAVAILABLE_MESSAGE =
        "图书服务暂时不可用，请稍后重试。";

    private BookServiceCheck() {
    }

    public static void main(String[] args) {
        Logger.getLogger(BookServiceImpl.class.getName()).setLevel(Level.OFF);

        InMemoryBookDao dao = new InMemoryBookDao();
        BookService service = new BookServiceImpl(dao);
        AuthenticatedUser librarian = user(10L, Role.LIBRARIAN);
        AuthenticatedUser reader = user(20L, Role.READER);

        ServiceResult<Long> invalidInventory = service.createBook(librarian,
            book(0L, "BK-1000", "Invalid Copies", "Test Author", 1L, 2, 3, BookStatus.AVAILABLE));
        require(!invalidInventory.isSuccessful(), "invalid inventory should fail");
        require(invalidInventory.getErrors().containsKey("availableCopies"),
            "available copies greater than total should be rejected");

        ServiceResult<Long> denied = service.createBook(reader,
            book(0L, "BK-1001", "Reader Write", "Test Author", 1L, 1, 1, BookStatus.AVAILABLE));
        require(!denied.isSuccessful(), "reader write should fail");
        require("您无权管理图书。".equals(denied.getMessage()),
            "reader write should return permission message");

        ServiceResult<Long> deniedCategory = service.createCategory(reader,
            category(0L, "Reader Category", "Denied category"));
        require(!deniedCategory.isSuccessful(), "reader category create should fail");
        require("您无权管理图书。".equals(deniedCategory.getMessage()),
            "reader category write should return permission message");

        ServiceResult<Long> created = service.createBook(librarian,
            book(0L, "BK-1002", "Service Test", "Test Author", 1L, 2, 1, BookStatus.AVAILABLE));
        require(created.isSuccessful(), "librarian should create a valid book");
        long createdId = created.getData();

        ServiceResult<Long> duplicate = service.createBook(librarian,
            book(0L, "BK-1002", "Duplicate", "Test Author", 1L, 1, 1, BookStatus.AVAILABLE));
        require(!duplicate.isSuccessful(), "duplicate identifier should fail");
        require(duplicate.getErrors().containsKey("identifier"), "duplicate should target identifier field");

        ServiceResult<Void> updated = service.updateBook(librarian,
            book(createdId, "BK-1003", "Service Test Updated", "Test Author", 1L, 3, 3, BookStatus.AVAILABLE));
        require(updated.isSuccessful(), "librarian should update a valid book");
        require(dao.findById(createdId).get().getAvailableCopies() == 3, "update should persist available copies");

        ServiceResult<Void> deleteUsedCategory = service.deleteCategory(librarian, 1L);
        require(!deleteUsedCategory.isSuccessful(), "used category delete should fail");
        require("该分类已被现有图书使用，不能删除。".equals(deleteUsedCategory.getMessage()),
            "used category delete should return a safe specific message");
        require(deleteUsedCategory.getErrors().containsKey("category"),
            "used category delete should return a category-level field error");

        ServiceResult<List<Book>> search = service.searchBooks(new BookSearchCriteria("BK-1003", "", "", null));
        require(search.isSuccessful(), "search should succeed");
        require(search.getData().size() == 1, "search should find updated identifier");

        ServiceResult<Void> deleted = service.deleteBook(librarian, createdId);
        require(deleted.isSuccessful(), "librarian should delete a book");
        require(!dao.findById(createdId).isPresent(), "delete should remove the record");

        ServiceResult<Long> createdCategory = service.createCategory(librarian,
            category(0L, "Architecture", "Design and systems"));
        require(createdCategory.isSuccessful(), "librarian should create a category");
        long categoryId = createdCategory.getData();

        ServiceResult<Long> duplicateCategory = service.createCategory(librarian,
            category(0L, "Architecture", "Duplicate category"));
        require(!duplicateCategory.isSuccessful(), "duplicate category should fail");
        require(duplicateCategory.getErrors().containsKey("name"), "duplicate category should target name field");

        ServiceResult<Void> updatedCategory = service.updateCategory(librarian,
            category(categoryId, "Software Architecture", "Updated category"));
        require(updatedCategory.isSuccessful(), "librarian should update a category");
        require("Software Architecture".equals(dao.findCategoryById(categoryId).get().getName()),
            "category update should persist the new name");

        ServiceResult<Void> deletedCategory = service.deleteCategory(librarian, categoryId);
        require(deletedCategory.isSuccessful(), "unused category should be deleted");
        require(!dao.findCategoryById(categoryId).isPresent(), "category delete should remove unused category");

        BookService failingService = new BookServiceImpl(new FailingBookDao());
        ServiceResult<List<Book>> unavailable = failingService.searchBooks(new BookSearchCriteria());
        require(!unavailable.isSuccessful(), "DAO failure should not escape service");
        require(UNAVAILABLE_MESSAGE.equals(unavailable.getMessage()), "DAO failure should map to safe message");
    }

    private static AuthenticatedUser user(long id, Role role) {
        return new AuthenticatedUser(id, role.getCode(), role.getDisplayName(), role,
            role == Role.READER
                ? EnumSet.of(Permission.VIEW_CATALOG, Permission.BORROW_BOOKS)
                : EnumSet.of(Permission.MANAGE_BOOKS, Permission.VIEW_CATALOG));
    }

    private static Book book(long id, String identifier, String title, String author, long categoryId,
                             int totalCopies, int availableCopies, BookStatus status) {
        Book book = new Book();
        book.setId(id);
        book.setIdentifier(identifier);
        book.setTitle(title);
        book.setAuthor(author);
        book.setCategoryId(categoryId);
        book.setTotalCopies(totalCopies);
        book.setAvailableCopies(availableCopies);
        book.setStatus(status);
        return book;
    }

    private static BookCategory category(long id, String name, String description) {
        BookCategory category = new BookCategory();
        category.setId(id);
        category.setName(name);
        category.setDescription(description);
        return category;
    }

    private static void require(boolean condition, String message) {
        if (!condition) {
            throw new AssertionError(message);
        }
    }

    private static final class InMemoryBookDao implements BookDao {
        private final Map<Long, Book> books = new LinkedHashMap<>();
        private final Map<Long, BookCategory> categories = new LinkedHashMap<>();
        private long nextId = 1L;
        private long nextCategoryId = 2L;

        private InMemoryBookDao() {
            categories.put(1L, category(1L, "Computer Science", "Programming books"));
        }

        @Override
        public List<BookCategory> findAllCategories() {
            List<BookCategory> results = new ArrayList<>();
            for (BookCategory category : categories.values()) {
                results.add(copy(category));
            }
            return results;
        }

        @Override
        public Optional<BookCategory> findCategoryById(long id) {
            return Optional.ofNullable(categories.get(id)).map(this::copy);
        }

        @Override
        public Optional<BookCategory> findCategoryByName(String name) {
            for (BookCategory category : categories.values()) {
                if (category.getName().equals(name)) {
                    return Optional.of(copy(category));
                }
            }
            return Optional.empty();
        }

        @Override
        public long createCategory(BookCategory category) {
            long id = nextCategoryId++;
            BookCategory stored = copy(category);
            stored.setId(id);
            categories.put(id, stored);
            return id;
        }

        @Override
        public boolean updateCategory(BookCategory category) {
            if (!categories.containsKey(category.getId())) {
                return false;
            }
            categories.put(category.getId(), copy(category));
            return true;
        }

        @Override
        public boolean deleteCategory(long id) {
            return categories.remove(id) != null;
        }

        @Override
        public int countBooksByCategoryId(long categoryId) {
            int count = 0;
            for (Book book : books.values()) {
                if (book.getCategoryId() == categoryId) {
                    count++;
                }
            }
            return count;
        }

        @Override
        public List<Book> search(BookSearchCriteria criteria) {
            List<Book> matches = new ArrayList<>();
            for (Book book : books.values()) {
                if (matches(criteria.getIdentifier(), book.getIdentifier())
                    && matches(criteria.getTitle(), book.getTitle())
                    && matches(criteria.getAuthor(), book.getAuthor())
                    && (criteria.getCategoryId() == null || criteria.getCategoryId() == book.getCategoryId())) {
                    matches.add(copy(book));
                }
            }
            return matches;
        }

        @Override
        public Optional<Book> findById(long id) {
            return Optional.ofNullable(books.get(id)).map(this::copy);
        }

        @Override
        public Optional<Book> findByIdentifier(String identifier) {
            for (Book book : books.values()) {
                if (book.getIdentifier().equals(identifier)) {
                    return Optional.of(copy(book));
                }
            }
            return Optional.empty();
        }

        @Override
        public long create(Book book) {
            long id = nextId++;
            Book stored = copy(book);
            stored.setId(id);
            books.put(id, stored);
            return id;
        }

        @Override
        public boolean update(Book book) {
            if (!books.containsKey(book.getId())) {
                return false;
            }
            books.put(book.getId(), copy(book));
            return true;
        }

        @Override
        public boolean delete(long id) {
            return books.remove(id) != null;
        }

        private boolean matches(String filter, String value) {
            return filter == null || filter.isEmpty() || value.contains(filter);
        }

        private Book copy(Book source) {
            Book copy = book(source.getId(), source.getIdentifier(), source.getTitle(), source.getAuthor(),
                source.getCategoryId(), source.getTotalCopies(), source.getAvailableCopies(), source.getStatus());
            copy.setCategoryName(source.getCategoryName());
            return copy;
        }

        private BookCategory copy(BookCategory source) {
            return category(source.getId(), source.getName(), source.getDescription());
        }
    }

    private static final class FailingBookDao implements BookDao {
        @Override
        public List<BookCategory> findAllCategories() {
            throw new DaoException("Simulated category failure", null);
        }

        @Override
        public Optional<BookCategory> findCategoryById(long id) {
            throw new DaoException("Simulated category find failure", null);
        }

        @Override
        public Optional<BookCategory> findCategoryByName(String name) {
            throw new DaoException("Simulated category find failure", null);
        }

        @Override
        public long createCategory(BookCategory category) {
            throw new DaoException("Simulated category create failure", null);
        }

        @Override
        public boolean updateCategory(BookCategory category) {
            throw new DaoException("Simulated category update failure", null);
        }

        @Override
        public boolean deleteCategory(long id) {
            throw new DaoException("Simulated category delete failure", null);
        }

        @Override
        public int countBooksByCategoryId(long categoryId) {
            throw new DaoException("Simulated category count failure", null);
        }

        @Override
        public List<Book> search(BookSearchCriteria criteria) {
            throw new DaoException("Simulated search failure", null);
        }

        @Override
        public Optional<Book> findById(long id) {
            throw new DaoException("Simulated find failure", null);
        }

        @Override
        public Optional<Book> findByIdentifier(String identifier) {
            throw new DaoException("Simulated find failure", null);
        }

        @Override
        public long create(Book book) {
            throw new DaoException("Simulated create failure", null);
        }

        @Override
        public boolean update(Book book) {
            throw new DaoException("Simulated update failure", null);
        }

        @Override
        public boolean delete(long id) {
            throw new DaoException("Simulated delete failure", null);
        }
    }
}
