InventoryItemServiceImpl.java

package com.smartsupplypro.inventory.service.impl;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.smartsupplypro.inventory.dto.InventoryItemDTO;
import com.smartsupplypro.inventory.enums.StockChangeReason;
import com.smartsupplypro.inventory.mapper.InventoryItemMapper;
import com.smartsupplypro.inventory.model.InventoryItem;
import com.smartsupplypro.inventory.repository.InventoryItemRepository;
import com.smartsupplypro.inventory.repository.SupplierRepository;
import com.smartsupplypro.inventory.service.InventoryItemService;
import com.smartsupplypro.inventory.service.StockHistoryService;
import com.smartsupplypro.inventory.validation.InventoryItemSecurityValidator;
import com.smartsupplypro.inventory.validation.InventoryItemValidator;
import static com.smartsupplypro.inventory.validation.InventoryItemValidator.assertFinalQuantityNonNegative;
import static com.smartsupplypro.inventory.validation.InventoryItemValidator.assertPriceValid;

/**
 * Service implementation for inventory item lifecycle management with audit trails.
 *
 * <p><strong>Characteristics</strong>:
 * <ul>
 *   <li><strong>CRUD Operations</strong>: Create, read, update, delete with validation</li>
 *   <li><strong>Audit Trail Integration</strong>: Every change logged via {@link StockHistoryService}</li>
 *   <li><strong>Validation Delegation</strong>: {@link InventoryItemValidator} + {@link InventoryItemSecurityValidator}</li>
 *   <li><strong>Security Context</strong>: Authenticated user tracking for audit compliance</li>
 *   <li><strong>Stock Adjustments</strong>: Quantity changes with reason tracking</li>
 *   <li><strong>Price Management</strong>: Separate price updates (WAC compatibility)</li>
 * </ul>
 *
 * <p><strong>Key Patterns</strong>:
 * <ul>
 *   <li>Every mutation → stock history log (quantity delta + reason + user + price snapshot)</li>
 *   <li>Transactional atomicity (item change + audit log in single transaction)</li>
 *   <li>Layered validation (business rules + security permissions)</li>
 *   <li>Supplier existence validation before item creation/update</li>
 * </ul>
 *
 * <p><strong>Business Rules</strong>:
 * <ul>
 *   <li>Uniqueness: No duplicate name+price combinations</li>
 *   <li>Positive prices: Unit price must be > 0</li>
 *   <li>Non-negative quantities: Final quantity cannot be negative after adjustment</li>
 *   <li>Supplier validation: Referenced supplier must exist</li>
 * </ul>
 *
 * <p><strong>Transaction Management</strong>:
 * Write operations use {@code @Transactional}, read operations use {@code @Transactional(readOnly = true)}.
 *
 * <p><strong>Architecture Documentation</strong>:
 * For detailed operation flows, audit trail patterns, security integration, and refactoring notes, see:
 * <a href="../../../../../../docs/architecture/services/inventory-item-service.md">Inventory Item Service Architecture</a>
 *
 * @see InventoryItemService
 * @see StockHistoryService
 * @see InventoryItemValidator
 * @see InventoryItemSecurityValidator
 */
@Service
public class InventoryItemServiceImpl implements InventoryItemService {

    private final InventoryItemRepository repository;
    private final StockHistoryService stockHistoryService;
    private final SupplierRepository supplierRepository;

    /**
     * Constructor with dependency injection.
     *
     * @param repository the inventory item repository for database operations
     * @param stockHistoryService the audit trail logging service
     * @param supplierRepository the supplier repository for validation
     */
    public InventoryItemServiceImpl(
            InventoryItemRepository repository,
            StockHistoryService stockHistoryService,
            SupplierRepository supplierRepository
    ) {
        this.repository = repository;
        this.stockHistoryService = stockHistoryService;
        this.supplierRepository = supplierRepository;
    }

    /**
     * {@inheritDoc}
     * 
     * <p><strong>Performance Warning</strong>: Loads ALL items. Use pagination for large datasets.
     *
     * @return list of all inventory items as DTOs
     */
    @Override
    public List<InventoryItemDTO> getAll() {
        return repository.findAll().stream().map(InventoryItemMapper::toDTO).toList();
    }

