package de.effigenix.domain.inventory;

import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.Objects;

/**
 * StockMovement aggregate root – immutable after creation.
 *
 * Invariants:
 * - StockId, ArticleId, StockBatchId are required and non-blank
 * - BatchReference (batchId + batchType) is required
 * - MovementType is required
 * - Direction is derived from MovementType, except ADJUSTMENT where it must be provided
 * - Quantity must be positive
 * - WASTE and ADJUSTMENT require a non-empty reason
 * - INTER_BRANCH_TRANSFER requires a referenceDocumentId; always recorded as OUT
 *   (the receiving branch records a separate GOODS_RECEIPT movement)
 * - PerformedBy is required and non-blank
 */
public class StockMovement {

    private final StockMovementId id;
    private final StockId stockId;
    private final ArticleId articleId;
    private final StockBatchId stockBatchId;
    private final BatchReference batchReference;
    private final MovementType movementType;
    private final MovementDirection direction;
    private final Quantity quantity;
    private final String reason;
    private final String referenceDocumentId;
    private final String performedBy;
    private final Instant performedAt;

    private StockMovement(
            StockMovementId id,
            StockId stockId,
            ArticleId articleId,
            StockBatchId stockBatchId,
            BatchReference batchReference,
            MovementType movementType,
            MovementDirection direction,
            Quantity quantity,
            String reason,
            String referenceDocumentId,
            String performedBy,
            Instant performedAt
    ) {
        this.id = id;
        this.stockId = stockId;
        this.articleId = articleId;
        this.stockBatchId = stockBatchId;
        this.batchReference = batchReference;
        this.movementType = movementType;
        this.direction = direction;
        this.quantity = quantity;
        this.reason = reason;
        this.referenceDocumentId = referenceDocumentId;
        this.performedBy = performedBy;
        this.performedAt = performedAt;
    }

    /**
     * Factory: Erzeugt eine neue StockMovement aus rohen Eingaben.
     * Orchestriert Validierung aller VOs intern.
     */
    public static Result<StockMovementError, StockMovement> record(StockMovementDraft draft) {
        // 1. StockId validieren
        StockId stockId;
        try {
            stockId = StockId.of(draft.stockId());
        } catch (IllegalArgumentException e) {
            return Result.failure(new StockMovementError.InvalidStockId(e.getMessage()));
        }

        // 2. ArticleId validieren
        ArticleId articleId;
        try {
            articleId = ArticleId.of(draft.articleId());
        } catch (IllegalArgumentException e) {
            return Result.failure(new StockMovementError.InvalidArticleId(e.getMessage()));
        }

        // 3. StockBatchId validieren
        StockBatchId stockBatchId;
        try {
            stockBatchId = StockBatchId.of(draft.stockBatchId());
        } catch (IllegalArgumentException e) {
            return Result.failure(new StockMovementError.InvalidStockBatchId(e.getMessage()));
        }

        // 4. BatchReference validieren
        if (draft.batchId() == null || draft.batchId().isBlank()) {
            return Result.failure(new StockMovementError.InvalidBatchReference("batchId must not be blank"));
        }
        BatchType batchType;
        try {
            batchType = BatchType.valueOf(draft.batchType());
        } catch (IllegalArgumentException | NullPointerException e) {
            return Result.failure(new StockMovementError.InvalidBatchReference(
                    "Invalid batchType: " + draft.batchType() + ". Allowed values: PRODUCED, PURCHASED"));
        }
        var batchReference = new BatchReference(draft.batchId(), batchType);

        // 5. MovementType validieren
        MovementType movementType;
        try {
            movementType = MovementType.valueOf(draft.movementType());
        } catch (IllegalArgumentException | NullPointerException e) {
            return Result.failure(new StockMovementError.InvalidMovementType(
                    "Invalid movement type: " + draft.movementType()));
        }

        // 6. Direction ableiten oder validieren
        MovementDirection direction;
        if (movementType == MovementType.ADJUSTMENT) {
            if (draft.direction() == null || draft.direction().isBlank()) {
                return Result.failure(new StockMovementError.InvalidDirection(
                        "Direction is required for ADJUSTMENT movements"));
            }
            try {
                direction = MovementDirection.valueOf(draft.direction());
            } catch (IllegalArgumentException e) {
                return Result.failure(new StockMovementError.InvalidDirection(
                        "Invalid direction: " + draft.direction() + ". Allowed values: IN, OUT"));
            }
        } else {
            direction = deriveDirection(movementType);
        }

        // 7. Quantity validieren
        Quantity quantity;
        try {
            BigDecimal amount = new BigDecimal(draft.quantityAmount());
            UnitOfMeasure uom;
            try {
                uom = UnitOfMeasure.valueOf(draft.quantityUnit());
            } catch (IllegalArgumentException | NullPointerException e) {
                return Result.failure(new StockMovementError.InvalidQuantity("Invalid unit: " + draft.quantityUnit()));
            }
            switch (Quantity.of(amount, uom)) {
                case Result.Failure(var err) -> {
                    return Result.failure(new StockMovementError.InvalidQuantity(err.message()));
                }
                case Result.Success(var val) -> quantity = val;
            }
        } catch (NumberFormatException | NullPointerException e) {
            return Result.failure(new StockMovementError.InvalidQuantity(
                    "Invalid quantity amount: " + draft.quantityAmount()));
        }

        // 8. Reason-Pflicht bei WASTE und ADJUSTMENT
        if ((movementType == MovementType.WASTE || movementType == MovementType.ADJUSTMENT)
                && (draft.reason() == null || draft.reason().isBlank())) {
            return Result.failure(new StockMovementError.ReasonRequired(movementType.name()));
        }

        // 9. ReferenceDocumentId-Pflicht bei INTER_BRANCH_TRANSFER
        if (movementType == MovementType.INTER_BRANCH_TRANSFER
                && (draft.referenceDocumentId() == null || draft.referenceDocumentId().isBlank())) {
            return Result.failure(new StockMovementError.ReferenceDocumentRequired(movementType.name()));
        }

        // 10. PerformedBy validieren
        if (draft.performedBy() == null || draft.performedBy().isBlank()) {
            return Result.failure(new StockMovementError.InvalidPerformedBy("must not be blank"));
        }

        return Result.success(new StockMovement(
                StockMovementId.generate(),
                stockId,
                articleId,
                stockBatchId,
                batchReference,
                movementType,
                direction,
                quantity,
                draft.reason(),
                draft.referenceDocumentId(),
                draft.performedBy(),
                Instant.now()
        ));
    }

