package de.effigenix.domain.masterdata.customer;

import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Address;
import de.effigenix.shared.common.ContactInfo;
import de.effigenix.shared.common.Money;
import de.effigenix.shared.common.PaymentTerms;
import de.effigenix.shared.common.Result;

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

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.*;

/**
 * Aggregate Root for Customer.
 *
 * Invariants:
 * 1. Name, CustomerType und billingAddress sind Pflicht
 * 2. ContactInfo (phone) ist Pflicht
 * 3. PaymentTerms ist optional
 * 4. FrameContract nur für B2B-Kunden
 * 5. No duplicate ContractLineItems per ArticleId (enforced by FrameContract)
 */
public class Customer {

    private final CustomerId id;
    private CustomerName name;
    private final CustomerType type;
    private Address billingAddress;
    private ContactInfo contactInfo;
    private PaymentTerms paymentTerms;
    private final List<DeliveryAddress> deliveryAddresses;
    private FrameContract frameContract;
    private final Set<CustomerPreference> preferences;
    private CustomerStatus status;
    private final OffsetDateTime createdAt;
    private OffsetDateTime updatedAt;

    private Customer(
            CustomerId id,
            CustomerName name,
            CustomerType type,
            Address billingAddress,
            ContactInfo contactInfo,
            PaymentTerms paymentTerms,
            List<DeliveryAddress> deliveryAddresses,
            FrameContract frameContract,
            Set<CustomerPreference> preferences,
            CustomerStatus status,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt
    ) {
        this.id = id;
        this.name = name;
        this.type = type;
        this.billingAddress = billingAddress;
        this.contactInfo = contactInfo;
        this.paymentTerms = paymentTerms;
        this.deliveryAddresses = new ArrayList<>(deliveryAddresses);
        this.frameContract = frameContract;
        this.preferences = new HashSet<>(preferences);
        this.status = status;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }

    /**
     * Factory: Erzeugt einen neuen Customer aus rohen Eingaben.
     * Orchestriert Validierung aller VOs intern.
     * Pflicht: name, type, phone, street, city, country. Optional: alles andere.
     */
    public static Result<CustomerError, Customer> create(CustomerDraft draft) {
        // 1. Name validieren (Pflicht)
        CustomerName name;
        switch (CustomerName.of(draft.name())) {
            case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
            case Success(var val) -> name = val;
        }

        // 2. ContactInfo validieren (Pflicht: phone; optional: email, contactPerson)
        ContactInfo contactInfo;
        switch (ContactInfo.create(draft.phone(), draft.email(), draft.contactPerson())) {
            case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
            case Success(var val) -> contactInfo = val;
        }

        // 3. billingAddress (Pflicht-Invariante)
        Address billingAddress;
        switch (Address.create(draft.street(), draft.houseNumber(), draft.postalCode(), draft.city(), draft.country())) {
            case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
            case Success(var val) -> billingAddress = val;
        }

        // 4. PaymentTerms optional
        PaymentTerms paymentTerms = null;
        if (draft.paymentDueDays() != null) {
            switch (PaymentTerms.create(draft.paymentDueDays(), draft.paymentDescription())) {
                case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
                case Success(var val) -> paymentTerms = val;
            }
        }

        var now = OffsetDateTime.now(ZoneOffset.UTC);
        return Result.success(new Customer(
                CustomerId.generate(), name, draft.type(), billingAddress, contactInfo, paymentTerms,
                List.of(), null, Set.of(), CustomerStatus.ACTIVE, now, now
        ));
    }

    public static Customer reconstitute(
            CustomerId id,
            CustomerName name,
            CustomerType type,
            Address billingAddress,
            ContactInfo contactInfo,
            PaymentTerms paymentTerms,
            List<DeliveryAddress> deliveryAddresses,
            FrameContract frameContract,
            Set<CustomerPreference> preferences,
            CustomerStatus status,
            OffsetDateTime createdAt,
            OffsetDateTime updatedAt
    ) {
        return new Customer(id, name, type, billingAddress, contactInfo, paymentTerms,
                deliveryAddresses, frameContract, preferences, status, createdAt, updatedAt);
    }