    /**
     * {@inheritDoc}
     * 
     * @param id the unique identifier of the inventory item
     * @return Optional containing the item DTO if found, empty otherwise
     */
    @Override
    public Optional<InventoryItemDTO> getById(String id) {
        return repository.findById(id).map(InventoryItemMapper::toDTO);
    }

    /**
     * {@inheritDoc}
     *
     * @param name the search term for item name (partial match supported)
     * @param pageable pagination and sorting parameters
     * @return paginated results sorted by price, empty page if no matches
     */
    @Override
    public Page<InventoryItemDTO> findByNameSortedByPrice(String name, Pageable pageable) {
        Page<InventoryItem> page = repository.findByNameSortedByPrice(name, pageable);
        return page == null ? Page.empty() : page.map(InventoryItemMapper::toDTO);
    }

    /**
     * {@inheritDoc}
     *
     * @return total count of inventory items in the database
     */
    @Override
    @Transactional(readOnly = true)
    public long countItems() {
        return repository.count();
    }

    /**
     * Creates a new inventory item with validation and audit trail initialization.
     * 
     * <p><strong>Operation Flow</strong>: Validates DTO → checks uniqueness → validates supplier → 
     * generates server fields → persists entity → logs INITIAL_STOCK history.
     * 
     * <p><strong>Key Rules</strong>:
     * <ul>
     *   <li>Uniqueness: No duplicate (name, price) combinations</li>
     *   <li>Price validation: Must be positive (price > 0)</li>
     *   <li>Supplier validation: Must exist before item creation</li>
     *   <li>Minimum quantity default: 10 if not provided or ≤ 0</li>
     *   <li>Authoritative createdBy: Always from SecurityContext</li>
     * </ul>
     * 
     * <p><strong>Audit Trail</strong>: Creates {@code StockHistory} entry with reason=INITIAL_STOCK, 
     * quantityChange=initial quantity, user=authenticated username, price=current unit price.
     *
     * @param dto the inventory item data transfer object (client-provided)
     * @return the saved inventory item as DTO with server-generated fields
     * @throws IllegalArgumentException if validation fails (duplicate, invalid price, missing supplier)
     */
    @Override
    @Transactional
    public InventoryItemDTO save(InventoryItemDTO dto) {
        // ===== STEP 1: Populate createdBy from authenticated user =====
        // Ensure createdBy is populated from authenticated user before validation
        if (dto.getCreatedBy() == null || dto.getCreatedBy().trim().isEmpty()) {
            dto.setCreatedBy(currentUsername());
        }
        
        // ===== STEP 2: Validate DTO fields =====
        // Checks: non-null name, price > 0, quantity >= 0, supplierId exists
        InventoryItemValidator.validateBase(dto);
        
        // ===== STEP 3: Check uniqueness (name + price) =====
        // Business rule: No two items can have same name and price
        InventoryItemValidator.validateInventoryItemNotExists(dto.getName(), dto.getPrice(), repository);
        
        // ===== STEP 4: Validate supplier exists =====
        // Foreign key integrity: Supplier must exist before item creation
        validateSupplierExists(dto.getSupplierId());

        // ===== STEP 5: Convert DTO to entity =====
        InventoryItem entity = InventoryItemMapper.toEntity(dto);

        // ===== STEP 6: Generate server-side fields (authoritative source) =====
        // ID: Generate UUID if not provided
        if (entity.getId() == null || entity.getId().isBlank()) {
            entity.setId(UUID.randomUUID().toString());
        }
        
        // createdBy: Always set from SecurityContext (ignore client-provided value)
        entity.setCreatedBy(currentUsername());
        
        // createdAt: Set to current timestamp if not already set
        if (entity.getCreatedAt() == null) {
            entity.setCreatedAt(LocalDateTime.now());
        }
        
        // ===== STEP 7: Apply default minimum quantity =====
        // Business rule: Default to 10 if not provided or invalid
        if (entity.getMinimumQuantity() <= 0) {
            entity.setMinimumQuantity(10);
        }

        // ===== STEP 8: Persist entity to database =====
        InventoryItem saved = repository.save(entity);

        // ===== STEP 9: Log INITIAL_STOCK history entry =====
        // Audit trail: Record baseline quantity and price for future WAC calculations
        // This establishes the starting point for all stock movements
        stockHistoryService.logStockChange(
                saved.getId(),
                saved.getQuantity(),              // Initial quantity (positive number)
                StockChangeReason.INITIAL_STOCK,  // Special reason for first entry
                currentUsername(),                // Who created this item
                saved.getPrice()                  // Price snapshot at creation
        );

        // ===== STEP 10: Return saved entity as DTO =====
        return InventoryItemMapper.toDTO(saved);
    }

