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;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Batch aggregate root.
 *
 * Invariants:
 * 1. PlannedQuantity must be positive
 * 2. BestBeforeDate must be after ProductionDate
 * 3. BatchNumber is auto-generated (format P-YYYY-MM-DD-XXX)
 * 4. New batches always start in PLANNED status
 * 5. RecipeId must reference an ACTIVE recipe (enforced by Use Case)
 * 6. startProduction() only allowed from PLANNED status
 * 7. recordConsumption() only allowed in IN_PRODUCTION status
 * 8. No duplicate inputBatchId within consumptions
 * 9. Consumption quantity must be positive
 * 10. complete() only allowed from IN_PRODUCTION status
 * 11. At least one Consumption required before completion
 * 12. ActualQuantity must be positive
 * 13. Waste must be zero or positive
 * 14. cancel() only allowed from PLANNED or IN_PRODUCTION status
 * 15. CancellationReason must not be null or blank (max 500 chars)
 */
public class Batch {

    private final BatchId id;
    private final BatchNumber batchNumber;
    private final RecipeId recipeId;
    private BatchStatus status;
    private final Quantity plannedQuantity;
    private Quantity actualQuantity;
    private Quantity waste;
    private String remarks;
    private final LocalDate productionDate;
    private final LocalDate bestBeforeDate;
    private final OffsetDateTime createdAt;
    private OffsetDateTime updatedAt;
    private OffsetDateTime completedAt;
    private String cancellationReason;
    private OffsetDateTime cancelledAt;
    private final long version;
    private final List<Consumption> consumptions;

    private Batch(
            BatchId id,
            BatchNumber batchNumber,
            RecipeId recipeId,
            BatchStatus status,
            Quantity plannedQuantity,
            Quantity actualQuantity,
            Quantity waste,
            String remarks,
            LocalDate productionDate,
            LocalDate bestBeforeDate,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt,
            OffsetDateTime completedAt,
            String cancellationReason,
            OffsetDateTime cancelledAt,
            long version,
            List<Consumption> consumptions
    ) {
        this.id = id;
        this.batchNumber = batchNumber;
        this.recipeId = recipeId;
        this.status = status;
        this.plannedQuantity = plannedQuantity;
        this.actualQuantity = actualQuantity;
        this.waste = waste;
        this.remarks = remarks;
        this.productionDate = productionDate;
        this.bestBeforeDate = bestBeforeDate;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
        this.completedAt = completedAt;
        this.cancellationReason = cancellationReason;
        this.cancelledAt = cancelledAt;
        this.version = version;
        this.consumptions = consumptions;
    }

