// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace Azure.Iot.Operations.Protocol.Connection
{
    public class MqttConnectionSettings
    {
        private const int DefaultTcpPort = 8883;
        private const bool DefaultUseTls = true;
        private const bool DefaultCleanStart = false;

        private static readonly TimeSpan s_defaultKeepAlive = TimeSpan.FromSeconds(60);
        private static readonly TimeSpan s_defaultSessionExpiry = TimeSpan.FromSeconds(3600);

        /// <summary>
        /// The fully qualified domain name of the MQTT broker.
        /// </summary>
        public string HostName { get; set; }

        /// <summary>
        /// The TCP port to use when connecting to the MQTT broker.
        /// </summary>
        public int TcpPort { get; set; } = DefaultTcpPort;

        /// <summary>
        /// Enable TLS negotiation (disabling not recommended for production).
        /// </summary>
        public bool UseTls { get; set; } = DefaultUseTls;

        /// <summary>
        /// Path to a PEM file to validate server identity.
        /// </summary>
        public string? CaFile { get; set; }

        /// <summary>
        /// If true, the client will connect to the MQTT broker without attempting to resume an MQTT session. If false, the client
        /// will try to resume an MQTT session if one is present.
        /// </summary>
        public bool CleanStart { get; set; } = DefaultCleanStart;

        /// <summary>
        /// The MQTT connection-level keep alive period.
        /// </summary>
        /// <remarks>
        /// Higher values cause less network traffic, but lower values lead to faster responses to connection loss.
        /// </remarks>
        public TimeSpan KeepAlive { get; set; } = s_defaultKeepAlive;

        /// <summary>
        /// The MQTT client Id that this client will use when connecting.
        /// </summary>
        public string ClientId { get; set; }

        /// <summary>
        /// The session expiry period that this client's connections will use.
        /// </summary>
        /// <remarks>
        /// Longer periods allow for the client to reconnect and re-establish the MQTT session after longer delays.
        /// </remarks>
        public TimeSpan SessionExpiry { get; set; } = s_defaultSessionExpiry;

        /// <summary>
        /// The MQTT connection username to include in the CONNECT packet.
        /// </summary>
        public string? Username { get; set; }

        /// <summary>
        /// The path to the file that contains the MQTT connection password to include in the CONNECT packet.
        /// </summary>
        public string? PasswordFile { get; set; }

        /// <summary>
        /// Path to a PEM file to establish X509 client authentication
        /// </summary>
        public string? CertFile { get; set; }

        /// <summary>
        /// Path to a KEY file to establish X509 client authentication
        /// </summary>
        public string? KeyFile { get; set; }

        /// <summary>
        /// The file containing the password for accessing <see cref="KeyFile"/>.
        /// </summary>
        public string? KeyPasswordFile { get; set; }

        /// <summary>
        /// The client certificate for X509 authentication.
        /// </summary>
        public X509Certificate2? ClientCertificate { get; set; }

        /// <summary>
        /// The certificate authority to trust.
        /// </summary>
        /// <remarks>
        /// If provided, this field supercedes any value provided in <see cref="CaFile"/>.
        /// </remarks>
        public X509Certificate2Collection? TrustChain { get; set; }

        /// <summary>
        /// The file that contains the shared access token used for authentication. Only applicable for applications deployed in kubernetes pods.
        /// </summary>
        public string? SatAuthFile { get; set; }

        /// <summary>
        /// The upper bound of concurrent in-flight QoS 1 or QoS 2 messages that the broker will be allowed to send to this client.
        /// </summary>
        public ushort? ReceiveMaximum { get; set; }

        public MqttConnectionSettings(string hostname, string clientId)
            : this(new Dictionary<string, string> { { nameof(HostName), hostname }, {nameof(ClientId), clientId} }, false)
        {
        }

        protected MqttConnectionSettings(IDictionary<string, string> connectionSettings, bool validateOptionalSettings, bool isSettingFromConnStr = false)
        {
            try
            {
                HostName = GetStringValue(connectionSettings, nameof(HostName)) ?? string.Empty;
                ClientId = GetStringValue(connectionSettings, nameof(ClientId)) ?? string.Empty;
                CertFile = GetStringValue(connectionSettings, nameof(CertFile));
                KeyFile = GetStringValue(connectionSettings, nameof(KeyFile));
                KeyPasswordFile = GetStringValue(connectionSettings, nameof(KeyPasswordFile));
                Username = GetStringValue(connectionSettings, nameof(Username));
                PasswordFile = GetStringValue(connectionSettings, nameof(PasswordFile));
                KeepAlive = GetTimeSpanValue(connectionSettings, nameof(KeepAlive), s_defaultKeepAlive);
                CleanStart = GetBooleanValue(connectionSettings, nameof(CleanStart), DefaultCleanStart);
                SessionExpiry = GetTimeSpanValue(connectionSettings, nameof(SessionExpiry), s_defaultSessionExpiry);
                TcpPort = GetPositiveIntValueOrDefault(connectionSettings, nameof(TcpPort), DefaultTcpPort);
                UseTls = GetBooleanValue(connectionSettings, nameof(UseTls), DefaultUseTls);
                CaFile = GetStringValue(connectionSettings, nameof(CaFile));
                SatAuthFile = GetStringValue(connectionSettings, nameof(SatAuthFile));

                ValidateMqttSettings(validateOptionalSettings);
            }
            catch (ArgumentException ex)
            {
                Debug.Assert(ex.ParamName != null);
                _ = connectionSettings.TryGetValue(ex.ParamName, out string? paramValue);

                throw AkriMqttException.GetConfigurationInvalidException(
                    ex.ParamName,
                    paramValue ?? string.Empty,
                    isSettingFromConnStr ? "Invalid settings in provided Connection String: " + ex.Message : ex.Message,
                    ex);
            }
        }

        public static MqttConnectionSettings FromEnvVars()
        {
            string? hostname = Environment.GetEnvironmentVariable("AIO_BROKER_HOSTNAME");

            if (string.IsNullOrEmpty(hostname))
            {
                throw AkriMqttException.GetConfigurationInvalidException(
                    "AIO_BROKER_HOSTNAME",
                    string.Empty,
                    "Invalid settings in provided Environment Variables: 'AIO_BROKER_HOSTNAME' is missing.");
            }

            string? tcpPort = Environment.GetEnvironmentVariable("AIO_BROKER_TCP_PORT");

            string? clientId = Environment.GetEnvironmentVariable("AIO_MQTT_CLIENT_ID");
            if (string.IsNullOrEmpty(clientId))
            {
                throw AkriMqttException.GetConfigurationInvalidException(
                    "AIO_MQTT_CLIENT_ID",
                    string.Empty,
                    "Invalid settings in provided Environment Variables: 'AIO_MQTT_CLIENT_ID' is missing.");
            }

            string? certFile = Environment.GetEnvironmentVariable("AIO_TLS_CERT_FILE");
            string? keyFile = Environment.GetEnvironmentVariable("AIO_TLS_KEY_FILE");
            string? username = Environment.GetEnvironmentVariable("AIO_MQTT_USERNAME");
            string? passwordFile = Environment.GetEnvironmentVariable("AIO_MQTT_PASSWORD_FILE");
            string? keepAlive = Environment.GetEnvironmentVariable("AIO_MQTT_KEEP_ALIVE");
            string? sessionExpiry = Environment.GetEnvironmentVariable("AIO_MQTT_SESSION_EXPIRY");
            string? cleanStart = Environment.GetEnvironmentVariable("AIO_MQTT_CLEAN_START");
            string? useTls = Environment.GetEnvironmentVariable("AIO_MQTT_USE_TLS");
            string? caFile = Environment.GetEnvironmentVariable("AIO_TLS_CA_FILE");
            string? keyPasswordFile = Environment.GetEnvironmentVariable("AIO_TLS_KEY_PASSWORD_FILE");
            string? satAuthFile = Environment.GetEnvironmentVariable("AIO_SAT_FILE");

            try
            {
                return new MqttConnectionSettings(hostname, clientId)
                {
                    CertFile = certFile,
                    KeyFile = keyFile,
                    Username = username,
                    PasswordFile = passwordFile,
                    KeepAlive = string.IsNullOrEmpty(keepAlive) ? s_defaultKeepAlive : TimeSpan.FromSeconds(int.Parse(keepAlive, CultureInfo.InvariantCulture)),
                    SessionExpiry = string.IsNullOrEmpty(sessionExpiry) ? s_defaultSessionExpiry : TimeSpan.FromSeconds(int.Parse(sessionExpiry, CultureInfo.InvariantCulture)),
                    CleanStart = string.IsNullOrEmpty(cleanStart) || CheckForValidBooleanInput(nameof(CleanStart), cleanStart),
                    TcpPort = string.IsNullOrEmpty(tcpPort) ? DefaultTcpPort : CheckForValidIntegerInput(nameof(TcpPort), tcpPort),
                    UseTls = string.IsNullOrEmpty(useTls) || CheckForValidBooleanInput(nameof(UseTls), useTls),
                    CaFile = caFile,
                    KeyPasswordFile = string.IsNullOrEmpty(keyPasswordFile) ? null : File.ReadAllText(keyPasswordFile).Trim(),
                    SatAuthFile = satAuthFile
                };
            }
            catch (ArgumentException ex)
            {
                throw AkriMqttException.GetConfigurationInvalidException(ex.ParamName!, string.Empty, "Invalid settings in provided Environment Variables: " + ex.Message, ex);
            }
        }

        public static MqttConnectionSettings FromConnectionString(string connectionString)
        {
            IDictionary<string, string> map = connectionString.ToDictionary(';', '=');
            return new MqttConnectionSettings(map, true, true);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly")]
        protected internal void ValidateMqttSettings(bool validateOptionalSettings)
        {
            if (string.IsNullOrWhiteSpace(HostName))
            {
                throw new ArgumentException($"{nameof(HostName)} is mandatory.", nameof(HostName));
            }

            if (string.IsNullOrEmpty(ClientId))
            {
                throw new ArgumentException($"{nameof(ClientId)} is mandatory.", nameof(ClientId));
            }

            if (!string.IsNullOrEmpty(SatAuthFile) && (!string.IsNullOrEmpty(PasswordFile)))
            {
                throw new ArgumentException(
                    $"{nameof(SatAuthFile)} cannot be used with {nameof(PasswordFile)}", nameof(SatAuthFile));
            }

            if (validateOptionalSettings)
            {
                if (!string.IsNullOrWhiteSpace(KeyFile) && string.IsNullOrWhiteSpace(CertFile))
                {
                    throw new ArgumentException(
                        $"{nameof(CertFile)} and {nameof(KeyFile)} need to be provided together.",
                        $"{nameof(CertFile)} and {nameof(KeyFile)}");
                }
            }

            if (!string.IsNullOrWhiteSpace(CertFile))
            {
                ClientCertificate = string.IsNullOrWhiteSpace(KeyPasswordFile)
                    ? X509Certificate2.CreateFromPemFile(CertFile, KeyFile)
                    : X509Certificate2.CreateFromEncryptedPemFile(CertFile, KeyPasswordFile, KeyFile);
            }

            if (!string.IsNullOrWhiteSpace(CaFile))
            {
                X509Certificate2Collection chain = [];
                chain.ImportFromPemFile(CaFile);
                TrustChain = chain;
            }
        }

        protected static string? GetStringValue(IDictionary<string, string> dict, string propertyName)
        {
            string? result = default;
            if (dict.TryGetValue(propertyName, out string? value))
            {
                result = value;
            }
            return result;
        }

        protected static int GetPositiveIntValueOrDefault(IDictionary<string, string> dict, string propertyName, int defaultValue = default)
        {
            int result = defaultValue;
            if (dict.TryGetValue(propertyName, out string? stringValue))
            {
                try
                {
                    result = int.Parse(stringValue, CultureInfo.InvariantCulture);
                }
                catch (FormatException ex) // re-throw ex to ArgumentException in order to include the propertyName
                {
                    throw new ArgumentException(ex.Message, propertyName, ex);
                }
            }
            return result;
        }

        protected static bool GetBooleanValue(IDictionary<string, string> dict, string propertyName, bool defaultValue = default)
        {
            bool result = defaultValue;
            if (dict.TryGetValue(propertyName, out string? stringValue))
            {
                try
                {
                    result = bool.Parse(stringValue);
                }
                catch (FormatException ex) // re-throw ex to ArgumentException in order to include the propertyName
                {
                    throw new ArgumentException(ex.Message, propertyName, ex);
                }
            }
            return result;
        }

        protected static TimeSpan GetTimeSpanValue(IDictionary<string, string> dict, string propertyName, TimeSpan defaultValue = default)
        {
            TimeSpan result = defaultValue;
            if (dict.TryGetValue(propertyName, out string? stringValue))
            {
                try
                {
                    // Convert the string directly to a TimeSpan by interpreting it as seconds
                    int seconds = int.Parse(stringValue, CultureInfo.InvariantCulture);
                    result = TimeSpan.FromSeconds(seconds);
                }
                catch (FormatException ex)
                {
                    // Re-throw as ArgumentException to include the propertyName
                    throw new ArgumentException(ex.Message, propertyName, ex);
                }
                catch (OverflowException ex)
                {
                    throw new ArgumentException($"The value for {propertyName} is out of range for TimeSpan.", propertyName, ex);
                }
            }
            return result;
        }

        private static void AppendIfNotNullOrEmpty(StringBuilder sb, string name, string? val)
        {
            if (!string.IsNullOrWhiteSpace(val))
            {
                if (name.ToLower(CultureInfo.InvariantCulture).Contains("key", StringComparison.InvariantCulture)
                    || name.ToLower(CultureInfo.InvariantCulture).Contains("password", StringComparison.InvariantCulture))
                {
                    sb.Append(CultureInfo.InvariantCulture, $"{name}=***;");
                }
                else
                {
                    sb.Append(CultureInfo.InvariantCulture, $"{name}={val};");
                }
            }
        }

        private static int CheckForValidIntegerInput(string envVarName, string envVarValue)
        {
            return int.TryParse(envVarValue, out int result)
                ? result
                : throw new ArgumentException($"{envVarName}={envVarValue}. Expecting an integer value.", envVarName);
        }

        private static bool CheckForValidBooleanInput(string envVarName, string envVarValue)
        {
            return bool.TryParse(envVarValue, out bool result)
                ? result
                : throw new ArgumentException($"{envVarName}={envVarValue}. Expecting a boolean value.", envVarName);
        }

        public override string ToString()
        {
            StringBuilder result = new();
            AppendIfNotNullOrEmpty(result, nameof(HostName), HostName);
            AppendIfNotNullOrEmpty(result, nameof(ClientId), ClientId);
            AppendIfNotNullOrEmpty(result, nameof(Username), Username);
            AppendIfNotNullOrEmpty(result, nameof(PasswordFile), PasswordFile);
            AppendIfNotNullOrEmpty(result, nameof(CertFile), CertFile);
            AppendIfNotNullOrEmpty(result, nameof(KeyFile), KeyFile);
            AppendIfNotNullOrEmpty(result, nameof(KeyPasswordFile), KeyPasswordFile);
            AppendIfNotNullOrEmpty(result, nameof(TcpPort), TcpPort.ToString(CultureInfo.InvariantCulture));
            AppendIfNotNullOrEmpty(result, nameof(CleanStart), CleanStart.ToString());
            AppendIfNotNullOrEmpty(result, nameof(SessionExpiry), ((int)SessionExpiry.TotalSeconds).ToString(CultureInfo.InvariantCulture));
            AppendIfNotNullOrEmpty(result, nameof(KeepAlive), ((int)KeepAlive.TotalSeconds).ToString(CultureInfo.InvariantCulture));
            AppendIfNotNullOrEmpty(result, nameof(CaFile), CaFile);
            AppendIfNotNullOrEmpty(result, nameof(UseTls), UseTls.ToString());
            result.Remove(result.Length - 1, 1);
            return result.ToString();
        }
    }
}