    /**
     * Updates an existing inventory item with validation, security checks, and audit trail.
     * 
     * <p><strong>Key Rules</strong>:
     * <ul>
     *   <li>Security validation: User permissions checked before update</li>
     *   <li>Uniqueness check: If name OR price changed, verify no duplicate (name, price)</li>
     *   <li>Immutable createdBy: Original creator never overwritten</li>
     *   <li>Conditional audit: Stock history only logged if quantity changed (delta ≠ 0)</li>
     *   <li>Price validation: If price changed, must be positive</li>
     * </ul>
     * 
     * <p><strong>Audit Trail Behavior</strong>:
     * <ul>
     *   <li><strong>Quantity Changed</strong>: Logs MANUAL_UPDATE with quantityDelta and price snapshot</li>
     *   <li><strong>Quantity Unchanged</strong>: No stock history entry (no movement to audit)</li>
     * </ul>
     *
     * @param id the unique identifier of the item to update
     * @param dto the updated inventory item data
     * @return Optional containing updated item DTO (always present if no exception thrown)
     * @throws IllegalArgumentException if validation fails (item not found, duplicate, invalid supplier)
     */
    @Override
    @Transactional
    public Optional<InventoryItemDTO> update(String id, InventoryItemDTO dto) {
        // ===== STEP 1: Validate DTO fields =====
        InventoryItemValidator.validateBase(dto);
        
        // ===== STEP 2: Validate supplier exists =====
        validateSupplierExists(dto.getSupplierId());

        // ===== STEP 3: Verify item exists and retrieve =====
        InventoryItem existing = InventoryItemValidator.validateExists(id, repository);

        // ===== STEP 4: Check user permissions =====
        // Security validation: Ensure user can modify this item
        InventoryItemSecurityValidator.validateUpdatePermissions(existing, dto);

        // ===== STEP 5: Detect name/price changes for uniqueness check =====
        // If name OR price changed, check if new combination conflicts with another item
        boolean nameChanged  = !existing.getName().equalsIgnoreCase(dto.getName());
        boolean priceChanged = !existing.getPrice().equals(dto.getPrice());
        if (nameChanged || priceChanged) {
            // Uniqueness check: (newName, newPrice) must not exist for a DIFFERENT item
            InventoryItemValidator.validateInventoryItemNotExists(id, dto.getName(), dto.getPrice(), repository);
        }

        // ===== STEP 6: Calculate quantity delta for audit trail =====
        // Positive = stock increase, Negative = stock decrease, Zero = no stock movement
        int quantityDiff = dto.getQuantity() - existing.getQuantity();

        // ===== STEP 7: Update entity fields =====
        existing.setName(dto.getName());
        existing.setQuantity(dto.getQuantity());
        existing.setSupplierId(dto.getSupplierId());
        
        // Only update minimum quantity if new value is valid (> 0)
        if (dto.getMinimumQuantity() > 0) {
            existing.setMinimumQuantity(dto.getMinimumQuantity());
        }
        
        // If price changed, validate and update
        if (priceChanged) {
            assertPriceValid(dto.getPrice());
            existing.setPrice(dto.getPrice());
        }
        
        // ===== CRITICAL: DO NOT overwrite createdBy on update =====
        // Original creator must remain immutable for audit compliance
        // existing.setCreatedBy(...); // NEVER DO THIS

        // ===== STEP 8: Persist changes to database =====
        InventoryItem updated = repository.save(existing);

        // ===== STEP 9: Log stock history if quantity changed =====
        // Only create audit trail entry if there was actual stock movement
        if (quantityDiff != 0) {
            stockHistoryService.logStockChange(
                    updated.getId(),
                    quantityDiff,                     // Positive or negative delta
                    StockChangeReason.MANUAL_UPDATE,  // Generic update reason
                    currentUsername(),                // Who made the change
                    updated.getPrice()                // Price snapshot (possibly updated)
            );
        }
        // Note: If quantity unchanged, NO stock history entry is created
        // For price-only updates with PRICE_CHANGE logging, use updatePrice() instead

        // ===== STEP 10: Return updated entity as DTO =====
        return Optional.of(InventoryItemMapper.toDTO(updated));
    }

