/**
 * Copyright © 2017 The Thingsboard Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.thingsboard.gateway.extensions.sigfox;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.gateway.extensions.sigfox.conf.SigfoxConfiguration;
import org.thingsboard.gateway.extensions.sigfox.conf.SigfoxDeviceTypeConfiguration;
import org.thingsboard.gateway.extensions.sigfox.conf.mapping.SigfoxDeviceDataConverter;
import org.thingsboard.gateway.service.GatewayService;
import org.thingsboard.gateway.service.MqttDeliveryFuture;
import org.thingsboard.gateway.service.data.DeviceData;
import org.thingsboard.gateway.util.ConfigurationTools;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

@Service
@Slf4j
@ConditionalOnProperty(prefix = "sigfox", value = "enabled", havingValue = "true")
public class DefaultSigfoxService implements SigfoxService {

    private static final int OPERATION_TIMEOUT_IN_SEC = 10;

    @Value("${sigfox.configuration}")
    private String configurationFile;

    @Autowired
    private GatewayService gateway;

    private Map<String, SigfoxDeviceTypeConfiguration> sigfoxDeviceTypeConfigurations;

    @PostConstruct
    public void init() throws IOException {
        SigfoxConfiguration configuration;
        try {
            configuration = ConfigurationTools.readConfiguration(configurationFile, SigfoxConfiguration.class);
            sigfoxDeviceTypeConfigurations = configuration
                    .getDeviceTypeConfigurations()
                    .stream()
                    .collect(Collectors.toMap(SigfoxDeviceTypeConfiguration::getDeviceTypeId, Function.identity()));
        } catch (IOException e) {
            log.error("Sigfox service configuration failed!", e);
            throw e;
        }
    }

    @Override
    public void processRequest(String deviceTypeId, String token, String body) throws Exception {
        log.trace("Processing request body [{}] for deviceTypeId [{}] and token [{}]", body, deviceTypeId, token);
        SigfoxDeviceTypeConfiguration configuration = sigfoxDeviceTypeConfigurations.get(deviceTypeId);
        if (configuration != null) {
            if (configuration.getToken().equals(token)) {
                processBody(body, configuration);
            } else {
                log.error("Request token [{}] for device type id [{}] doesn't match configuration token!", token, deviceTypeId);
                throw new SecurityException("Request token [" + token + "] for device type id [" + deviceTypeId + "] doesn't match configuration token!");
            }
        } else {
            log.error("No configuration found for device type id [{}]. Please add configuration for this device type id!", deviceTypeId);
            throw new IllegalArgumentException("No configuration found for device type id [" + deviceTypeId + "]. Please add configuration for this device type id!");
        }
    }

    private void processBody(String body, SigfoxDeviceTypeConfiguration configuration) throws Exception {
        for (SigfoxDeviceDataConverter converter : configuration.getConverters()) {
            if (converter.isApplicable(body)) {
                DeviceData dd = converter.parseBody(body);
                if (dd != null) {
                    waitWithTimeout(gateway.onDeviceConnect(dd.getName()));
                    List<MqttDeliveryFuture> futures = new ArrayList<>();
                    if (!dd.getAttributes().isEmpty()) {
                        futures.add(gateway.onDeviceAttributesUpdate(dd.getName(), dd.getAttributes()));
                    }
                    if (!dd.getTelemetry().isEmpty()) {
                        futures.add(gateway.onDeviceTelemetry(dd.getName(), dd.getTelemetry()));
                    }
                    for (Future future : futures) {
                        waitWithTimeout(future);
                    }
                    Optional<MqttDeliveryFuture> future = gateway.onDeviceDisconnect(dd.getName());
                    if (future.isPresent()) {
                        waitWithTimeout(future.get());
                    }
                } else {
                    log.error("DeviceData is null. Body [{}] was not parsed successfully!", body);
                    throw new IllegalArgumentException("Device Data is null. Body [" + body + "] was not parsed successfully!");
                }
            }
        }
    }

    private void waitWithTimeout(Future future) throws Exception {
        future.get(OPERATION_TIMEOUT_IN_SEC, TimeUnit.SECONDS);
    }
}
