package de.effigenix.domain.masterdata.article;

import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.domain.masterdata.supplier.SupplierId;
import de.effigenix.domain.masterdata.Unit;
import de.effigenix.shared.common.Money;
import de.effigenix.shared.common.Result;

import static de.effigenix.shared.common.Result.*;

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;

/**
 * Aggregate Root for Article.
 *
 * Invariants:
 * 1. At least one SalesUnit
 * 2. No duplicate Unit types
 * 3. Unit/PriceModel consistency (enforced by SalesUnit)
 * 4. ArticleNumber uniqueness (enforced at Repository level)
 */
public class Article {

    private final ArticleId id;
    private ArticleName name;
    private final ArticleNumber articleNumber;
    private ProductCategoryId categoryId;
    private final List<SalesUnit> salesUnits;
    private ArticleStatus status;
    private final Set<SupplierId> supplierReferences;
    private final OffsetDateTime createdAt;
    private OffsetDateTime updatedAt;

    private Article(
            ArticleId id,
            ArticleName name,
            ArticleNumber articleNumber,
            ProductCategoryId categoryId,
            List<SalesUnit> salesUnits,
            ArticleStatus status,
            Set<SupplierId> supplierReferences,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt
    ) {
        this.id = id;
        this.name = name;
        this.articleNumber = articleNumber;
        this.categoryId = categoryId;
        this.salesUnits = new ArrayList<>(salesUnits);
        this.status = status;
        this.supplierReferences = new HashSet<>(supplierReferences);
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }

    /**
     * Factory: Erzeugt einen neuen Article aus rohen Eingaben.
     * Orchestriert Validierung aller VOs intern.
     * Alle Felder sind Pflicht.
     */
    public static Result<ArticleError, Article> create(ArticleDraft draft) {
        // 1. ArticleName validieren
        ArticleName name;
        switch (ArticleName.of(draft.name())) {
            case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
            case Success(var val) -> name = val;
        }

        // 2. ArticleNumber validieren
        ArticleNumber articleNumber;
        switch (ArticleNumber.of(draft.articleNumber())) {
            case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
            case Success(var val) -> articleNumber = val;
        }

        // 3. ProductCategoryId – reines UUID-Wrapping
        var categoryId = ProductCategoryId.of(draft.categoryId());

        // 4. Money validieren
        Money price;
        switch (Money.tryEuro(draft.price())) {
            case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
            case Success(var val) -> price = val;
        }

        // 5. SalesUnit erzeugen (initiale Pflicht-Einheit)
        SalesUnit salesUnit;
        switch (SalesUnit.create(draft.unit(), draft.priceModel(), price)) {
            case Failure(var err) -> { return Result.failure(err); }
            case Success(var val) -> salesUnit = val;
        }

        var now = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(new Article(
                ArticleId.generate(),
                name,
                articleNumber,
                categoryId,
                List.of(salesUnit),
                ArticleStatus.ACTIVE,
                Set.of(),
                now,
                now
        ));
    }

    public static Article reconstitute(
            ArticleId id,
            ArticleName name,
            ArticleNumber articleNumber,
            ProductCategoryId categoryId,
            List<SalesUnit> salesUnits,
            ArticleStatus status,
            Set<SupplierId> supplierReferences,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt
    ) {
        return new Article(id, name, articleNumber, categoryId, salesUnits, status,
                supplierReferences, createdAt, updatedAt);
    }

    // ==================== Sales Unit Management ====================

    public Result<ArticleError, Void> addSalesUnit(SalesUnit salesUnit) {
        if (hasUnitType(salesUnit.unit())) {
            return Result.failure(new ArticleError.DuplicateSalesUnitType(salesUnit.unit()));
        }
        this.salesUnits.add(salesUnit);
        touch();
        return Result.success(null);
    }

    public Result<ArticleError, Void> removeSalesUnit(SalesUnitId salesUnitId) {
        if (this.salesUnits.size() <= 1) {
            return Result.failure(new ArticleError.MinimumSalesUnitRequired());
        }
        boolean removed = this.salesUnits.removeIf(su -> su.id().equals(salesUnitId));
        if (!removed) {
            return Result.failure(new ArticleError.SalesUnitNotFound(salesUnitId));
        }
        touch();
        return Result.success(null);
    }

    public Result<ArticleError, Void> updateSalesUnitPrice(SalesUnitId salesUnitId, Money newPrice) {
        var salesUnit = findSalesUnit(salesUnitId);
        if (salesUnit == null) {
            return Result.failure(new ArticleError.SalesUnitNotFound(salesUnitId));
        }
        var result = salesUnit.updatePrice(newPrice);
        if (result.isSuccess()) {
            touch();
        }
        return result;
    }

    // ==================== Article Properties ====================

    /**
     * Wendet partielle Updates an. Das Aggregate validiert intern.
     * null-Felder im Draft werden ignoriert.
     */
    public Result<ArticleError, Void> update(ArticleUpdateDraft draft) {
        if (draft.name() != null) {
            switch (ArticleName.of(draft.name())) {
                case Failure(var msg) -> { return Result.failure(new ArticleError.ValidationFailure(msg)); }
                case Success(var val) -> this.name = val;
            }
        }
        if (draft.categoryId() != null) {
            this.categoryId = ProductCategoryId.of(draft.categoryId());
        }
        touch();
        return Result.success(null);
    }

    public void activate() {
        this.status = ArticleStatus.ACTIVE;
        touch();
    }

    public void deactivate() {
        this.status = ArticleStatus.INACTIVE;
        touch();
    }

    // ==================== Supplier References ====================

    public void addSupplierReference(SupplierId supplierId) {
        this.supplierReferences.add(supplierId);
        touch();
    }

    public void removeSupplierReference(SupplierId supplierId) {
        this.supplierReferences.remove(supplierId);
        touch();
    }

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

    private boolean hasUnitType(Unit unit) {
        return salesUnits.stream().anyMatch(su -> su.unit() == unit);
    }

    private SalesUnit findSalesUnit(SalesUnitId id) {
        return salesUnits.stream()
                .filter(su -> su.id().equals(id))
                .findFirst()
                .orElse(null);
    }

    private void touch() {
        this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
    }

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

    public ArticleId id() { return id; }
    public ArticleName name() { return name; }
    public ArticleNumber articleNumber() { return articleNumber; }
    public ProductCategoryId categoryId() { return categoryId; }
    public List<SalesUnit> salesUnits() { return Collections.unmodifiableList(salesUnits); }
    public ArticleStatus status() { return status; }
    public Set<SupplierId> supplierReferences() { return Collections.unmodifiableSet(supplierReferences); }
    public OffsetDateTime createdAt() { return createdAt; }
    public OffsetDateTime updatedAt() { return updatedAt; }

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

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

    @Override
    public String toString() {
        return "Article{id=" + id + ", name=" + name + ", number=" + articleNumber + ", status=" + status + "}";
    }
}
