package com.mzh.library.service;

import com.mzh.library.dao.BorrowRecordDao;
import com.mzh.library.entity.AuthenticatedUser;
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.Permission;
import com.mzh.library.entity.Reader;
import com.mzh.library.entity.ReaderStatus;
import com.mzh.library.entity.Role;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.impl.BorrowingServiceImpl;

import java.sql.Connection;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
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 BorrowingServiceCheck {
    private static final String UNAVAILABLE_MESSAGE =
        "借阅服务暂时不可用，请稍后重试。";
    private static final Clock FIXED_CLOCK = Clock.fixed(
        Instant.parse("2026-04-27T00:00:00Z"),
        ZoneId.of("UTC")
    );

    private BorrowingServiceCheck() {
    }

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

        InMemoryBorrowRecordDao dao = new InMemoryBorrowRecordDao();
        BorrowingService service = service(dao);
        AuthenticatedUser librarian = user(10L, Role.LIBRARIAN);
        AuthenticatedUser readerUser = user(20L, Role.READER);
        AuthenticatedUser administrator = user(30L, Role.ADMINISTRATOR);

        Reader activeReader = reader(1L, "RD-1000", 20L, "Active Reader", ReaderStatus.ACTIVE, 1);
        Reader suspendedReader = reader(2L, "RD-1001", null, "Suspended Reader", ReaderStatus.SUSPENDED, 3);
        Reader renewingReader = reader(3L, "RD-1002", null, "Renewing Reader", ReaderStatus.ACTIVE, 2);
        dao.putReader(activeReader);
        dao.putReader(suspendedReader);
        dao.putReader(renewingReader);
        dao.putBook(book(1L, "BK-1000", "Available Book", BookStatus.AVAILABLE, 2, 1));
        dao.putBook(book(2L, "BK-1001", "Unavailable Book", BookStatus.UNAVAILABLE, 1, 1));
        dao.putBook(book(3L, "BK-1002", "No Copies", BookStatus.AVAILABLE, 1, 0));
        dao.putBook(book(4L, "BK-1003", "Renewable Book", BookStatus.AVAILABLE, 1, 1));
        dao.putBook(book(5L, "BK-1004", "Limit Book", BookStatus.AVAILABLE, 1, 1));

        ServiceResult<Long> denied = service.borrowBook(readerUser, "RD-1000", "BK-1000");
        require(!denied.isSuccessful(), "reader should not manage borrow creation");
        require("您无权管理借阅。".equals(denied.getMessage()),
            "reader borrow creation should return permission message");

        ServiceResult<Long> suspended = service.borrowBook(librarian, "RD-1001", "BK-1000");
        require(!suspended.isSuccessful(), "suspended reader should not borrow");
        require(suspended.getErrors().containsKey("readerIdentifier"),
            "suspended reader failure should target readerIdentifier");

        ServiceResult<Long> unavailable = service.borrowBook(librarian, "RD-1000", "BK-1001");
        require(!unavailable.isSuccessful(), "unavailable book should not be borrowed");
        require(unavailable.getErrors().containsKey("bookIdentifier"),
            "unavailable book failure should target bookIdentifier");

        ServiceResult<Long> noCopies = service.borrowBook(librarian, "RD-1000", "BK-1002");
        require(!noCopies.isSuccessful(), "book with no copies should not be borrowed");
        require(noCopies.getErrors().containsKey("bookIdentifier"),
            "no-copy book failure should target bookIdentifier");

        ServiceResult<Long> borrowed = service.borrowBook(librarian, "RD-1000", "BK-1000");
        require(borrowed.isSuccessful(), "librarian should create a valid borrow record");
        long borrowedId = borrowed.getData();
        require(dao.books.get(1L).getAvailableCopies() == 0, "borrow should decrement available copies");

        ServiceResult<Long> overLimit = service.borrowBook(librarian, "RD-1000", "BK-1004");
        require(!overLimit.isSuccessful(), "reader at max active loans should be blocked");
        require(overLimit.getErrors().containsKey("readerIdentifier"),
            "borrow limit failure should target readerIdentifier");

        ServiceResult<List<BorrowRecord>> history = service.listCurrentReaderHistory(readerUser);
        require(history.isSuccessful(), "linked reader should view own loan history");
        require(history.getData().size() == 1, "reader history should include own record");

        ServiceResult<List<BorrowRecord>> staffHistory = service.listCurrentReaderHistory(administrator);
        require(!staffHistory.isSuccessful(), "staff should use management history, not reader loan history");
        require("您无权查看借阅历史。".equals(staffHistory.getMessage()),
            "staff reader-history access should return permission message");

        ServiceResult<Void> returned = service.returnBook(librarian, borrowedId);
        require(returned.isSuccessful(), "librarian should return an active loan");
        require(dao.records.get(borrowedId).getStatus() == BorrowRecordStatus.RETURNED,
            "return should mark record returned");
        require(dao.books.get(1L).getAvailableCopies() == 1, "return should restore available copies");

        ServiceResult<Long> renewable = service.borrowBook(librarian, "RD-1002", "BK-1003");
        require(renewable.isSuccessful(), "renewal test borrow should succeed");
        long renewableId = renewable.getData();
        LocalDateTime firstDueAt = dao.records.get(renewableId).getDueAt();

        ServiceResult<Void> renewed = service.renewLoan(librarian, renewableId);
        require(renewed.isSuccessful(), "first renewal should succeed");
        BorrowRecord renewedRecord = dao.records.get(renewableId);
        require(renewedRecord.getRenewalCount() == 1, "renew should increment renewal count");
        require(renewedRecord.getDueAt().equals(firstDueAt.plusDays(14)), "renew should extend due date");

        ServiceResult<Void> secondRenewal = service.renewLoan(librarian, renewableId);
        require(!secondRenewal.isSuccessful(), "second renewal should hit the explicit renewal limit");
        require(secondRenewal.getErrors().containsKey("renewalCount"),
            "renewal limit should target renewalCount");

        BorrowRecord overdue = record(90L, 3L, 5L, LocalDateTime.of(2000, 1, 1, 12, 0),
            LocalDateTime.of(2000, 1, 15, 12, 0), null, 0, BorrowRecordStatus.ACTIVE);
        dao.records.put(overdue.getId(), overdue);
        ServiceResult<List<BorrowRecord>> overdueSearch = service.searchRecords(librarian,
            new BorrowRecordSearchCriteria("", "", BorrowRecordSearchCriteria.OVERDUE_STATUS));
        require(overdueSearch.isSuccessful(), "overdue search should succeed");
        require(overdueSearch.getData().size() == 1, "overdue search should only return overdue active loans");

        AuthenticatedUser unlinkedReader = user(21L, Role.READER);
        ServiceResult<List<BorrowRecord>> unlinkedHistory = service.listCurrentReaderHistory(unlinkedReader);
        require(unlinkedHistory.isSuccessful(), "unlinked reader history should return a safe empty result");
        require(unlinkedHistory.getData().isEmpty(), "unlinked reader history should be empty");

        BorrowingService failingService = service(new FailingBorrowRecordDao());
        ServiceResult<List<BorrowRecord>> failingSearch = failingService.searchRecords(librarian,
            new BorrowRecordSearchCriteria());
        require(!failingSearch.isSuccessful(), "DAO failure should not escape search");
        require(UNAVAILABLE_MESSAGE.equals(failingSearch.getMessage()),
            "DAO failure should map to safe borrowing message");
    }

    private static BorrowingService service(BorrowRecordDao dao) {
        return new BorrowingServiceImpl(dao, new PermissionPolicy(), FIXED_CLOCK,
            new BorrowingServiceImpl.DirectTransactionExecutor());
    }

    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_BORROWING, Permission.VIEW_CATALOG));
    }

    private static Reader reader(long id, String identifier, Long userId, String fullName,
                                 ReaderStatus status, int maxBorrowCount) {
        Reader reader = new Reader();
        reader.setId(id);
        reader.setIdentifier(identifier);
        reader.setUserId(userId);
        reader.setFullName(fullName);
        reader.setStatus(status);
        reader.setMaxBorrowCount(maxBorrowCount);
        return reader;
    }

    private static Book book(long id, String identifier, String title, BookStatus status,
                             int totalCopies, int availableCopies) {
        Book book = new Book();
        book.setId(id);
        book.setIdentifier(identifier);
        book.setTitle(title);
        book.setAuthor("Test Author");
        book.setCategoryId(1L);
        book.setCategoryName("Test Category");
        book.setStatus(status);
        book.setTotalCopies(totalCopies);
        book.setAvailableCopies(availableCopies);
        return book;
    }

    private static BorrowRecord record(long id, long readerId, long bookId, LocalDateTime borrowedAt,
                                       LocalDateTime dueAt, LocalDateTime returnedAt, int renewalCount,
                                       BorrowRecordStatus status) {
        BorrowRecord record = new BorrowRecord();
        record.setId(id);
        record.setReaderId(readerId);
        record.setBookId(bookId);
        record.setBorrowedAt(borrowedAt);
        record.setDueAt(dueAt);
        record.setReturnedAt(returnedAt);
        record.setRenewalCount(renewalCount);
        record.setStatus(status);
        return record;
    }

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

    private static final class InMemoryBorrowRecordDao implements BorrowRecordDao {
        private final Map<Long, Reader> readers = new LinkedHashMap<>();
        private final Map<Long, Book> books = new LinkedHashMap<>();
        private final Map<Long, BorrowRecord> records = new LinkedHashMap<>();
        private long nextId = 1L;

        private void putReader(Reader reader) {
            readers.put(reader.getId(), reader);
        }

        private void putBook(Book book) {
            books.put(book.getId(), book);
        }

        @Override
        public List<BorrowRecord> search(BorrowRecordSearchCriteria criteria) {
            List<BorrowRecord> matches = new ArrayList<>();
            for (BorrowRecord record : records.values()) {
                BorrowRecord enriched = enrich(copy(record));
                if (matches(criteria.getReaderIdentifier(), enriched.getReaderIdentifier())
                    && matches(criteria.getBookIdentifier(), enriched.getBookIdentifier())
                    && statusMatches(criteria, enriched)) {
                    matches.add(enriched);
                }
            }
            return matches;
        }

        @Override
        public List<BorrowRecord> findByReaderId(long readerId) {
            List<BorrowRecord> matches = new ArrayList<>();
            for (BorrowRecord record : records.values()) {
                if (record.getReaderId() == readerId) {
                    matches.add(enrich(copy(record)));
                }
            }
            return matches;
        }

        @Override
        public Optional<Reader> findReaderByUserId(long userId) {
            for (Reader reader : readers.values()) {
                if (reader.getUserId() != null && reader.getUserId() == userId) {
                    return Optional.of(copy(reader));
                }
            }
            return Optional.empty();
        }

        @Override
        public Optional<Reader> findReaderByIdentifierForUpdate(Connection connection, String identifier) {
            for (Reader reader : readers.values()) {
                if (reader.getIdentifier().equals(identifier)) {
                    return Optional.of(copy(reader));
                }
            }
            return Optional.empty();
        }

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

        @Override
        public Optional<BorrowRecord> findByIdForUpdate(Connection connection, long id) {
            return Optional.ofNullable(records.get(id)).map(this::copy).map(this::enrich);
        }

        @Override
        public int countActiveByReaderId(Connection connection, long readerId) {
            int count = 0;
            for (BorrowRecord record : records.values()) {
                if (record.getReaderId() == readerId
                    && record.getStatus() == BorrowRecordStatus.ACTIVE
                    && record.getReturnedAt() == null) {
                    count++;
                }
            }
            return count;
        }

        @Override
        public long create(Connection connection, BorrowRecord record) {
            long id = nextId++;
            BorrowRecord stored = copy(record);
            stored.setId(id);
            records.put(id, stored);
            return id;
        }

        @Override
        public boolean decrementAvailableCopies(Connection connection, long bookId) {
            Book book = books.get(bookId);
            if (book == null || book.getAvailableCopies() <= 0) {
                return false;
            }
            book.setAvailableCopies(book.getAvailableCopies() - 1);
            return true;
        }

        @Override
        public boolean incrementAvailableCopies(Connection connection, long bookId) {
            Book book = books.get(bookId);
            if (book == null) {
                return false;
            }
            book.setAvailableCopies(Math.min(book.getAvailableCopies() + 1, book.getTotalCopies()));
            return true;
        }

        @Override
        public boolean markReturned(Connection connection, long id, LocalDateTime returnedAt) {
            BorrowRecord record = records.get(id);
            if (record == null || record.getStatus() != BorrowRecordStatus.ACTIVE || record.getReturnedAt() != null) {
                return false;
            }
            record.setStatus(BorrowRecordStatus.RETURNED);
            record.setReturnedAt(returnedAt);
            return true;
        }

        @Override
        public boolean renew(Connection connection, long id, LocalDateTime dueAt) {
            BorrowRecord record = records.get(id);
            if (record == null || record.getStatus() != BorrowRecordStatus.ACTIVE || record.getReturnedAt() != null) {
                return false;
            }
            record.setDueAt(dueAt);
            record.setRenewalCount(record.getRenewalCount() + 1);
            return true;
        }

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

        private boolean statusMatches(BorrowRecordSearchCriteria criteria, BorrowRecord record) {
            String statusCode = criteria.getStatusCode();
            if (statusCode == null || statusCode.isEmpty()) {
                return true;
            }
            if (criteria.isOverdueOnly()) {
                return record.isOverdue();
            }
            return record.getStatus().getCode().equals(statusCode);
        }

        private BorrowRecord enrich(BorrowRecord record) {
            Reader reader = readers.get(record.getReaderId());
            Book book = books.get(record.getBookId());
            if (reader != null) {
                record.setReaderIdentifier(reader.getIdentifier());
                record.setReaderName(reader.getFullName());
            }
            if (book != null) {
                record.setBookIdentifier(book.getIdentifier());
                record.setBookTitle(book.getTitle());
            }
            return record;
        }

        private Reader copy(Reader source) {
            return reader(source.getId(), source.getIdentifier(), source.getUserId(), source.getFullName(),
                source.getStatus(), source.getMaxBorrowCount());
        }

        private Book copy(Book source) {
            return book(source.getId(), source.getIdentifier(), source.getTitle(), source.getStatus(),
                source.getTotalCopies(), source.getAvailableCopies());
        }

        private BorrowRecord copy(BorrowRecord source) {
            BorrowRecord copy = record(source.getId(), source.getReaderId(), source.getBookId(),
                source.getBorrowedAt(), source.getDueAt(), source.getReturnedAt(), source.getRenewalCount(),
                source.getStatus());
            copy.setReaderIdentifier(source.getReaderIdentifier());
            copy.setReaderName(source.getReaderName());
            copy.setBookIdentifier(source.getBookIdentifier());
            copy.setBookTitle(source.getBookTitle());
            return copy;
        }
    }

    private static final class FailingBorrowRecordDao implements BorrowRecordDao {
        @Override
        public List<BorrowRecord> search(BorrowRecordSearchCriteria criteria) {
            throw new DaoException("Simulated search failure", null);
        }

        @Override
        public List<BorrowRecord> findByReaderId(long readerId) {
            throw new DaoException("Simulated history failure", null);
        }

        @Override
        public Optional<Reader> findReaderByUserId(long userId) {
            throw new DaoException("Simulated reader failure", null);
        }

        @Override
        public Optional<Reader> findReaderByIdentifierForUpdate(Connection connection, String identifier) {
            throw new DaoException("Simulated reader lock failure", null);
        }

        @Override
        public Optional<Book> findBookByIdentifierForUpdate(Connection connection, String identifier) {
            throw new DaoException("Simulated book lock failure", null);
        }

        @Override
        public Optional<BorrowRecord> findByIdForUpdate(Connection connection, long id) {
            throw new DaoException("Simulated record lock failure", null);
        }

        @Override
        public int countActiveByReaderId(Connection connection, long readerId) {
            throw new DaoException("Simulated count failure", null);
        }

        @Override
        public long create(Connection connection, BorrowRecord record) {
            throw new DaoException("Simulated create failure", null);
        }

        @Override
        public boolean decrementAvailableCopies(Connection connection, long bookId) {
            throw new DaoException("Simulated decrement failure", null);
        }

        @Override
        public boolean incrementAvailableCopies(Connection connection, long bookId) {
            throw new DaoException("Simulated increment failure", null);
        }

        @Override
        public boolean markReturned(Connection connection, long id, LocalDateTime returnedAt) {
            throw new DaoException("Simulated return failure", null);
        }

        @Override
        public boolean renew(Connection connection, long id, LocalDateTime dueAt) {
            throw new DaoException("Simulated renew failure", null);
        }
    }
}
