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 java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * InventoryCount aggregate root.
 *
 * Invariants:
 * - StorageLocationId must be valid
 * - CountDate must be valid ISO date, not in the future
 * - InitiatedBy must not be blank
 * - Max. one active count (OPEN/COUNTING) per StorageLocation (Application Layer)
 * - ArticleId unique within countItems
 * - addCountItem only in status OPEN
 * - startCounting only in status OPEN, requires non-empty countItems
 * - updateCountItem only in status COUNTING
 * - complete only in status COUNTING, requires all items counted, Vier-Augen-Prinzip (completedBy ≠ initiatedBy)
 * - cancel only in status OPEN or COUNTING, requires non-blank reason → CANCELLED
 */
public class InventoryCount {

    private final InventoryCountId id;
    private final StorageLocationId storageLocationId;
    private final LocalDate countDate;
    private final String initiatedBy;
    private String completedBy;
    private InventoryCountStatus status;
    private String cancellationReason;
    private final Instant createdAt;
    private final List<CountItem> countItems;

    private InventoryCount(
            InventoryCountId id,
            StorageLocationId storageLocationId,
            LocalDate countDate,
            String initiatedBy,
            String completedBy,
            InventoryCountStatus status,
            String cancellationReason,
            Instant createdAt,
            List<CountItem> countItems
    ) {
        this.id = id;
        this.storageLocationId = storageLocationId;
        this.countDate = countDate;
        this.initiatedBy = initiatedBy;
        this.completedBy = completedBy;
        this.status = status;
        this.cancellationReason = cancellationReason;
        this.createdAt = createdAt;
        this.countItems = new ArrayList<>(countItems);
    }

    /**
     * Factory: Erzeugt eine neue Inventur aus rohen Eingaben.
     */
    public static Result<InventoryCountError, InventoryCount> create(InventoryCountDraft draft) {
        // 1. StorageLocationId validieren
        if (draft.storageLocationId() == null || draft.storageLocationId().isBlank()) {
            return Result.failure(new InventoryCountError.InvalidStorageLocationId("must not be blank"));
        }
        StorageLocationId storageLocationId;
        try {
            storageLocationId = StorageLocationId.of(draft.storageLocationId());
        } catch (IllegalArgumentException e) {
            return Result.failure(new InventoryCountError.InvalidStorageLocationId(e.getMessage()));
        }

        // 2. CountDate validieren
        if (draft.countDate() == null || draft.countDate().isBlank()) {
            return Result.failure(new InventoryCountError.InvalidCountDate("must not be blank"));
        }
        LocalDate countDate;
        try {
            countDate = LocalDate.parse(draft.countDate());
        } catch (DateTimeParseException e) {
            return Result.failure(new InventoryCountError.InvalidCountDate("Invalid ISO date: " + draft.countDate()));
        }
        if (countDate.isAfter(LocalDate.now())) {
            return Result.failure(new InventoryCountError.CountDateInFuture(draft.countDate()));
        }

        // 3. InitiatedBy validieren
        if (draft.initiatedBy() == null || draft.initiatedBy().isBlank()) {
            return Result.failure(new InventoryCountError.InvalidInitiatedBy("must not be blank"));
        }

        return Result.success(new InventoryCount(
                InventoryCountId.generate(), storageLocationId, countDate,
                draft.initiatedBy(), null, InventoryCountStatus.OPEN, null, Instant.now(), List.of()
        ));
    }

    /**
     * Reconstitute from persistence without re-validation.
     */
    public static InventoryCount reconstitute(
            InventoryCountId id,
            StorageLocationId storageLocationId,
            LocalDate countDate,
            String initiatedBy,
            String completedBy,
            InventoryCountStatus status,
            String cancellationReason,
            Instant createdAt,
            List<CountItem> countItems
    ) {
        return new InventoryCount(id, storageLocationId, countDate, initiatedBy, completedBy, status, cancellationReason, createdAt, countItems);
    }

    // ==================== Count Item Management ====================

    public Result<InventoryCountError, CountItem> addCountItem(CountItemDraft draft) {
        if (status != InventoryCountStatus.OPEN) {
            return Result.failure(new InventoryCountError.InvalidStatusTransition(
                    status.name(), InventoryCountStatus.OPEN.name()));
        }

        CountItem item;
        switch (CountItem.create(draft)) {
            case Result.Failure(var err) -> { return Result.failure(err); }
            case Result.Success(var val) -> item = val;
        }

        if (hasArticle(item.articleId())) {
            return Result.failure(new InventoryCountError.DuplicateArticle(draft.articleId()));
        }

        this.countItems.add(item);
        return Result.success(item);
    }