    /**
     * Reconstitute from persistence without re-validation.
     */
    public static StockMovement reconstitute(
            StockMovementId id,
            StockId stockId,
            ArticleId articleId,
            StockBatchId stockBatchId,
            BatchReference batchReference,
            MovementType movementType,
            MovementDirection direction,
            Quantity quantity,
            String reason,
            String referenceDocumentId,
            String performedBy,
            Instant performedAt
    ) {
        return new StockMovement(id, stockId, articleId, stockBatchId, batchReference,
                movementType, direction, quantity, reason, referenceDocumentId, performedBy, performedAt);
    }

    // ==================== Queries ====================

    public boolean isIncoming() {
        return direction == MovementDirection.IN;
    }

    public boolean isOutgoing() {
        return direction == MovementDirection.OUT;
    }

    // ==================== Getters ====================

    public StockMovementId id() { return id; }
    public StockId stockId() { return stockId; }
    public ArticleId articleId() { return articleId; }
    public StockBatchId stockBatchId() { return stockBatchId; }
    public BatchReference batchReference() { return batchReference; }
    public MovementType movementType() { return movementType; }
    public MovementDirection direction() { return direction; }
    public Quantity quantity() { return quantity; }
    public String reason() { return reason; }
    public String referenceDocumentId() { return referenceDocumentId; }
    public String performedBy() { return performedBy; }
    public Instant performedAt() { return performedAt; }

    // ==================== Helpers ====================

    /**
     * Derives direction from movement type. Must NOT be called for ADJUSTMENT
     * (which requires explicit direction and is handled before this method is called).
     */
    private static MovementDirection deriveDirection(MovementType type) {
        return switch (type) {
            case GOODS_RECEIPT, PRODUCTION_OUTPUT, RETURN -> MovementDirection.IN;
            case PRODUCTION_CONSUMPTION, SALE, WASTE, INTER_BRANCH_TRANSFER -> MovementDirection.OUT;
            case ADJUSTMENT -> MovementDirection.IN; // unreachable – ADJUSTMENT handled before call
        };
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof StockMovement other)) return false;
        return Objects.equals(id, other.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return "StockMovement{id=" + id + ", type=" + movementType + ", direction=" + direction + "}";
    }
}
