using Microsoft.Data.SqlClient;
using System.Data;
using System.Security;
using System.Xml.Linq;
using System.Xml;

namespace e_suite.Database.Migrator.SqlWrapper;

public class SqlWrapper : ISqlWrapper
{
    /// <summary>
    /// contains the details of parameter including the value.  Settings the value to null or DBNull.Value will result in a DBNull.Value being passed with the stored procedure call.
    /// </summary>
    private struct SqlParam
    {
        public string Name;
        public SqlDbType Type;
        public ParameterDirection Direction;
        public object Value;
    }

    private const string ReturnValueParamName = "_ReturnValue";
    private readonly IDbConnection _sqlConn;
    private readonly List<SqlParam> _sqlParams;
    private string _commandName = string.Empty;
    private string _commandText = string.Empty;

    public string CommandName
    {
        get => _commandName;
        set
        {
            if (_commandName != value)
            {
                _commandName = value;
                if (!string.IsNullOrEmpty(_commandText))
                {
                    _commandText = string.Empty;
                }
            }
        }
    }

    public string CommandText
    {
        get => _commandText;
        set
        {
            if (_commandText != value)
            {
                _commandText = value;
                if (!string.IsNullOrEmpty(_commandName))
                {
                    _commandName = string.Empty;
                }
            }
        }
    }

    /// <summary>
    /// Creates an instance of the SqlCommand.  Wraps setting everything needed to make a stored procedure call based on the parameters supplied.
    /// </summary>
    /// <returns>Prepared SqlCommand object ready for execution.</returns>
    /// FxCop warning has been deliberately suppressed as the CommandText is only allowed to be a stored procedure name.
    /// This is ensured by the hard coding of the CommandType.
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
    private SqlCommand PrepareSqlCommand()
    {
        SqlCommand sqlCommand;
        SqlCommand? tempSqlCommand = null;

        try
        {
            tempSqlCommand = new SqlCommand
            {
                CommandTimeout = Timeout,
                Connection = _sqlConn as SqlConnection
            };

            if (!string.IsNullOrEmpty(CommandName))
            {
                tempSqlCommand.CommandType = CommandType.StoredProcedure;
                tempSqlCommand.CommandText = CommandName;
            }
            else if (!string.IsNullOrEmpty(CommandText))
            {
                tempSqlCommand.CommandType = CommandType.Text;
                tempSqlCommand.CommandText = CommandText;
            }

            foreach (var sqlParam in _sqlParams)
            {
                if (sqlParam.Value is SecureString sqlParamValue)
                {
                    tempSqlCommand.Parameters.AddSecure(sqlParam.Name, sqlParamValue);
                }
                else
                {
                    var sqlParameter = tempSqlCommand.Parameters.Add(sqlParam.Name, sqlParam.Type);

                    if (sqlParam.Type == SqlDbType.VarChar)
                    {
                        sqlParameter.Size = -1;
                    }
                    sqlParameter.Direction = sqlParam.Direction;
                    sqlParameter.Value = sqlParam.Value;
                }
            }

            sqlCommand = tempSqlCommand;
        }
        catch
        {
            tempSqlCommand?.Dispose();
            throw;
        }

        return sqlCommand;
    }

    /// <summary>
    /// retrieves the output parameters putting each into their respective SqlParam contained in the sqlParams list.
    /// </summary>
    /// <param name="sqlCommand">the SqlCommand object after execution so that the output parameters may be retrieved.</param>
    private void RetrieveOutputParams(SqlCommand sqlCommand)
    {
        ParameterDirection[] outParamDirections = { ParameterDirection.InputOutput, ParameterDirection.Output, ParameterDirection.ReturnValue };

        foreach (SqlParameter sqlParameter in sqlCommand.Parameters)
        {
            if (outParamDirections.Contains(sqlParameter.Direction))
            {
                for (int i = 0; i < _sqlParams.Count; i++)
                {
                    SqlParam sqlParam = _sqlParams[i];

                    if (sqlParam.Name == sqlParameter.ParameterName)
                    {
                        sqlParam.Value = sqlParameter.Value;
                        _sqlParams[i] = sqlParam; //will this be needed?
                        break;
                    }
                }
            }
        }
    }

