//-----------------------------------------------------------------------------------
//
// Distributed under MIT Licence
//   See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
//
//-----------------------------------------------------------------------------------
//
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
// tested on a Venu 2 device. The source code is provided at:
//            https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey, 10 January 2024
//
//
// Description:
//
// Home Assistant Webhook creation.
//
// Reference:
//  * https://developers.home-assistant.io/docs/api/native-app-integration
//
//-----------------------------------------------------------------------------------

using Toybox.Lang;
using Toybox.Communications;
using Toybox.System;
using Toybox.WatchUi;

// Can use push view so must never be run in a glance context
class WebhookManager {

    function onReturnRequestWebhookId(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String) as Void {
        switch (responseCode) {
            case Communications.BLE_HOST_TIMEOUT:
            case Communications.BLE_CONNECTION_UNAVAILABLE:
                // System.println("WebhookManager onReturnRequestWebhookId() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
                break;

            case Communications.BLE_QUEUE_FULL:
                // System.println("WebhookManager onReturnRequestWebhookId() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
                break;

            case Communications.NETWORK_REQUEST_TIMED_OUT:
                // System.println("WebhookManager onReturnRequestWebhookId() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
                break;

            case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
                // System.println("WebhookManager onReturnRequestWebhookId() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
                // Ignore and see if we can carry on
                break;
            case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
                // System.println("WebhookManager onReturnRequestWebhookId() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
                Settings.unsetIsSensorsLevelEnabled();
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoJson) as Lang.String);
                break;

