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

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

/**
 * Recipe aggregate root.
 *
 * Invariants:
 * 1. Name must not be blank
 * 2. Version >= 1
 * 3. YieldPercentage 1-200%
 * 4. ShelfLifeDays > 0 for FINISHED_PRODUCT and INTERMEDIATE
 * 5. OutputQuantity must be positive
 * 6. New recipes always start in DRAFT status
 * 7. Ingredients can only be added/removed in DRAFT status
 * 8. Ingredient positions must be unique within a recipe
 * 9. ProductionSteps can only be added/removed in DRAFT status
 * 10. Step numbers must be unique within a recipe
 * 11. Recipe can only be activated when it has at least one ingredient
 * 12. Recipe can only be activated from DRAFT status
 * 13. Recipe can only be archived from ACTIVE status
 * 14. ArticleId must not be blank (references the output article in Master Data)
 */
public class Recipe {

    private final RecipeId id;
    private RecipeName name;
    private int version;
    private RecipeType type;
    private String description;
    private YieldPercentage yieldPercentage;
    private Integer shelfLifeDays;
    private Quantity outputQuantity;
    private String articleId;
    private RecipeStatus status;
    private final List<Ingredient> ingredients;
    private final List<ProductionStep> productionSteps;
    private final OffsetDateTime createdAt;
    private OffsetDateTime updatedAt;

    private Recipe(
            RecipeId id,
            RecipeName name,
            int version,
            RecipeType type,
            String description,
            YieldPercentage yieldPercentage,
            Integer shelfLifeDays,
            Quantity outputQuantity,
            String articleId,
            RecipeStatus status,
            List<Ingredient> ingredients,
            List<ProductionStep> productionSteps,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt
    ) {
        this.id = id;
        this.name = name;
        this.version = version;
        this.type = type;
        this.description = description;
        this.yieldPercentage = yieldPercentage;
        this.shelfLifeDays = shelfLifeDays;
        this.outputQuantity = outputQuantity;
        this.articleId = articleId;
        this.status = status;
        this.ingredients = new ArrayList<>(ingredients);
        this.productionSteps = new ArrayList<>(productionSteps);
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }

    /**
     * Factory: Creates a new Recipe in DRAFT status from raw inputs.
     * Orchestrates VO validation internally.
     */
    public static Result<RecipeError, Recipe> create(RecipeDraft draft) {
        // 1. Name (required)
        RecipeName name;
        switch (RecipeName.of(draft.name())) {
            case Failure(var msg) -> { return Result.failure(new RecipeError.ValidationFailure(msg)); }
            case Success(var val) -> name = val;
        }

        // 2. Version >= 1
        if (draft.version() < 1) {
            return Result.failure(new RecipeError.ValidationFailure("Version must be >= 1, was: " + draft.version()));
        }

        // 3. YieldPercentage (1-200)
        YieldPercentage yieldPercentage;
        switch (YieldPercentage.of(draft.yieldPercentage())) {
            case Failure(var err) -> { return Result.failure(new RecipeError.ValidationFailure(err.message())); }
            case Success(var val) -> yieldPercentage = val;
        }

        // 4. ShelfLifeDays: required and > 0 for FINISHED_PRODUCT and INTERMEDIATE
        Integer shelfLifeDays = draft.shelfLifeDays();
        if (draft.type() == RecipeType.FINISHED_PRODUCT || draft.type() == RecipeType.INTERMEDIATE) {
            if (shelfLifeDays == null || shelfLifeDays <= 0) {
                return Result.failure(new RecipeError.InvalidShelfLife(
                        "ShelfLifeDays must be > 0 for " + draft.type() + ", was: " + shelfLifeDays));
            }
        }

        // 5. ArticleId (required)
        if (draft.articleId() == null || draft.articleId().isBlank()) {
            return Result.failure(new RecipeError.ValidationFailure("ArticleId must not be blank"));
        }

        // 6. Output Quantity (required, positive)
        Quantity outputQuantity;
        try {
            BigDecimal amount = new BigDecimal(draft.outputQuantity());
            UnitOfMeasure uom = UnitOfMeasure.valueOf(draft.outputUom());
            switch (Quantity.of(amount, uom)) {
                case Failure(var err) -> { return Result.failure(new RecipeError.ValidationFailure(err.message())); }
                case Success(var val) -> outputQuantity = val;
            }
        } catch (IllegalArgumentException e) {
            return Result.failure(new RecipeError.ValidationFailure("Invalid output quantity: " + e.getMessage()));
        }

        var now = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(new Recipe(
                RecipeId.generate(), name, draft.version(), draft.type(),
                draft.description(), yieldPercentage, shelfLifeDays, outputQuantity,
                draft.articleId(), RecipeStatus.DRAFT, List.of(), List.of(), now, now
        ));
    }

