/*  Copyright (C) 2023-2024 José Rebelo

    This file is part of Gadgetbridge.

    Gadgetbridge is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Gadgetbridge is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services;

import android.annotation.SuppressLint;
import android.widget.Toast;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;

public class ZeppOsLogsService extends AbstractZeppOsService {
    private static final Logger LOG = LoggerFactory.getLogger(ZeppOsLogsService.class);

    private static final short ENDPOINT = 0x003a;

    public static final byte CMD_CAPABILITIES_REQUEST = 0x01;
    public static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
    public static final byte CMD_LOGS_START = 0x03;
    public static final byte CMD_LOGS_START_ACK = 0x04;
    public static final byte CMD_LOGS_STOP = 0x05;
    public static final byte CMD_LOGS_STOP_ACK = 0x06;
    public static final byte CMD_LOGS_DATA = 0x07;
    public static final byte CMD_UNKNOWN_8 = 0x08;
    public static final byte CMD_UNKNOWN_9 = 0x09;

    public static final String PREF_VERSION = "zepp_os_logs_version";

    private String logsType;
    private final Set<Byte> sessions = new HashSet<>();

    @SuppressLint("SimpleDateFormat")
    private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

    public ZeppOsLogsService(final ZeppOsSupport support) {
        super(support, false);
    }

    @Override
    public short getEndpoint() {
        return ENDPOINT;
    }

    @Override
    public void handlePayload(final byte[] payload) {
        switch (payload[0]) {
            case CMD_CAPABILITIES_RESPONSE:
                handleCapabilitiesResponse(payload);
                break;
            case CMD_LOGS_START_ACK:
                if (payload[1] != 1) {
                    LOG.warn("Failed to start logs, status = {}", payload[1]);
                    GB.toast(getContext(), "Failed to start logs", Toast.LENGTH_SHORT, GB.WARN);
                    return;
                }
                final byte sessionId = payload[2];
                LOG.info("Got logs start ack, sessionId = {}", sessionId);
                GB.toast(getContext(), "App logs started", Toast.LENGTH_SHORT, GB.INFO);
                sessions.add(sessionId);
                break;
            case CMD_LOGS_STOP_ACK:
                LOG.info("Got logs stop ack, status = {}", payload[1]);
                GB.toast(getContext(), "App logs stopped", Toast.LENGTH_SHORT, GB.INFO);
                break;
            case CMD_LOGS_DATA:
                handleLogsData(payload);
                break;
            case CMD_UNKNOWN_8:
                LOG.info("Got unknown 8, replying with unknown 9");
                write("reply logs unknown 9", CMD_UNKNOWN_9);
                break;
            default:
                LOG.warn("Unexpected logs payload byte {}", String.format("0x%02x", payload[0]));
        }
    }

    @Override
    public boolean onSendConfiguration(final String config, final Prefs prefs) {
        switch (config) {
            case DeviceSettingsPreferenceConst.PREF_APP_LOGS_START:
                start();
                return true;
            case DeviceSettingsPreferenceConst.PREF_APP_LOGS_STOP:
                stop();
                return true;
        }

        return false;
    }

    @Override
    public void initialize(final TransactionBuilder builder) {
        requestCapabilities(builder);
    }

    public void requestCapabilities(final TransactionBuilder builder) {
        write(builder, CMD_CAPABILITIES_REQUEST);
    }

    public void start() {
        if (logsType == null) {
            LOG.error("logsType is null");
            return;
        }

        LOG.info("Starting logs");

        final byte[] logsTypeBytes = logsType.getBytes(StandardCharsets.UTF_8);
        final ByteBuffer buf = ByteBuffer.allocate(1 + logsTypeBytes.length + 1)
                .order(ByteOrder.LITTLE_ENDIAN);

        buf.put(CMD_LOGS_START);
        buf.put(logsTypeBytes);
        buf.put((byte) 0);

        write("start logs", buf.array());
    }

    public void stop() {
        LOG.info("Stopping {} log sessions", sessions.size());

        for (final Byte session : sessions) {
            stop(session);
        }
    }

    private void stop(final byte sessionId) {
        final ByteBuffer buf = ByteBuffer.allocate(3)
                .order(ByteOrder.LITTLE_ENDIAN);

        buf.put(CMD_LOGS_STOP);
        buf.put((byte) 0);
        buf.put(sessionId);

        write("stop logs session " + sessionId, buf.array());
    }

    private void handleCapabilitiesResponse(final byte[] payload) {
        final int version = payload[1] & 0xFF;
        if (version != 1) {
            LOG.warn("Unsupported logs service version {}", version);
            return;
        }
        final byte var1 = payload[2];
        if (var1 != 1) {
            LOG.warn("Unexpected value for var1 '{}'", var1);
        }
        final byte var2 = payload[3];
        if (var2 != 0) {
            LOG.warn("Unexpected value for var2 '{}'", var2);
        }

        logsType = StringUtils.untilNullTerminator(payload, 4);

        LOG.info("Logs version={}, var1={}, var2={}, logsType={}", version, var1, var2, logsType);

        getSupport().evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_VERSION, version));
    }

    private void handleLogsData(final byte[] payload) {
        final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
        buf.get(); // discard first byte
        final byte index = buf.get();
        final byte sessionId = buf.get();
        if (!sessions.contains(sessionId)) {
            LOG.warn("Got log data for unknown session {}", sessionId);
        }
        final String appIdDecimal = StringUtils.untilNullTerminator(buf);
        final byte unknown1 = buf.get();
        if (unknown1 != 0) {
            LOG.warn("Unexpected value for unknown1 = {}", unknown1);
        }
        final long timestampMillis = buf.getLong();
        final byte unknown2 = buf.get();
        if (unknown2 != 2) {
            LOG.warn("Unexpected value for unknown2 = {}", unknown2);
        }
        final String message = StringUtils.untilNullTerminator(buf);
        if (buf.position() < buf.limit()) {
            LOG.warn("There are {} log data bytes still in the buffer", (buf.limit() - buf.position()));
        }

        LOG.info(
                "Log entry - {} [{}] - {}",
                sdf.format(new Date(timestampMillis)),
                appIdDecimal,
                message
        );
    }

    public static boolean isSupported(final Prefs devicePrefs) {
        return devicePrefs.getInt(PREF_VERSION, 0) == 1;
    }
}