            case 404:
                // System.println("WebhookManager onReturnRequestWebhookId() Response Code: 404, page not found. Check API URL setting.");
                Settings.unsetIsSensorsLevelEnabled();
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiUrlNotFound) as Lang.String);
                break;

            case 200:
            case 201:
                var id = data.get("webhook_id") as Lang.String or Null;
                if (id != null) {
                    Settings.setWebhookId(id);
                    // System.println("WebhookManager onReturnRegisterWebhookSensor(): Registering first sensor: Battery Level");
                    registerWebhookSensors();
                } else {
                    // System.println("WebhookManager onReturnRequestWebhookId(): No webhook id in response data.");
                    Settings.unsetIsSensorsLevelEnabled();
                    ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + ".");
                }
                break;

            default:
                // System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
                Settings.unsetIsSensorsLevelEnabled();
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + responseCode);
        }
    }

    function requestWebhookId() {
        var deviceSettings = System.getDeviceSettings();
        // System.println("WebhookManager requestWebhookId(): Requesting webhook id for device = " + deviceSettings.uniqueIdentifier);
        Communications.makeWebRequest(
            Settings.getApiUrl() + "/mobile_app/registrations",
            {
                "device_id"           => deviceSettings.uniqueIdentifier,
                "app_id"              => "garmin_home_assistant",
                "app_name"            => WatchUi.loadResource($.Rez.Strings.AppName) as Lang.String,
                "app_version"         => "",
                "device_name"         => "Garmin Device",
                "manufacturer"        => "Garmin",
                // An unhelpful part number that can be translated to a familiar model name.
                "model"               => deviceSettings.partNumber,
                "os_name"             => "",
                "os_version"          => Lang.format("$1$.$2$", deviceSettings.firmwareVersion),
                "supports_encryption" => false,
                "app_data"            => {}
            },
            {
                :method       => Communications.HTTP_REQUEST_METHOD_POST,
                :headers      => {
                    "Content-Type"  => Communications.REQUEST_CONTENT_TYPE_JSON,
                    "Authorization" => "Bearer " + Settings.getApiKey()
                },
                :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
            },
            method(:onReturnRequestWebhookId)
        );
    }

    function onReturnRegisterWebhookSensor(responseCode as Lang.Number, data as Null or Lang.Dictionary or Lang.String, sensors as Lang.Array<Lang.Object>) as Void {
        switch (responseCode) {
            case Communications.BLE_HOST_TIMEOUT:
            case Communications.BLE_CONNECTION_UNAVAILABLE:
                // System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: BLE_HOST_TIMEOUT or BLE_CONNECTION_UNAVAILABLE, Bluetooth connection severed.");
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoPhone) as Lang.String);
                break;

            case Communications.BLE_QUEUE_FULL:
                // System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: BLE_QUEUE_FULL, API calls too rapid.");
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.ApiFlood) as Lang.String);
                break;

            case Communications.NETWORK_REQUEST_TIMED_OUT:
                // System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: NETWORK_REQUEST_TIMED_OUT, check Internet connection.");
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.NoResponse) as Lang.String);
                break;

            case Communications.NETWORK_RESPONSE_OUT_OF_MEMORY:
                // System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: NETWORK_RESPONSE_OUT_OF_MEMORY, are we going too fast?");
                // Ignore and see if we can carry on
                break;

            case Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE:
                // System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: INVALID_HTTP_BODY_IN_NETWORK_RESPONSE, check JSON is returned.");
                // Webhook ID might have been deleted on Home Assistant server
                Settings.unsetWebhookId();
                // System.println("WebhookManager onReturnRegisterWebhookSensor(): Webhook ID invalid, going full chain.");
                requestWebhookId();
                break;

            case 404:
                // System.println("WebhookManager onReturnRegisterWebhookSensor() Response Code: 404, page not found. Check API URL setting.");
                // Webhook ID might have been deleted on Home Assistant server
                Settings.unsetWebhookId();
                // System.println("WebhookManager onReturnRegisterWebhookSensor(): Webhook ID invalid, going full chain.");
                requestWebhookId();
                break;

            case 200:
            case 201:
                if (data instanceof Lang.Dictionary) {
                    var d = data as Lang.Dictionary;
                    var b = d.get("success") as Lang.Boolean or Null;
                    if (b != null and b != false) {
                        if (sensors.size() == 0) {
                            getApp().startUpdates();
                        } else {
                            registerWebhookSensor(sensors);
                        }
                    } else {
                        // System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure, no 'success'.");
                        Settings.unsetWebhookId();
                        Settings.unsetIsSensorsLevelEnabled();
                        ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + ".");
                    }
                } else {
                    // !! Speculative code for an application crash !!
                    // System.println("WebhookManager onReturnRegisterWebhookSensor(): Failure, not a Lang.Dict");
                    // Webhook ID might have been deleted on Home Assistant server and a Lang.String is trying to tell us an error message
                    Settings.unsetWebhookId();
                    Settings.unsetIsSensorsLevelEnabled();
                    if (data == null) {
                        ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\nNull data");
                    } else {
                        // All objects have toString()
                        ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + data.toString());
                    }
                }
                break;

            default:
                // System.println("WebhookManager onReturnRequestWebhookId(): Unhandled HTTP response code = " + responseCode);
                Settings.unsetWebhookId();
                Settings.unsetIsSensorsLevelEnabled();
                ErrorView.show(WatchUi.loadResource($.Rez.Strings.WebhookFailed) as Lang.String + "\n" + WatchUi.loadResource($.Rez.Strings.UnhandledHttpErr) as Lang.String + " " + responseCode);
        }
    }

    function registerWebhookSensor(sensors as Lang.Array<Lang.Object>) {
        var url = Settings.getApiUrl() + "/webhook/" + Settings.getWebhookId();
        // System.println("WebhookManager registerWebhookSensor(): Registering webhook sensor: " + sensor.toString());
        // System.println("WebhookManager registerWebhookSensor(): URL=" + url);
        // https://developers.home-assistant.io/docs/api/native-app-integration/sensors/#registering-a-sensor
        Communications.makeWebRequest(
            url,
            {
                "type" => "register_sensor",
                "data" => sensors[0]
            },
            {
                :method       => Communications.HTTP_REQUEST_METHOD_POST,
                :headers      => {
                    "Content-Type" => Communications.REQUEST_CONTENT_TYPE_JSON
                },
                :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON,
                :context      => sensors.slice(1, null)
            },
            method(:onReturnRegisterWebhookSensor)
        );
    }

    function registerWebhookSensors() {
        var heartRate = Activity.getActivityInfo().currentHeartRate;

        var sensors = [
            {
                "device_class"        => "battery",
                "name"                => "Battery Level",
                "state"               => System.getSystemStats().battery,
                "type"                => "sensor",
                "unique_id"           => "battery_level",
                "icon"                => "mdi:battery",
                "unit_of_measurement" => "%",
                "state_class"         => "measurement",
                "entity_category"     => "diagnostic",
                "disabled"            => !Settings.isSensorsLevelEnabled()
            },
            {
                "device_class"        => "battery_charging",
                "name"                => "Battery is Charging",
                "state"               => System.getSystemStats().charging,
                "type"                => "binary_sensor",
                "unique_id"           => "battery_is_charging",
                "icon"                => System.getSystemStats().charging ? "mdi:battery-plus" : "mdi:battery-minus",
                "entity_category"     => "diagnostic",
                "disabled"            => !Settings.isSensorsLevelEnabled()
            },
            {
                "name"                => "Heart rate",
                "state"               => heartRate == null ? "unknown" : heartRate,
                "type"                => "sensor",
                "unique_id"           => "heart_rate",
                "icon"                => "mdi:heart-pulse",
                "unit_of_measurement" => "bpm",
                "state_class"         => "measurement",
                "disabled"            => !Settings.isSensorsLevelEnabled()
            }
        ];

        if (Toybox has :ActivityMonitor) {
            // System.println("WebhookManager registerWebhookSensors(): has ActivityMonitor class");
            var activityInfo = ActivityMonitor.getInfo();
            sensors.add({
                "name"                => "Steps today",
                "state"               => activityInfo.steps == null ? "unknown" : activityInfo.steps,
                "type"                => "sensor",
                "unique_id"           => "steps_today",
                "icon"                => "mdi:walk",
                "state_class"         => "total",
                "disabled"            => !Settings.isSensorsLevelEnabled()
            });

            if (ActivityMonitor.Info has :floorsClimbed) {
                sensors.add({
                    "name"                => "Floors climbed today",
                    "state"               => activityInfo.floorsClimbed == null ? "unknown" : activityInfo.floorsClimbed,
                    "type"                => "sensor",
                    "unique_id"           => "floors_climbed_today",
                    "icon"                => "mdi:stairs-up",
                    "state_class"         => "total",
                    "disabled"            => !Settings.isSensorsLevelEnabled()
                });
            }

            if (ActivityMonitor.Info has :floorsDescended) {
                sensors.add({
                    "name"                => "Floors descended today",
                    "state"               => activityInfo.floorsDescended == null ? "unknown" : activityInfo.floorsDescended,
                    "type"                => "sensor",
                    "unique_id"           => "floors_descended_today",
                    "icon"                => "mdi:stairs-down",
                    "state_class"         => "total",
                    "disabled"            => !Settings.isSensorsLevelEnabled()
                });
            }

            if (ActivityMonitor.Info has :respirationRate) {
                sensors.add({
                    "name"                => "Respiration rate",
                    "state"               => activityInfo.respirationRate == null ? "unknown" : activityInfo.respirationRate,
                    "type"                => "sensor",
                    "unique_id"           => "respiration_rate",
                    "icon"                => "mdi:lungs",
                    "unit_of_measurement" => "bpm",
                    "state_class"         => "measurement",
                    "disabled"            => !Settings.isSensorsLevelEnabled()
                });
            }
        } else {
            //System.println("WebhookManager registerWebhookSensors(): has no ActivityMonitor class");
        }

        if (Activity has :getProfileInfo) {
            var activity     = Activity.getProfileInfo().sport;
            var sub_activity = Activity.getProfileInfo().subSport;

            if ((Activity.getActivityInfo() != null) and
                ((Activity.getActivityInfo().elapsedTime == null) or
                    (Activity.getActivityInfo().elapsedTime == 0))) {
                // Indicate no activity with -1, not part of Garmin's activity codes.
                // https://developer.garmin.com/connect-iq/api-docs/Toybox/Activity.html#Sport-module
                activity     = -1;
                sub_activity = -1;
            }
            sensors.add({
                "name"      => "Activity",
                "state"     => activity,
                "type"      => "sensor",
                "unique_id" => "activity",
                "disabled"  => !Settings.isSensorsLevelEnabled()
            });
            sensors.add({
                "name"      => "Sub-activity",
                "state"     => sub_activity,
                "type"      => "sensor",
                "unique_id" => "sub_activity",
                "disabled"  => !Settings.isSensorsLevelEnabled()
            });
        }

        registerWebhookSensor(sensors);
    }

}
