using System.Security.Cryptography;
using System.Text;

namespace ScadaLink.Commons.Types.InboundApi;

/// <summary>
/// Computes a deterministic, keyed hash of an inbound-API key value
/// (ConfigurationDatabase-012). API keys are persisted as this hash, never as
/// plaintext, so a configuration-database dump does not yield usable credentials.
/// The hash is deterministic so authentication can still resolve a key by value.
/// </summary>
public interface IApiKeyHasher
{
    /// <summary>
    /// Returns the keyed hash of <paramref name="apiKey"/> as a Base64 string.
    /// The same input always produces the same output (deterministic), which keeps
    /// the by-value lookup working.
    /// </summary>
    string Hash(string apiKey);
}

/// <summary>
/// HMAC-SHA256 implementation of <see cref="IApiKeyHasher"/>. The HMAC key is a
/// server-side <em>pepper</em> bound from configuration. A per-row random salt is
/// intentionally NOT used: an API key is already a high-entropy random token, and a
/// random salt would break the deterministic by-value lookup the authentication
/// path relies on. The pepper instead binds every hash to this deployment, so a
/// stolen database is useless without it.
/// </summary>
public sealed class ApiKeyHasher : IApiKeyHasher
{
    /// <summary>
    /// Minimum accepted pepper length. A pepper shorter than this is treated as a
    /// deployment misconfiguration and rejected — see <see cref="ApiKeyHasher(string)"/>.
    /// </summary>
    public const int MinimumPepperLength = 16;

    private readonly byte[] _pepper;

    /// <summary>
    /// An unpeppered hasher (HMAC-SHA256 keyed with a fixed, empty-equivalent value).
    /// It is still a one-way hash, but carries no deployment-specific binding. It
    /// exists for tests and non-production wiring; production must construct an
    /// <see cref="ApiKeyHasher"/> with a real pepper.
    /// </summary>
    public static ApiKeyHasher Default { get; } = new ApiKeyHasher();

    private ApiKeyHasher()
    {
        // Fixed, deployment-independent key for the unpeppered default.
        _pepper = Encoding.UTF8.GetBytes("ScadaLink.InboundApi.DefaultApiKeyHasher");
    }

    /// <summary>
    /// Creates a hasher keyed with the given server-side pepper.
    /// </summary>
    /// <exception cref="ArgumentException">
    /// Thrown if <paramref name="pepper"/> is null, blank, or shorter than
    /// <see cref="MinimumPepperLength"/> — a missing or weak pepper is a deployment
    /// misconfiguration and must fail loudly rather than degrade silently.
    /// </exception>
    public ApiKeyHasher(string pepper)
    {
        if (string.IsNullOrWhiteSpace(pepper))
        {
            throw new ArgumentException(
                "The API-key HMAC pepper must be configured. Set a strong, random value " +
                "in configuration (ScadaLink:InboundApi:ApiKeyPepper).",
                nameof(pepper));
        }

        if (pepper.Trim().Length < MinimumPepperLength)
        {
            throw new ArgumentException(
                $"The API-key HMAC pepper is too weak: it must be at least {MinimumPepperLength} " +
                "characters. Use a strong, random value.",
                nameof(pepper));
        }

        _pepper = Encoding.UTF8.GetBytes(pepper);
    }

    /// <inheritdoc />
    public string Hash(string apiKey)
    {
        ArgumentNullException.ThrowIfNull(apiKey);

        using var hmac = new HMACSHA256(_pepper);
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(apiKey));
        return Convert.ToBase64String(hash);
    }
}
