package de.effigenix.domain.production;

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.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

/**
 * ProductionOrder aggregate root.
 *
 * Invariants:
 * 1. PlannedQuantity must be positive
 * 2. PlannedDate must not be in the past (today is allowed)
 * 3. Status starts as PLANNED
 * 4. RecipeId must be set (not blank)
 * 5. Priority must be valid (LOW, NORMAL, HIGH, URGENT)
 * 6. Only PLANNED → RELEASED transition allowed via release()
 *
 * TODO: Further transitions (RELEASED → IN_PROGRESS → COMPLETED, RELEASED/IN_PROGRESS → CANCELLED)
 *       must be guarded by explicit transition methods once those use cases are implemented.
 */
public class ProductionOrder {

    private final ProductionOrderId id;
    private final RecipeId recipeId;
    private ProductionOrderStatus status;
    private final Quantity plannedQuantity;
    private final LocalDate plannedDate;
    private final Priority priority;
    private final String notes;
    private final OffsetDateTime createdAt;
    private OffsetDateTime updatedAt;
    private final long version;

    private ProductionOrder(
            ProductionOrderId id,
            RecipeId recipeId,
            ProductionOrderStatus status,
            Quantity plannedQuantity,
            LocalDate plannedDate,
            Priority priority,
            String notes,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt,
            long version
    ) {
        this.id = id;
        this.recipeId = recipeId;
        this.status = status;
        this.plannedQuantity = plannedQuantity;
        this.plannedDate = plannedDate;
        this.priority = priority;
        this.notes = notes;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.version = version;
    }

    public static Result<ProductionOrderError, ProductionOrder> create(ProductionOrderDraft draft) {
        if (draft.recipeId() == null || draft.recipeId().isBlank()) {
            return Result.failure(new ProductionOrderError.ValidationFailure("recipeId must not be blank"));
        }

        if (draft.plannedDate() == null) {
            return Result.failure(new ProductionOrderError.ValidationFailure("plannedDate must not be null"));
        }

        if (draft.plannedDate().isBefore(LocalDate.now())) {
            return Result.failure(new ProductionOrderError.PlannedDateInPast(draft.plannedDate()));
        }

        Priority priority;
        try {
            priority = Priority.valueOf(draft.priority());
        } catch (IllegalArgumentException | NullPointerException e) {
            return Result.failure(new ProductionOrderError.InvalidPriority(draft.priority()));
        }

        if (draft.plannedQuantity() == null || draft.plannedQuantity().isBlank()) {
            return Result.failure(new ProductionOrderError.InvalidPlannedQuantity("plannedQuantity must not be blank"));
        }
        if (draft.plannedQuantityUnit() == null || draft.plannedQuantityUnit().isBlank()) {
            return Result.failure(new ProductionOrderError.InvalidPlannedQuantity("plannedQuantityUnit must not be blank"));
        }

        Quantity plannedQuantity;
        try {
            var amount = new BigDecimal(draft.plannedQuantity());
            var uom = UnitOfMeasure.valueOf(draft.plannedQuantityUnit());
            switch (Quantity.of(amount, uom)) {
                case Result.Failure(var err) -> {
                    return Result.failure(new ProductionOrderError.InvalidPlannedQuantity(err.toString()));
                }
                case Result.Success(var qty) -> plannedQuantity = qty;
            }
        } catch (NumberFormatException e) {
            return Result.failure(new ProductionOrderError.InvalidPlannedQuantity(
                    "Invalid amount format: " + draft.plannedQuantity()));
        } catch (IllegalArgumentException e) {
            return Result.failure(new ProductionOrderError.InvalidPlannedQuantity(
                    "Invalid unit: " + draft.plannedQuantityUnit()));
        }

        var now = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(new ProductionOrder(
                ProductionOrderId.generate(),
                RecipeId.of(draft.recipeId()),
                ProductionOrderStatus.PLANNED,
                plannedQuantity,
                draft.plannedDate(),
                priority,
                draft.notes(),
                now,
                now,
                0L
        ));
    }

    public static ProductionOrder reconstitute(
            ProductionOrderId id,
            RecipeId recipeId,
            ProductionOrderStatus status,
            Quantity plannedQuantity,
            LocalDate plannedDate,
            Priority priority,
            String notes,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt,
            long version
    ) {
        return new ProductionOrder(id, recipeId, status, plannedQuantity, plannedDate,
                priority, notes, createdAt, updatedAt, version);
    }

    public ProductionOrderId id() { return id; }
    public RecipeId recipeId() { return recipeId; }
    public ProductionOrderStatus status() { return status; }
    public Quantity plannedQuantity() { return plannedQuantity; }
    public LocalDate plannedDate() { return plannedDate; }
    public Priority priority() { return priority; }
    public String notes() { return notes; }
    public OffsetDateTime createdAt() { return createdAt; }
    public OffsetDateTime updatedAt() { return updatedAt; }
    public long version() { return version; }

    public Result<ProductionOrderError, Void> release() {
        if (status != ProductionOrderStatus.PLANNED) {
            return Result.failure(new ProductionOrderError.InvalidStatusTransition(status, ProductionOrderStatus.RELEASED));
        }
        this.status = ProductionOrderStatus.RELEASED;
        this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(null);
    }
}
