/*
 * 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.util.function.Function;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.common.SafeCaller;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemStateConverter;
import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.library.items.StringItem;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.internal.CommunicationManager;
import org.openhab.core.thing.link.ItemChannelLink;
import org.openhab.core.thing.profiles.ProfileCallback;
import org.openhab.core.thing.util.ThingHandlerHelper;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.TimeSeries;
import org.openhab.core.types.TypeParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link ProfileCallback} implementation.
 *
 * @author Simon Kaufmann - Initial contribution
 */
@NonNullByDefault
public class ProfileCallbackImpl implements ProfileCallback {

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

    private final EventPublisher eventPublisher;
    private final ItemChannelLink link;
    private final Function<ThingUID, @Nullable Thing> thingProvider;
    private final Function<String, @Nullable Item> itemProvider;
    private final SafeCaller safeCaller;
    private final ItemStateConverter itemStateConverter;
    private final AcceptedTypeConverter acceptedTypeConverter;

    public ProfileCallbackImpl(EventPublisher eventPublisher, SafeCaller safeCaller,
            ItemStateConverter itemStateConverter, ItemChannelLink link,
            Function<ThingUID, @Nullable Thing> thingProvider, Function<String, @Nullable Item> itemProvider,
            AcceptedTypeConverter acceptedTypeConverter) {
        this.eventPublisher = eventPublisher;
        this.safeCaller = safeCaller;
        this.itemStateConverter = itemStateConverter;
        this.link = link;
        this.thingProvider = thingProvider;
        this.itemProvider = itemProvider;
        this.acceptedTypeConverter = acceptedTypeConverter;
    }

    @Override
    public ItemChannelLink getItemChannelLink() {
        return link;
    }

    @Override
    public void handleCommand(Command command) {
        Thing thing = thingProvider.apply(link.getLinkedUID().getThingUID());
        if (thing != null) {
            final ThingHandler handler = thing.getHandler();
            if (handler != null) {
                if (ThingHandlerHelper.isHandlerInitialized(thing)) {
                    logger.debug("Delegating command '{}' for item '{}' to handler for channel '{}'", command,
                            link.getItemName(), link.getLinkedUID());
                    Channel channel = thing.getChannel(link.getLinkedUID());
                    Command convertedCommand = acceptedTypeConverter.toAcceptedCommand(command, channel,
                            itemProvider.apply(link.getItemName()));
                    if (convertedCommand != null) {
                        safeCaller.create(handler, ThingHandler.class)
                                .withTimeout(CommunicationManager.THINGHANDLER_EVENT_TIMEOUT).onTimeout(() -> {
                                    logger.warn("Handler for thing '{}' takes more than {}ms for handling a command",
                                            handler.getThing().getUID(),
                                            CommunicationManager.THINGHANDLER_EVENT_TIMEOUT);
                                }).build().handleCommand(link.getLinkedUID(), command);
                    } else {
                        logger.debug(
                                "Not delegating command '{}' for item '{}' to handler for channel '{}', "
                                        + "because it was not possible to convert it to an accepted type).",
                                command, link.getItemName(), link.getLinkedUID());
                    }
                } else {
                    logger.debug("Not delegating command '{}' for item '{}' to handler for channel '{}', "
                            + "because handler is not initialized (thing must be in status UNKNOWN, ONLINE or OFFLINE but was {}).",
                            command, link.getItemName(), link.getLinkedUID(), thing.getStatus());
                }
            } else {
                logger.warn("Cannot delegate command '{}' for item '{}' to handler for channel '{}', "
                        + "because no handler is assigned. Maybe the binding is not installed or not "
                        + "propertly initialized.", command, link.getItemName(), link.getLinkedUID());
            }
        } else {
            logger.warn(
                    "Cannot delegate command '{}' for item '{}' to handler for channel '{}', "
                            + "because no thing with the UID '{}' could be found.",
                    command, link.getItemName(), link.getLinkedUID(), link.getLinkedUID().getThingUID());
        }
    }

    @Override
    public void sendCommand(Command command) {
        eventPublisher
                .post(ItemEventFactory.createCommandEvent(link.getItemName(), command, link.getLinkedUID().toString()));
    }

    @Override
    public void sendUpdate(State state) {
        Item item = itemProvider.apply(link.getItemName());
        if (item == null) {
            logger.warn("Cannot post update event '{}' for item '{}', because no item could be found.", state,
                    link.getItemName());
            return;
        }

        State acceptedState;
        if (state instanceof StringType && !(item instanceof StringItem)) {
            acceptedState = TypeParser.parseState(item.getAcceptedDataTypes(), state.toString());
            if (acceptedState == null) {
                acceptedState = state;
            }
        } else {
            acceptedState = itemStateConverter.convertToAcceptedState(state, item);
        }

        eventPublisher.post(
                ItemEventFactory.createStateEvent(link.getItemName(), acceptedState, link.getLinkedUID().toString()));
    }

    @Override
    public void sendTimeSeries(TimeSeries timeSeries) {
        Item item = itemProvider.apply(link.getItemName());
        if (item == null) {
            logger.warn("Cannot send time series event '{}' for item '{}', because no item could be found.", timeSeries,
                    link.getItemName());
            return;
        }

        eventPublisher.post(
                ItemEventFactory.createTimeSeriesEvent(link.getItemName(), timeSeries, link.getLinkedUID().toString()));
    }

    @FunctionalInterface
    public interface AcceptedTypeConverter {
        @Nullable
        Command toAcceptedCommand(Command originalType, @Nullable Channel channel, @Nullable Item item);
    }
}