    /**
     * Deletes an inventory item after logging complete stock removal to audit trail.
     * 
     * <p><strong>Allowed Deletion Reasons</strong>: SCRAPPED, DESTROYED, DAMAGED, EXPIRED, LOST, RETURNED_TO_SUPPLIER.
     * 
     * <p><strong>Audit Trail</strong>: Before deletion, creates StockHistory entry with 
     * quantityChange=-currentQuantity, reason=provided reason, price=current price, user=authenticated username.
     * 
     * <p><strong>Hard Delete</strong>: Item physically removed from database (not soft delete with flag).
     *
     * @param id the unique identifier of the item to delete
     * @param reason the business reason for deletion (must be one of the allowed reasons)
     * @throws IllegalArgumentException if reason is invalid or item not found
     */
    @Override
    @Transactional
    public void delete(String id, StockChangeReason reason) {
        // ===== STEP 1: Validate deletion reason =====
        // Only specific reasons allowed for complete item deletion
        if (reason != StockChangeReason.SCRAPPED &&
            reason != StockChangeReason.DESTROYED &&
            reason != StockChangeReason.DAMAGED &&
            reason != StockChangeReason.EXPIRED &&
            reason != StockChangeReason.LOST &&
            reason != StockChangeReason.RETURNED_TO_SUPPLIER) {
            throw new IllegalArgumentException("Invalid reason for deletion");
        }

        // ===== STEP 2: Verify item exists =====
        InventoryItem item = repository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Item not found"));

        // ===== STEP 3: Log full stock removal to audit trail =====
        // Record negative adjustment (full quantity) with price snapshot
        // This preserves audit trail even though item will be deleted
        stockHistoryService.logStockChange(
                item.getId(),
                -item.getQuantity(),              // Full removal (negative)
                reason,                           // Specific deletion reason
                currentUsername(),                // Who initiated deletion
                item.getPrice()                   // Price snapshot for financial records
        );

        // ===== STEP 4: Perform hard delete =====
        // Physical removal from database (no soft delete flag)
        repository.deleteById(id);
        // Note: Stock history remains as permanent audit record
    }

    /**
     * Adjusts inventory quantity by a delta (positive for stock-in, negative for stock-out).
     * 
     * <p><strong>Key Rules</strong>:
     * <ul>
     *   <li>Final quantity cannot be negative (prevents overselling)</li>
     *   <li>Positive delta: Stock increase (purchases, returns from customers)</li>
     *   <li>Negative delta: Stock decrease (sales, damages, losses)</li>
     *   <li>Zero delta: Allowed but creates no-op history entry</li>
     * </ul>
     * 
     * <p><strong>Audit Trail</strong>: Creates StockHistory entry with quantityChange=delta, 
     * reason=provided reason, price=current unit price, user=authenticated username.
     *
     * @param id the unique identifier of the item to adjust
     * @param delta the quantity change (positive = increase, negative = decrease)
     * @param reason the business reason for the adjustment (determines financial categorization)
     * @return the updated inventory item as DTO
     * @throws IllegalArgumentException if item not found or final quantity would be negative
     */
    @Override
    @Transactional
    public InventoryItemDTO adjustQuantity(String id, int delta, StockChangeReason reason) {
        // ===== STEP 1: Verify item exists =====
        InventoryItem item = repository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Item not found"));

        // ===== STEP 2: Calculate new quantity =====
        int newQty = item.getQuantity() + delta;
        
        // ===== STEP 3: Validate non-negative quantity =====
        // Business rule: Cannot reduce stock below zero
        assertFinalQuantityNonNegative(newQty);

        // ===== STEP 4: Update item quantity =====
        item.setQuantity(newQty);
        
        // ===== STEP 5: Persist to database =====
        InventoryItem saved = repository.save(item);

        // ===== STEP 6: Log stock history =====
        // Record the adjustment with reason and price snapshot
        stockHistoryService.logStockChange(
                saved.getId(),
                delta,                     // Exact delta (positive or negative)
                reason,                    // Business reason (PURCHASE, SALE, etc.)
                currentUsername(),         // Who made the adjustment
                saved.getPrice()           // Price snapshot (for WAC/COGS calculations)
        );

        // ===== STEP 7: Return updated entity as DTO =====
        return InventoryItemMapper.toDTO(saved);
    }