    /// <summary>
    /// class constructor.  this will create and immediately open the database connection.
    /// </summary>
    public SqlWrapper(string connectionString) //constructor
    {
        _sqlConn = new SqlConnection(connectionString);
        _sqlConn.Open();
        _sqlParams = new List<SqlParam>();
    }

    public SqlWrapper(IDbConnection sqlConnection) //constructor
    {
        _sqlConn = sqlConnection;
        _sqlConn.Open();
        _sqlParams = new List<SqlParam>();
    }

    /// <summary>
    /// Overrideable Dispose method. This will be called when the object is destroyed, either by ending a uses clause or by the garbage collector.
    /// </summary>
    /// <param name="disposing">states that this was called by the garbage collector</param>
    protected virtual void Dispose(bool disposing)
    {
        if (!IsDisposed)
        {
            if (disposing)
            {
                _sqlConn.Close();
            }
        }
        IsDisposed = true;
    }

    /// <summary>
    /// object destructor, will call the overridable Dispose method.
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    public bool IsDisposed { get; private set; }

    /// <summary>
    /// Removes all parameters from the internal parameter list.
    /// </summary>
    public void Clear()
    {
        _sqlParams.Clear();
    }

    public int ParamCount => _sqlParams.Count;

    public int Timeout { get; set; } = 30;

    public void AddParameter(string name, SqlDbType type, object value)
    {
        AddParameter(name, type, ParameterDirection.Input, value);
    }

    /// <summary>
    /// Adds a new parameter to the stored procedure
    /// </summary>
    /// <param name="name">name of the parameter. For example @test   the @ symbol is optional</param>
    /// <param name="type">database type of the parameter</param>
    /// <param name="direction">Specifies if this is an input or output parameter</param>
    /// <param name="value">actual value for the parameter</param>
    public void AddParameter(string name, SqlDbType type, ParameterDirection direction, object? value)
    {
        if (value == null)
        {
            value = DBNull.Value;
        }

        SqlParam sqlParam = new SqlParam
        {
            Name = name,
            Type = type,
            Direction = direction,
            Value = value
        };

        _sqlParams.Add(sqlParam);
    }

    /// <summary>
    /// Retrieves the current value of a parameter
    /// </summary>
    /// <param name="name">name of the parameter that you need the value for</param>
    /// <returns>either the value of the named parameter, or null if it does not exist</returns>
    public object? GetParamValue(string name)
    {
        foreach (var sqlParam in _sqlParams)
        {
            if (sqlParam.Name == name)
            {
                return sqlParam.Value;
            }
        }
        return null;
    }

    /// <summary>
    /// Retrieves the procedures return value
    /// </summary>
    /// <returns>the return value from the procedure call</returns>
    public object? GetReturnValue()
    {
        return GetParamValue(ReturnValueParamName);
    }

    /// <summary>
    /// Execute the stored procedure without the expectation of any result sets being returned.
    /// note: output parameters will be set when the method completes.
    /// </summary>
    public void Execute()
    {
        AddParameter(ReturnValueParamName, SqlDbType.Int, ParameterDirection.ReturnValue, null);
        using (SqlCommand sqlCommand = PrepareSqlCommand())
        {
            sqlCommand.ExecuteNonQuery();
            RetrieveOutputParams(sqlCommand);
        }
    }


    public XmlReader ExecuteXmlReader()
    {
        using (SqlCommand sqlCommand = PrepareSqlCommand())
        {
            return sqlCommand.ExecuteXmlReader();
        }
    }

    /// <summary>
    /// Execute the stored procedure with the expection of an XML Stream result set.
    /// </summary>
    /// <returns>XML document containing the result of the stored procedure call</returns>
    public XmlDocument ExecuteXmlDocument()
    {
        using (XmlReader xReader = ExecuteXmlReader())
        {
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.Load(xReader);
            return xmlDocument;
        }
    }

    public XDocument ExecuteXDocument()
    {
        using (XmlReader xReader = ExecuteXmlReader())
        {
            XDocument xmlDocument = XDocument.Load(xReader);
            return xmlDocument;
        }
    }

    public IDataReader ExecuteReader()
    {
        using (SqlCommand sqlCommand = PrepareSqlCommand())
        {
            return sqlCommand.ExecuteReader();
        }
    }
}