/*
 * Copyright (c) 2010-2025 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.core.thing.internal.profiles;

import java.math.BigDecimal;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.profiles.ProfileContext;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.thing.profiles.StateProfile;
import org.openhab.core.thing.profiles.SystemProfiles;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.Type;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tech.units.indriya.AbstractUnit;

/***
 * This is the default implementation for a {@link SystemHysteresisStateProfile}.
 *
 * @author Christoph Weitkamp - Initial contribution
 */
@NonNullByDefault
public class SystemHysteresisStateProfile implements StateProfile {

    static final String LOWER_PARAM = "lower";
    private static final String UPPER_PARAM = "upper";
    private static final String INVERTED_PARAM = "inverted";

    private final Logger logger = LoggerFactory.getLogger(SystemHysteresisStateProfile.class);

    private final ProfileCallback callback;

    private final QuantityType<?> lower;
    private final QuantityType<?> upper;
    private final OnOffType low;
    private final OnOffType high;

    private Type previousType = UnDefType.UNDEF;

    public SystemHysteresisStateProfile(ProfileCallback callback, ProfileContext context) {
        this.callback = callback;

        final QuantityType<?> lowerParam = getParam(context, LOWER_PARAM);
        if (lowerParam == null) {
            throw new IllegalArgumentException(String.format("Parameter '%s' is not a Number value.", LOWER_PARAM));
        }
        this.lower = lowerParam;
        final QuantityType<?> upperParam = getParam(context, UPPER_PARAM);
        final QuantityType<?> convertedUpperParam = upperParam == null ? lower
                : upperParam.toInvertibleUnit(lower.getUnit());
        if (convertedUpperParam == null) {
            throw new IllegalArgumentException(
                    String.format("Units of parameters '%s' and '%s' are not compatible: %s != %s", LOWER_PARAM,
                            UPPER_PARAM, lower, upperParam));
        }
        this.upper = convertedUpperParam;

        final Object paramValue = context.getConfiguration().get(INVERTED_PARAM);
        final boolean inverted = paramValue != null && Boolean.parseBoolean(paramValue.toString());
        this.low = OnOffType.from(inverted);
        this.high = OnOffType.from(!inverted);
    }

    private @Nullable QuantityType<?> getParam(ProfileContext context, String param) {
        final Object paramValue = context.getConfiguration().get(param);
        logger.debug("Configuring profile with {} parameter '{}'", param, paramValue);
        if (paramValue instanceof String string) {
            try {
                return new QuantityType<>(string);
            } catch (IllegalArgumentException e) {
                logger.error("Cannot convert value '{}' of parameter {} into a valid QuantityType.", paramValue, param);
            }
        } else if (paramValue instanceof BigDecimal value) {
            return QuantityType.valueOf(value.doubleValue(), AbstractUnit.ONE);
        }
        return null;
    }

    @Override
    public ProfileTypeUID getProfileTypeUID() {
        return SystemProfiles.HYSTERESIS;
    }

    @Override
    public void onStateUpdateFromItem(State state) {
        // do nothing
    }

    @Override
    public void onCommandFromHandler(Command command) {
        final Type mappedCommand = mapValue(command);
        logger.trace("Mapped command from '{}' to command '{}'.", command, mappedCommand);
        if (mappedCommand instanceof Command command1) {
            callback.sendCommand(command1);
        }
    }

    @Override
    public void onCommandFromItem(Command command) {
        // do nothing
    }

    @Override
    public void onStateUpdateFromHandler(State state) {
        final Type mappedState = mapValue(state);
        logger.trace("Mapped state from '{}' to state '{}'.", state, mappedState);
        if (mappedState instanceof State state1) {
            callback.sendUpdate(state1);
        }
    }

    private Type mapValue(Type value) {
        if (value instanceof QuantityType qtState) {
            final QuantityType<?> finalLower;
            final QuantityType<?> finalUpper;
            if (Units.ONE.equals(lower.getUnit()) && Units.ONE.equals(upper.getUnit())) {
                // allow bounds without unit -> implicitly assume its the same as the one from the state, but warn
                // the user
                if (!Units.ONE.equals(qtState.getUnit())) {
                    logger.warn(
                            "Received a QuantityType '{}' with unit, but the boundaries are defined as a plain number without units (lower={}, upper={}), please consider adding units to them.",
                            value, lower, upper);
                }
                finalLower = new QuantityType<>(lower.toBigDecimal(), qtState.getUnit());
                finalUpper = new QuantityType<>(upper.toBigDecimal(), qtState.getUnit());
            } else {
                finalLower = lower.toInvertibleUnit(qtState.getUnit());
                finalUpper = upper.toInvertibleUnit(qtState.getUnit());
                if (finalLower == null || finalUpper == null) {
                    logger.warn(
                            "Cannot compare state '{}' to boundaries because units (lower={}, upper={}) do not match.",
                            qtState, lower, upper);
                    return previousType;
                }
            }
            return previousType = mapValue(finalLower.doubleValue(), finalUpper.doubleValue(), qtState.doubleValue());
        } else if (value instanceof DecimalType type) {
            return previousType = mapValue(lower.doubleValue(), upper.doubleValue(), type.doubleValue());
        }
        return previousType;
    }

    private Type mapValue(double lower, double upper, double value) {
        if (value <= lower) {
            return low;
        } else if (value >= upper) {
            return high;
        }
        return previousType;
    }
}