    public static Result<BatchError, Batch> plan(BatchDraft draft, BatchNumber batchNumber) {
        if (draft.recipeId() == null || draft.recipeId().isBlank()) {
            return Result.failure(new BatchError.ValidationFailure("recipeId must not be blank"));
        }

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

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

        if (!draft.bestBeforeDate().isAfter(draft.productionDate())) {
            return Result.failure(new BatchError.InvalidDates(
                    "bestBeforeDate (" + draft.bestBeforeDate() + ") must be after productionDate (" + draft.productionDate() + ")"));
        }

        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 BatchError.InvalidPlannedQuantity(err.toString()));
                }
                case Result.Success(var qty) -> plannedQuantity = qty;
            }
        } catch (NumberFormatException e) {
            return Result.failure(new BatchError.InvalidPlannedQuantity(
                    "Invalid amount format: " + draft.plannedQuantity()));
        } catch (IllegalArgumentException e) {
            return Result.failure(new BatchError.InvalidPlannedQuantity(
                    "Invalid unit: " + draft.plannedQuantityUnit()));
        }

        var now = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(new Batch(
                BatchId.generate(),
                batchNumber,
                RecipeId.of(draft.recipeId()),
                BatchStatus.PLANNED,
                plannedQuantity,
                null,
                null,
                null,
                draft.productionDate(),
                draft.bestBeforeDate(),
                now,
                now,
                null,
                null,
                null,
                0L,
                new ArrayList<>()
        ));
    }

    public Result<BatchError, Void> startProduction() {
        if (status != BatchStatus.PLANNED) {
            return Result.failure(new BatchError.InvalidStatusTransition(status, BatchStatus.IN_PRODUCTION));
        }
        this.status = BatchStatus.IN_PRODUCTION;
        this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(null);
    }

    public Result<BatchError, Void> complete(CompleteBatchDraft draft) {
        if (status != BatchStatus.IN_PRODUCTION) {
            return Result.failure(new BatchError.InvalidStatusTransition(status, BatchStatus.COMPLETED));
        }

        if (consumptions.isEmpty()) {
            return Result.failure(new BatchError.MissingConsumptions(id));
        }

        Quantity actualQty;
        switch (parseQuantity(draft.actualQuantity(), draft.actualQuantityUnit(), BatchError.InvalidActualQuantity::new)) {
            case Result.Failure(var err) -> { return Result.failure(err); }
            case Result.Success(var qty) -> actualQty = qty;
        }

        Quantity wasteQty;
        switch (parseWasteQuantity(draft.waste(), draft.wasteUnit())) {
            case Result.Failure(var err) -> { return Result.failure(err); }
            case Result.Success(var qty) -> wasteQty = qty;
        }

        this.actualQuantity = actualQty;
        this.waste = wasteQty;
        this.remarks = draft.remarks();
        this.status = BatchStatus.COMPLETED;
        var now = OffsetDateTime.now(ZoneOffset.UTC);
        this.completedAt = now;
        this.updatedAt = now;
        return Result.success(null);
    }

    private Result<BatchError, Quantity> parseQuantity(String amountStr, String unitStr,
            java.util.function.Function<String, BatchError> errorFactory) {
        try {
            var amount = new BigDecimal(amountStr);
            var uom = UnitOfMeasure.valueOf(unitStr);
            switch (Quantity.of(amount, uom)) {
                case Result.Failure(var err) -> { return Result.failure(errorFactory.apply(err.toString())); }
                case Result.Success(var qty) -> { return Result.success(qty); }
            }
        } catch (NumberFormatException e) {
            return Result.failure(errorFactory.apply("Invalid amount format: " + amountStr));
        } catch (IllegalArgumentException e) {
            return Result.failure(errorFactory.apply("Invalid unit: " + unitStr));
        }
    }

    private Result<BatchError, Quantity> parseWasteQuantity(String amountStr, String unitStr) {
        try {
            var amount = new BigDecimal(amountStr);
            if (amount.compareTo(BigDecimal.ZERO) < 0) {
                return Result.failure(new BatchError.InvalidWaste("Waste must be zero or positive"));
            }
            var uom = UnitOfMeasure.valueOf(unitStr);
            if (amount.compareTo(BigDecimal.ZERO) == 0) {
                return Result.success(Quantity.reconstitute(amount, uom));
            }
            switch (Quantity.of(amount, uom)) {
                case Result.Failure(var err) -> { return Result.failure(new BatchError.InvalidWaste(err.toString())); }
                case Result.Success(var qty) -> { return Result.success(qty); }
            }
        } catch (NumberFormatException e) {
            return Result.failure(new BatchError.InvalidWaste("Invalid amount format: " + amountStr));
        } catch (IllegalArgumentException e) {
            return Result.failure(new BatchError.InvalidWaste("Invalid unit: " + unitStr));
        }
    }

    static final int MAX_CANCELLATION_REASON_LENGTH = 500;

    public Result<BatchError, Void> cancel(CancelBatchDraft draft) {
        if (status == BatchStatus.COMPLETED || status == BatchStatus.CANCELLED) {
            return Result.failure(new BatchError.InvalidStatusTransition(status, BatchStatus.CANCELLED));
        }

        if (draft.reason() == null || draft.reason().isBlank()) {
            return Result.failure(new BatchError.CancellationReasonRequired());
        }

        if (draft.reason().length() > MAX_CANCELLATION_REASON_LENGTH) {
            return Result.failure(new BatchError.CancellationReasonTooLong(MAX_CANCELLATION_REASON_LENGTH));
        }

        this.cancellationReason = draft.reason().strip();
        this.status = BatchStatus.CANCELLED;
        var now = OffsetDateTime.now(ZoneOffset.UTC);
        this.cancelledAt = now;
        this.updatedAt = now;
        return Result.success(null);
    }

    public Result<BatchError, Consumption> recordConsumption(ConsumptionDraft draft) {
        if (status != BatchStatus.IN_PRODUCTION) {
            return Result.failure(new BatchError.NotInProduction(id));
        }

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

        boolean duplicate = consumptions.stream()
                .anyMatch(c -> c.inputBatchId().equals(consumption.inputBatchId()));
        if (duplicate) {
            return Result.failure(new BatchError.DuplicateInputBatch(consumption.inputBatchId()));
        }

        consumptions.add(consumption);
        this.updatedAt = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(consumption);
    }

    // TODO: Replace 17-parameter signature with BatchSnapshot record (#72)
    public static Batch reconstitute(
            BatchId id,
            BatchNumber batchNumber,
            RecipeId recipeId,
            BatchStatus status,
            Quantity plannedQuantity,
            Quantity actualQuantity,
            Quantity waste,
            String remarks,
            LocalDate productionDate,
            LocalDate bestBeforeDate,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt,
            OffsetDateTime completedAt,
            String cancellationReason,
            OffsetDateTime cancelledAt,
            long version,
            List<Consumption> consumptions
    ) {
        return new Batch(id, batchNumber, recipeId, status, plannedQuantity, actualQuantity,
                waste, remarks, productionDate, bestBeforeDate, createdAt, updatedAt,
                completedAt, cancellationReason, cancelledAt, version, new ArrayList<>(consumptions));
    }

    public BatchId id() { return id; }
    public BatchNumber batchNumber() { return batchNumber; }
    public RecipeId recipeId() { return recipeId; }
    public BatchStatus status() { return status; }
    public Quantity plannedQuantity() { return plannedQuantity; }
    public Quantity actualQuantity() { return actualQuantity; }
    public Quantity waste() { return waste; }
    public String remarks() { return remarks; }
    public LocalDate productionDate() { return productionDate; }
    public LocalDate bestBeforeDate() { return bestBeforeDate; }
    public OffsetDateTime createdAt() { return createdAt; }
    public OffsetDateTime updatedAt() { return updatedAt; }
    public OffsetDateTime completedAt() { return completedAt; }
    public String cancellationReason() { return cancellationReason; }
    public OffsetDateTime cancelledAt() { return cancelledAt; }
    public long version() { return version; }
    public List<Consumption> consumptions() { return Collections.unmodifiableList(consumptions); }
}