    // ==================== Status Transitions ====================

    public Result<InventoryCountError, Void> startCounting() {
        if (status != InventoryCountStatus.OPEN) {
            return Result.failure(new InventoryCountError.InvalidStatusTransition(
                    status.name(), InventoryCountStatus.COUNTING.name()));
        }
        if (countItems.isEmpty()) {
            return Result.failure(new InventoryCountError.NoCountItems());
        }
        this.status = InventoryCountStatus.COUNTING;
        return Result.success(null);
    }

    public Result<InventoryCountError, Void> complete(String completedBy) {
        if (status != InventoryCountStatus.COUNTING) {
            return Result.failure(new InventoryCountError.InvalidStatusTransition(
                    status.name(), InventoryCountStatus.COMPLETED.name()));
        }
        if (completedBy == null || completedBy.isBlank()) {
            return Result.failure(new InventoryCountError.InvalidInitiatedBy("completedBy must not be blank"));
        }
        if (countItems.isEmpty()) {
            return Result.failure(new InventoryCountError.NoCountItems());
        }
        boolean allCounted = countItems.stream().allMatch(CountItem::isCounted);
        if (!allCounted) {
            return Result.failure(new InventoryCountError.IncompleteCountItems());
        }
        if (completedBy.equals(initiatedBy)) {
            return Result.failure(new InventoryCountError.SamePersonViolation());
        }
        this.completedBy = completedBy;
        this.status = InventoryCountStatus.COMPLETED;
        return Result.success(null);
    }

    public Result<InventoryCountError, Void> cancel(String reason) {
        if (reason == null || reason.isBlank()) {
            return Result.failure(new InventoryCountError.CancellationReasonRequired());
        }
        if (status != InventoryCountStatus.OPEN && status != InventoryCountStatus.COUNTING) {
            return Result.failure(new InventoryCountError.InvalidStatusTransition(
                    status.name(), InventoryCountStatus.CANCELLED.name()));
        }
        this.cancellationReason = reason;
        this.status = InventoryCountStatus.CANCELLED;
        return Result.success(null);
    }

    public Result<InventoryCountError, Void> updateCountItem(CountItemId itemId, Quantity actualQuantity) {
        if (status != InventoryCountStatus.COUNTING) {
            return Result.failure(new InventoryCountError.InvalidStatusTransition(
                    status.name(), InventoryCountStatus.COUNTING.name()));
        }

        var item = countItems.stream()
                .filter(ci -> ci.id().equals(itemId))
                .findFirst()
                .orElse(null);

        if (item == null) {
            return Result.failure(new InventoryCountError.CountItemNotFound(itemId.value()));
        }

        if (actualQuantity.uom() != item.expectedQuantity().uom()) {
            return Result.failure(new InventoryCountError.InvalidQuantity(
                    "Unit of measure mismatch: expected " + item.expectedQuantity().uom()
                            + " but got " + actualQuantity.uom()));
        }

        if (actualQuantity.amount().compareTo(BigDecimal.ZERO) < 0) {
            return Result.failure(new InventoryCountError.InvalidQuantity(
                    "Actual quantity must be >= 0"));
        }

        item.recordActualQuantity(actualQuantity);
        return Result.success(null);
    }

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

    public boolean isActive() {
        return status == InventoryCountStatus.OPEN || status == InventoryCountStatus.COUNTING;
    }

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

    public InventoryCountId id() { return id; }
    public StorageLocationId storageLocationId() { return storageLocationId; }
    public LocalDate countDate() { return countDate; }
    public String initiatedBy() { return initiatedBy; }
    public String completedBy() { return completedBy; }
    public InventoryCountStatus status() { return status; }
    public String cancellationReason() { return cancellationReason; }
    public Instant createdAt() { return createdAt; }
    public List<CountItem> countItems() { return Collections.unmodifiableList(countItems); }

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

    private boolean hasArticle(ArticleId articleId) {
        return countItems.stream().anyMatch(item -> item.articleId().equals(articleId));
    }

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

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

    @Override
    public String toString() {
        return "InventoryCount{id=" + id + ", storageLocationId=" + storageLocationId
                + ", countDate=" + countDate + ", status=" + status + "}";
    }
}
