package com.mzh.library.service;

import com.mzh.library.dao.ReaderDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Permission;
import com.mzh.library.entity.Reader;
import com.mzh.library.entity.ReaderSearchCriteria;
import com.mzh.library.entity.ReaderStatus;
import com.mzh.library.entity.Role;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.impl.ReaderServiceImpl;

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 ReaderServiceCheck {
    private static final String UNAVAILABLE_MESSAGE =
        "读者服务暂时不可用，请稍后重试。";

    private ReaderServiceCheck() {
    }

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

        InMemoryReaderDao dao = new InMemoryReaderDao();
        ReaderService service = new ReaderServiceImpl(dao);
        AuthenticatedUser librarian = user(10L, Role.LIBRARIAN);
        AuthenticatedUser readerUser = user(20L, Role.READER);

        ServiceResult<Long> invalidContact = service.createReader(librarian,
            reader(0L, "RD-1000", null, "Invalid Contact", "bad", "", ReaderStatus.ACTIVE, 5));
        require(!invalidContact.isSuccessful(), "invalid contact should fail");
        require(invalidContact.getErrors().containsKey("phone"), "invalid phone should target phone field");

        ServiceResult<Long> symbolsOnlyPhone = service.createReader(librarian,
            reader(0L, "RD-1009", null, "Symbols Only Phone", "------", "", ReaderStatus.ACTIVE, 5));
        require(!symbolsOnlyPhone.isSuccessful(), "phone without digits should fail");
        require(symbolsOnlyPhone.getErrors().containsKey("phone"), "phone without digits should target phone field");

        ServiceResult<Long> missingContact = service.createReader(librarian,
            reader(0L, "RD-1008", null, "Missing Contact", "", "", ReaderStatus.ACTIVE, 5));
        require(!missingContact.isSuccessful(), "missing contact should fail");
        require(missingContact.getErrors().containsKey("phone"), "missing contact should target phone field");

        ServiceResult<Long> invalidBorrowLimit = service.createReader(librarian,
            reader(0L, "RD-1001", null, "Invalid Limit", "13800000001", "", ReaderStatus.ACTIVE, 0));
        require(!invalidBorrowLimit.isSuccessful(), "invalid borrow limit should fail");
        require(invalidBorrowLimit.getErrors().containsKey("maxBorrowCount"),
            "invalid borrow limit should target maxBorrowCount field");

        ServiceResult<Long> invalidUserId = service.createReader(librarian,
            reader(0L, "RD-1002", -1L, "Invalid User", "13800000002", "", ReaderStatus.ACTIVE, 5));
        require(!invalidUserId.isSuccessful(), "invalid linked account should fail");
        require(invalidUserId.getErrors().containsKey("userId"),
            "invalid linked account should target userId field");

        ServiceResult<Long> denied = service.createReader(readerUser,
            reader(0L, "RD-1006", null, "Reader Write", "13800000007", "", ReaderStatus.ACTIVE, 5));
        require(!denied.isSuccessful(), "reader write should fail");
        require("您无权管理读者。".equals(denied.getMessage()),
            "reader write should return permission message");

        ServiceResult<Long> created = service.createReader(librarian,
            reader(0L, "RD-1003", 30L, "Service Reader", "13800000003",
                "service.reader@example.com", ReaderStatus.ACTIVE, 5));
        require(created.isSuccessful(), "librarian should create a valid reader");
        long createdId = created.getData();

        ServiceResult<Long> duplicateIdentifier = service.createReader(librarian,
            reader(0L, "RD-1003", null, "Duplicate Identifier", "13800000004", "", ReaderStatus.ACTIVE, 5));
        require(!duplicateIdentifier.isSuccessful(), "duplicate reader identifier should fail");
        require(duplicateIdentifier.getErrors().containsKey("identifier"),
            "duplicate identifier should target identifier field");

        ServiceResult<Long> duplicateUser = service.createReader(librarian,
            reader(0L, "RD-1004", 30L, "Duplicate User", "13800000005", "", ReaderStatus.ACTIVE, 5));
        require(!duplicateUser.isSuccessful(), "duplicate linked account should fail");
        require(duplicateUser.getErrors().containsKey("userId"), "duplicate linked account should target userId field");

        ServiceResult<Void> updated = service.updateReader(librarian,
            reader(createdId, "RD-1005", 30L, "Updated Reader", "13800000006",
                "updated.reader@example.com", ReaderStatus.SUSPENDED, 3));
        require(updated.isSuccessful(), "librarian should update a valid reader");
        require(dao.findById(createdId).get().getStatus() == ReaderStatus.SUSPENDED,
            "update should persist reader status");

        ServiceResult<List<Reader>> search = service.searchReaders(
            new ReaderSearchCriteria("RD-1005", "Updated", "updated.reader", "suspended"));
        require(search.isSuccessful(), "search should succeed");
        require(search.getData().size() == 1, "search should find updated reader by filters");

        ServiceResult<List<Reader>> invalidSearch = service.searchReaders(
            new ReaderSearchCriteria("", "", "", "missing"));
        require(!invalidSearch.isSuccessful(), "invalid search status should fail");
        require(invalidSearch.getErrors().containsKey("status"), "invalid search status should target status field");

        ServiceResult<Void> deactivated = service.deactivateReader(librarian, createdId);
        require(deactivated.isSuccessful(), "librarian should deactivate a reader");
        require(dao.findById(createdId).get().getStatus() == ReaderStatus.INACTIVE,
            "deactivate should mark the reader inactive");

        ReaderService failingService = new ReaderServiceImpl(new FailingReaderDao());
        ServiceResult<List<Reader>> unavailable = failingService.searchReaders(new ReaderSearchCriteria());
        require(!unavailable.isSuccessful(), "DAO failure should not escape service");
        require(UNAVAILABLE_MESSAGE.equals(unavailable.getMessage()), "search DAO failure should map to safe message");

        ServiceResult<Long> unavailableCreate = failingService.createReader(librarian,
            reader(0L, "RD-1007", null, "Unavailable Create", "13800000008", "", ReaderStatus.ACTIVE, 5));
        require(!unavailableCreate.isSuccessful(), "create DAO failure should not escape service");
        require(UNAVAILABLE_MESSAGE.equals(unavailableCreate.getMessage()),
            "create DAO failure should map to safe message");

        ServiceResult<Void> unavailableUpdate = failingService.updateReader(librarian,
            reader(1L, "RD-1007", null, "Unavailable Update", "13800000009", "", ReaderStatus.ACTIVE, 5));
        require(!unavailableUpdate.isSuccessful(), "update DAO failure should not escape service");
        require(UNAVAILABLE_MESSAGE.equals(unavailableUpdate.getMessage()),
            "update DAO failure should map to safe message");

        ServiceResult<Void> unavailableDeactivate = failingService.deactivateReader(librarian, 1L);
        require(!unavailableDeactivate.isSuccessful(), "deactivate DAO failure should not escape service");
        require(UNAVAILABLE_MESSAGE.equals(unavailableDeactivate.getMessage()),
            "deactivate 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.MANAGE_READERS, Permission.VIEW_CATALOG));
    }

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

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

    private static final class InMemoryReaderDao implements ReaderDao {
        private final Map<Long, Reader> readers = new LinkedHashMap<>();
        private long nextId = 1L;

        @Override
        public List<Reader> search(ReaderSearchCriteria criteria) {
            List<Reader> matches = new ArrayList<>();
            for (Reader reader : readers.values()) {
                if (matches(criteria.getIdentifier(), reader.getIdentifier())
                    && matches(criteria.getName(), reader.getFullName())
                    && contactMatches(criteria.getContact(), reader)
                    && (criteria.getStatusCode() == null || criteria.getStatusCode().isEmpty()
                        || reader.getStatus().getCode().equals(criteria.getStatusCode()))) {
                    matches.add(copy(reader));
                }
            }
            return matches;
        }

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

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

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

        @Override
        public long create(Reader reader) {
            long id = nextId++;
            Reader stored = copy(reader);
            stored.setId(id);
            readers.put(id, stored);
            return id;
        }

        @Override
        public boolean update(Reader reader) {
            if (!readers.containsKey(reader.getId())) {
                return false;
            }
            readers.put(reader.getId(), copy(reader));
            return true;
        }

        @Override
        public boolean deactivate(long id) {
            Reader reader = readers.get(id);
            if (reader == null) {
                return false;
            }
            reader.setStatus(ReaderStatus.INACTIVE);
            return true;
        }

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

        private boolean contactMatches(String filter, Reader reader) {
            if (filter == null || filter.isEmpty()) {
                return true;
            }
            return (reader.getPhone() != null && reader.getPhone().contains(filter))
                || (reader.getEmail() != null && reader.getEmail().contains(filter));
        }

        private Reader copy(Reader source) {
            Reader copy = reader(source.getId(), source.getIdentifier(), source.getUserId(), source.getFullName(),
                source.getPhone(), source.getEmail(), source.getStatus(), source.getMaxBorrowCount());
            copy.setUsername(source.getUsername());
            return copy;
        }
    }

    private static final class FailingReaderDao implements ReaderDao {
        @Override
        public List<Reader> search(ReaderSearchCriteria criteria) {
            throw new DaoException("Simulated search failure", null);
        }

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

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

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

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

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

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