    /**
     * Reconstitutes a Recipe from persistence. No validation.
     */
    public static Recipe reconstitute(
            RecipeId id,
            RecipeName name,
            int version,
            RecipeType type,
            String description,
            YieldPercentage yieldPercentage,
            Integer shelfLifeDays,
            Quantity outputQuantity,
            String articleId,
            RecipeStatus status,
            List<Ingredient> ingredients,
            List<ProductionStep> productionSteps,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt
    ) {
        return new Recipe(id, name, version, type, description,
                yieldPercentage, shelfLifeDays, outputQuantity, articleId, status, ingredients, productionSteps, createdAt, updatedAt);
    }

    // ==================== Ingredient Management ====================

    public Result<RecipeError, Ingredient> addIngredient(IngredientDraft draft) {
        if (status != RecipeStatus.DRAFT) {
            return Result.failure(new RecipeError.NotInDraftStatus());
        }
        if (hasPosition(draft.position())) {
            return Result.failure(new RecipeError.DuplicatePosition(draft.position()));
        }

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

        this.ingredients.add(ingredient);
        touch();
        return Result.success(ingredient);
    }

    public Result<RecipeError, Void> removeIngredient(IngredientId ingredientId) {
        if (status != RecipeStatus.DRAFT) {
            return Result.failure(new RecipeError.NotInDraftStatus());
        }
        boolean removed = this.ingredients.removeIf(i -> i.id().equals(ingredientId));
        if (!removed) {
            return Result.failure(new RecipeError.IngredientNotFound(ingredientId));
        }
        touch();
        return Result.success(null);
    }

    // ==================== ProductionStep Management ====================

    public Result<RecipeError, ProductionStep> addProductionStep(ProductionStepDraft draft) {
        if (status != RecipeStatus.DRAFT) {
            return Result.failure(new RecipeError.NotInDraftStatus());
        }
        if (hasStepNumber(draft.stepNumber())) {
            return Result.failure(new RecipeError.DuplicateStepNumber(draft.stepNumber()));
        }

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

        this.productionSteps.add(step);
        touch();
        return Result.success(step);
    }

    public Result<RecipeError, Void> removeProductionStep(int stepNumber) {
        if (status != RecipeStatus.DRAFT) {
            return Result.failure(new RecipeError.NotInDraftStatus());
        }
        boolean removed = this.productionSteps.removeIf(s -> s.stepNumber() == stepNumber);
        if (!removed) {
            return Result.failure(new RecipeError.StepNotFound(stepNumber));
        }
        touch();
        return Result.success(null);
    }

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

    public Result<RecipeError, Void> activate() {
        if (status != RecipeStatus.DRAFT) {
            return Result.failure(new RecipeError.InvalidStatusTransition(status, RecipeStatus.ACTIVE));
        }
        if (ingredients.isEmpty()) {
            return Result.failure(new RecipeError.NoIngredients());
        }
        this.status = RecipeStatus.ACTIVE;
        touch();
        return Result.success(null);
    }

    public Result<RecipeError, Void> archive() {
        if (status != RecipeStatus.ACTIVE) {
            return Result.failure(new RecipeError.InvalidStatusTransition(status, RecipeStatus.ARCHIVED));
        }
        this.status = RecipeStatus.ARCHIVED;
        touch();
        return Result.success(null);
    }

    // ==================== Business Logic ====================

    public Result<RecipeError, java.time.LocalDate> calculateBestBeforeDate(java.time.LocalDate productionDate) {
        if (shelfLifeDays == null || shelfLifeDays <= 0) {
            return Result.failure(new RecipeError.InvalidShelfLife(
                    "ShelfLifeDays must be > 0, was: " + shelfLifeDays));
        }
        return Result.success(productionDate.plusDays(shelfLifeDays));
    }

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

    public RecipeId id() { return id; }
    public RecipeName name() { return name; }
    public int version() { return version; }
    public RecipeType type() { return type; }
    public String description() { return description; }
    public YieldPercentage yieldPercentage() { return yieldPercentage; }
    public Integer shelfLifeDays() { return shelfLifeDays; }
    public Quantity outputQuantity() { return outputQuantity; }
    public String articleId() { return articleId; }
    public RecipeStatus status() { return status; }
    public List<Ingredient> ingredients() { return Collections.unmodifiableList(ingredients); }
    public List<ProductionStep> productionSteps() { return Collections.unmodifiableList(productionSteps); }
    public OffsetDateTime createdAt() { return createdAt; }
    public OffsetDateTime updatedAt() { return updatedAt; }

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

    private boolean hasPosition(int position) {
        return ingredients.stream().anyMatch(i -> i.position() == position);
    }

    private boolean hasStepNumber(int stepNumber) {
        return productionSteps.stream().anyMatch(s -> s.stepNumber() == stepNumber);
    }

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

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

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

    @Override
    public String toString() {
        return "Recipe{id=" + id + ", name=" + name + ", version=" + version + ", status=" + status + "}";
    }
}