    // ==================== Business Methods ====================

    /**
     * Wendet partielle Updates an. Das Aggregate validiert intern.
     * null-Felder im Draft werden ignoriert.
     */
    public Result<CustomerError, Void> update(CustomerUpdateDraft draft) {
        if (draft.name() != null) {
            switch (CustomerName.of(draft.name())) {
                case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
                case Success(var val) -> this.name = val;
            }
        }

        if (draft.phone() != null) {
            switch (ContactInfo.create(draft.phone(), draft.email(), draft.contactPerson())) {
                case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
                case Success(var val) -> this.contactInfo = val;
            }
        }

        if (draft.street() != null || draft.city() != null) {
            switch (Address.create(draft.street(), draft.houseNumber(), draft.postalCode(), draft.city(), draft.country())) {
                case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
                case Success(var val) -> this.billingAddress = val;
            }
        }

        if (draft.paymentDueDays() != null) {
            switch (PaymentTerms.create(draft.paymentDueDays(), draft.paymentDescription())) {
                case Failure(var msg) -> { return Result.failure(new CustomerError.ValidationFailure(msg)); }
                case Success(var val) -> this.paymentTerms = val;
            }
        }

        touch();
        return Result.success(null);
    }

    // ==================== Delivery Addresses ====================

    public void addDeliveryAddress(DeliveryAddress address) {
        this.deliveryAddresses.add(address);
        touch();
    }

    public void removeDeliveryAddress(String label) {
        this.deliveryAddresses.removeIf(da ->
                Objects.equals(da.label(), label));
        touch();
    }

    // ==================== Frame Contract ====================

    public Result<CustomerError, Void> setFrameContract(FrameContract contract) {
        if (this.type != CustomerType.B2B) {
            return Result.failure(new CustomerError.FrameContractNotAllowed());
        }
        this.frameContract = contract;
        touch();
        return Result.success(null);
    }

    public void removeFrameContract() {
        this.frameContract = null;
        touch();
    }

    public boolean hasActiveFrameContract() {
        return frameContract != null && frameContract.isActive();
    }

    public Optional<Money> getAgreedPrice(ArticleId articleId) {
        if (frameContract == null) return Optional.empty();
        return frameContract.getAgreedPrice(articleId);
    }

    // ==================== Preferences ====================

    public void addPreference(CustomerPreference preference) {
        this.preferences.add(preference);
        touch();
    }

    public void removePreference(CustomerPreference preference) {
        this.preferences.remove(preference);
        touch();
    }

    public void setPreferences(Set<CustomerPreference> newPreferences) {
        this.preferences.clear();
        this.preferences.addAll(newPreferences);
        touch();
    }

    // ==================== Status ====================

    public void activate() {
        this.status = CustomerStatus.ACTIVE;
        touch();
    }

    public void deactivate() {
        this.status = CustomerStatus.INACTIVE;
        touch();
    }

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

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

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

    public CustomerId id() { return id; }
    public CustomerName name() { return name; }
    public CustomerType type() { return type; }
    public Address billingAddress() { return billingAddress; }
    public ContactInfo contactInfo() { return contactInfo; }
    public PaymentTerms paymentTerms() { return paymentTerms; }
    public List<DeliveryAddress> deliveryAddresses() { return Collections.unmodifiableList(deliveryAddresses); }
    public FrameContract frameContract() { return frameContract; }
    public Set<CustomerPreference> preferences() { return Collections.unmodifiableSet(preferences); }
    public CustomerStatus status() { return status; }
    public OffsetDateTime createdAt() { return createdAt; }
    public OffsetDateTime updatedAt() { return updatedAt; }

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

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

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