package de.effigenix.shared.common;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;

/**
 * Value Object representing a quantity with optional Catch-Weight (dual quantity).
 *
 * <p>Primary quantity is always required (amount + unit of measure).
 * Secondary quantity (catch-weight) is optional and used when goods are counted
 * in one unit but weighed in another (e.g., 10 pieces = 23 kg).
 *
 * <p>Invariants:
 * <ul>
 *   <li>Primary amount must be positive</li>
 *   <li>Secondary amount, if present, must be positive</li>
 *   <li>Secondary UoM is required when secondary amount is provided (and vice versa)</li>
 *   <li>Arithmetic operations require matching primary UoM</li>
 * </ul>
 */
public final class Quantity {

    private static final int SCALE = 6;

    private final BigDecimal amount;
    private final UnitOfMeasure uom;
    private final BigDecimal secondaryAmount;
    private final UnitOfMeasure secondaryUom;

    private Quantity(BigDecimal amount, UnitOfMeasure uom,
                     BigDecimal secondaryAmount, UnitOfMeasure secondaryUom) {
        this.amount = amount;
        this.uom = uom;
        this.secondaryAmount = secondaryAmount;
        this.secondaryUom = secondaryUom;
    }

    /**
     * Creates a simple quantity with one unit of measure.
     */
    public static Result<QuantityError, Quantity> of(BigDecimal amount, UnitOfMeasure uom) {
        Objects.requireNonNull(uom, "Unit of measure must not be null");
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            return Result.failure(new QuantityError.AmountMustBePositive(
                    amount == null ? "null" : amount.toPlainString()));
        }
        return Result.success(new Quantity(scale(amount), uom, null, null));
    }

    /**
     * Creates a dual quantity (catch-weight) with primary and secondary units.
     */
    public static Result<QuantityError, Quantity> dual(BigDecimal amount, UnitOfMeasure uom,
                                                        BigDecimal secondaryAmount, UnitOfMeasure secondaryUom) {
        Objects.requireNonNull(uom, "Unit of measure must not be null");
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            return Result.failure(new QuantityError.AmountMustBePositive(
                    amount == null ? "null" : amount.toPlainString()));
        }
        if (secondaryAmount == null || secondaryAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return Result.failure(new QuantityError.SecondaryAmountMustBePositive(
                    secondaryAmount == null ? "null" : secondaryAmount.toPlainString()));
        }
        if (secondaryUom == null) {
            return Result.failure(new QuantityError.SecondaryUomRequired());
        }
        return Result.success(new Quantity(scale(amount), uom, scale(secondaryAmount), secondaryUom));
    }

    /**
     * Reconstitutes a simple Quantity from persistence. No validation.
     */
    public static Quantity reconstitute(BigDecimal amount, UnitOfMeasure uom) {
        return new Quantity(amount, uom, null, null);
    }

    /**
     * Reconstitutes a dual Quantity (catch-weight) from persistence. No validation.
     */
    public static Quantity reconstitute(BigDecimal amount, UnitOfMeasure uom,
                                         BigDecimal secondaryAmount, UnitOfMeasure secondaryUom) {
        return new Quantity(amount, uom, secondaryAmount, secondaryUom);
    }

    /**
     * Adds two quantities. Both must have the same primary UoM.
     * Secondary quantities are added if both are present.
     */
    public Result<QuantityError, Quantity> add(Quantity other) {
        if (this.uom != other.uom) {
            return Result.failure(new QuantityError.UnitOfMeasureMismatch(this.uom, other.uom));
        }
        BigDecimal newSecondary = addSecondary(this.secondaryAmount, other.secondaryAmount);
        return Result.success(new Quantity(
                scale(this.amount.add(other.amount)), this.uom,
                newSecondary, this.secondaryAmount != null ? this.secondaryUom : other.secondaryUom));
    }

    /**
     * Subtracts another quantity. Both must have the same primary UoM.
     * Result amount must remain positive.
     */
    public Result<QuantityError, Quantity> subtract(Quantity other) {
        if (this.uom != other.uom) {
            return Result.failure(new QuantityError.UnitOfMeasureMismatch(this.uom, other.uom));
        }
        BigDecimal newAmount = this.amount.subtract(other.amount);
        if (newAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return Result.failure(new QuantityError.AmountMustBePositive(newAmount.toPlainString()));
        }
        BigDecimal newSecondary = subtractSecondary(this.secondaryAmount, other.secondaryAmount);
        return Result.success(new Quantity(
                scale(newAmount), this.uom,
                newSecondary, this.secondaryUom));
    }

    /**
     * Multiplies this quantity by a factor.
     */
    public Result<QuantityError, Quantity> multiply(BigDecimal factor) {
        if (factor == null || factor.compareTo(BigDecimal.ZERO) <= 0) {
            return Result.failure(new QuantityError.AmountMustBePositive(
                    factor == null ? "null" : factor.toPlainString()));
        }
        BigDecimal newAmount = scale(this.amount.multiply(factor));
        BigDecimal newSecondary = this.secondaryAmount != null
                ? scale(this.secondaryAmount.multiply(factor))
                : null;
        return Result.success(new Quantity(newAmount, this.uom, newSecondary, this.secondaryUom));
    }

    public boolean isZero() {
        return amount.compareTo(BigDecimal.ZERO) == 0;
    }

    public boolean isPositive() {
        return amount.compareTo(BigDecimal.ZERO) > 0;
    }

    public boolean hasDualQuantity() {
        return secondaryAmount != null && secondaryUom != null;
    }

    public BigDecimal amount() { return amount; }
    public UnitOfMeasure uom() { return uom; }
    public BigDecimal secondaryAmount() { return secondaryAmount; }
    public UnitOfMeasure secondaryUom() { return secondaryUom; }

    private static BigDecimal scale(BigDecimal value) {
        return value.setScale(SCALE, RoundingMode.HALF_UP);
    }

    private static BigDecimal addSecondary(BigDecimal a, BigDecimal b) {
        if (a != null && b != null) return scale(a.add(b));
        return a != null ? a : b;
    }

    private static BigDecimal subtractSecondary(BigDecimal a, BigDecimal b) {
        if (a != null && b != null) return scale(a.subtract(b));
        return a;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Quantity that)) return false;
        return amount.compareTo(that.amount) == 0
                && uom == that.uom
                && (Objects.equals(secondaryAmount, that.secondaryAmount)
                    || (secondaryAmount != null && that.secondaryAmount != null
                        && secondaryAmount.compareTo(that.secondaryAmount) == 0))
                && secondaryUom == that.secondaryUom;
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount.stripTrailingZeros(), uom,
                secondaryAmount != null ? secondaryAmount.stripTrailingZeros() : null,
                secondaryUom);
    }

    @Override
    public String toString() {
        if (hasDualQuantity()) {
            return amount.toPlainString() + " " + uom.symbol()
                    + " (" + secondaryAmount.toPlainString() + " " + secondaryUom.symbol() + ")";
        }
        return amount.toPlainString() + " " + uom.symbol();
    }
}