    /**
     * Updates the unit price of an inventory item and logs a PRICE_CHANGE history entry.
     * 
     * <p><strong>Key Distinction</strong>: Price change vs quantity change:
     * <ul>
     *   <li><strong>updatePrice()</strong>: Changes price only, logs PRICE_CHANGE with delta=0, NO WAC impact</li>
     *   <li><strong>adjustQuantity()</strong>: Changes quantity, logs with actual delta, triggers WAC recalculation</li>
     * </ul>
     * 
     * <p><strong>Financial Impact</strong>: Existing inventory NOT revalued (keeps old cost). 
     * New price applies to future purchases only.
     * 
     * <p><strong>Audit Trail</strong>: Creates StockHistory entry with quantityChange=0, 
     * reason=PRICE_CHANGE, price=newPrice, user=authenticated username.
     *
     * @param id the unique identifier of the item
     * @param newPrice the new unit price (must be > 0)
     * @return the updated inventory item as DTO
     * @throws IllegalArgumentException if newPrice ≤ 0 or item not found
     */
    @Override
    @Transactional
    public InventoryItemDTO updatePrice(String id, BigDecimal newPrice) {
        // ===== STEP 1: Validate price is positive =====
        assertPriceValid(newPrice);

        // ===== STEP 2: Verify item exists =====
        InventoryItem item = repository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Item not found: " + id));
        
        // ===== STEP 3: Update item price =====
        item.setPrice(newPrice);
        
        // ===== STEP 4: Persist to database =====
        InventoryItem saved = repository.save(item);

        // ===== STEP 5: Log PRICE_CHANGE history =====
        // Record price change with delta=0 (no quantity movement)
        // This preserves price history timeline for analytics
        stockHistoryService.logStockChange(
                id,
                0,                             // No quantity change
                StockChangeReason.PRICE_CHANGE, // Special reason for price-only updates
                currentUsername(),             // Who changed the price
                newPrice                       // New price snapshot
        );
        
        // ===== STEP 6: Return updated entity as DTO =====
        return InventoryItemMapper.toDTO(saved);
    }

    // ==================================================================================
    // Helper Methods
    // ==================================================================================

    /**
     * Validates that the specified supplier exists in the database.
     * 
     * <p>Ensures inventory items reference valid suppliers for procurement workflows, 
     * analytics aggregation, and audit trail integrity.
     *
     * @param supplierId the supplier identifier to validate
     * @throws IllegalArgumentException if supplier does not exist in database
     */
    private void validateSupplierExists(String supplierId) {
        if (!supplierRepository.existsById(supplierId)) {
            throw new IllegalArgumentException("Supplier does not exist");
        }
    }

    /**
     * Retrieves the current authenticated username from Spring Security context.
     * 
     * <p><strong>Behavior</strong>: Returns Authentication.getName() if authenticated, 
     * otherwise "system" (for background jobs, scheduled tasks, test fixtures).
     * 
     * <p><strong>Security Pattern</strong>: Used for audit trail compliance - 
     * populates createdBy fields and stock history user tracking.
     *
     * @return the authenticated username, or "system" if no authentication present
     */
    private String currentUsername() {
        Authentication a = SecurityContextHolder.getContext() != null
                ? SecurityContextHolder.getContext().getAuthentication() : null;
        return a != null ? a.getName() : "system";
    }
}