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()
 * 7. Only RELEASED → IN_PROGRESS transition allowed via startProduction(BatchId)
 * 8. BatchId is set exactly once (null → non-null) during startProduction()
 * 9. BatchId must not already be assigned (BatchAlreadyAssigned)
 * 10. Only IN_PROGRESS → COMPLETED transition allowed via complete() (Batch must be COMPLETED – enforced by Use Case)
 * 11. Only PLANNED or RELEASED → CANCELLED transition allowed via cancel(reason)
 * 12. CancelledReason is set exactly once during cancel() and must not be blank
 * 13. Reschedule only in PLANNED or RELEASED, new date must not be null or in the past
 */
public class ProductionOrder {

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

    private ProductionOrder(
            ProductionOrderId id,
            RecipeId recipeId,
            ProductionOrderStatus status,
            BatchId batchId,
            Quantity plannedQuantity,
            LocalDate plannedDate,
            Priority priority,
            String notes,
            String cancelledReason,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt,
            long version
    ) {
        this.id = id;
        this.recipeId = recipeId;
        this.status = status;
        this.batchId = batchId;
        this.plannedQuantity = plannedQuantity;
        this.plannedDate = plannedDate;
        this.priority = priority;
        this.notes = notes;
        this.cancelledReason = cancelledReason;
        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,
                null,
                plannedQuantity,
                draft.plannedDate(),
                priority,
                draft.notes(),
                null,
                now,
                now,
                0L
        ));
    }

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

    public ProductionOrderId id() { return id; }
    public RecipeId recipeId() { return recipeId; }
    public ProductionOrderStatus status() { return status; }
    public BatchId batchId() { return batchId; }
    public Quantity plannedQuantity() { return plannedQuantity; }
    public LocalDate plannedDate() { return plannedDate; }
    public Priority priority() { return priority; }
    public String notes() { return notes; }
    public String cancelledReason() { return cancelledReason; }
    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);
    }

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

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

    public Result<ProductionOrderError, Void> reschedule(LocalDate newDate) {
        if (status != ProductionOrderStatus.PLANNED && status != ProductionOrderStatus.RELEASED) {
            return Result.failure(new ProductionOrderError.RescheduleNotAllowed(status));
        }
        if (newDate == null) {
            return Result.failure(new ProductionOrderError.ValidationFailure("newDate must not be null"));
        }
        if (newDate.isBefore(LocalDate.now())) {
            return Result.failure(new ProductionOrderError.PlannedDateInPast(newDate));
        }
        this.plannedDate = newDate;
        this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(null);
    }

    public Result<ProductionOrderError, Void> cancel(String reason) {
        if (reason == null || reason.isBlank()) {
            return Result.failure(new ProductionOrderError.ValidationFailure("Cancellation reason must not be blank"));
        }
        if (status != ProductionOrderStatus.PLANNED && status != ProductionOrderStatus.RELEASED) {
            return Result.failure(new ProductionOrderError.InvalidStatusTransition(status, ProductionOrderStatus.CANCELLED));
        }
        this.status = ProductionOrderStatus.CANCELLED;
        this.cancelledReason = reason;
        this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(null);
    }
}
