using System.Security.Cryptography;
using System.Text;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Types.InboundApi;

namespace ScadaLink.InboundAPI;

/// <summary>
/// WP-1: Validates API keys from X-API-Key header.
/// Checks that the key exists, is enabled, and is approved for the requested method.
/// </summary>
public class ApiKeyValidator
{
    private readonly IInboundApiRepository _repository;
    private readonly IApiKeyHasher _hasher;

    // InboundAPI-011: the single message used for both "method not found" and
    // "key not approved" so the two outcomes are indistinguishable to the caller.
    private const string NotApprovedMessage = "API key not approved for this method";

    /// <param name="repository">Inbound-API data access.</param>
    /// <param name="hasher">
    /// ConfigurationDatabase-012: hashes the presented candidate key with the same
    /// HMAC-SHA256 pepper used at key creation, so authentication compares hashes —
    /// the database never holds a plaintext credential. Defaults to the unpeppered
    /// <see cref="ApiKeyHasher.Default"/>; production wiring injects the configured,
    /// peppered hasher.
    /// </param>
    public ApiKeyValidator(IInboundApiRepository repository, IApiKeyHasher? hasher = null)
    {
        _repository = repository;
        _hasher = hasher ?? ApiKeyHasher.Default;
    }

    /// <summary>
    /// Validates an API key for a given method.
    /// Returns (isValid, apiKey, statusCode, errorMessage).
    /// </summary>
    public async Task<ApiKeyValidationResult> ValidateAsync(
        string? apiKeyValue,
        string methodName,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrEmpty(apiKeyValue))
        {
            return ApiKeyValidationResult.Unauthorized("Missing X-API-Key header");
        }

        // InboundAPI-003: do NOT resolve the key with a secret-equality lookup
        // (GetApiKeyByValueAsync translates to a SQL "WHERE KeyHash = @hash" early-exit
        // comparison — a timing side-channel). Fetch all keys and match in-process
        // with a constant-time comparison so neither match position nor length is
        // observable to a network attacker.
        // ConfigurationDatabase-012: the database stores only the HMAC hash of each
        // key, so the presented candidate is hashed with the same pepper and the
        // comparison runs over the resulting hashes — never over plaintext.
        var candidateHash = _hasher.Hash(apiKeyValue);
        var apiKey = FindKeyConstantTime(
            await _repository.GetAllApiKeysAsync(cancellationToken),
            candidateHash);
        if (apiKey == null || !apiKey.IsEnabled)
        {
            return ApiKeyValidationResult.Unauthorized("Invalid or disabled API key");
        }

        // InboundAPI-011: "method not found" and "key not approved" must produce an
        // indistinguishable response. Otherwise a caller holding any valid key could
        // enumerate which method names exist by observing the status/message
        // difference. Both cases return 403 with the identical message below, and the
        // caller-supplied method name is never echoed back into the response.
        var method = await _repository.GetMethodByNameAsync(methodName, cancellationToken);
        if (method == null)
        {
            return ApiKeyValidationResult.Forbidden(NotApprovedMessage);
        }

        // Check if this key is approved for the method
        var approvedKeys = await _repository.GetApprovedKeysForMethodAsync(method.Id, cancellationToken);
        var isApproved = approvedKeys.Any(k => k.Id == apiKey.Id);

        if (!isApproved)
        {
            return ApiKeyValidationResult.Forbidden(NotApprovedMessage);
        }

        return ApiKeyValidationResult.Valid(apiKey, method);
    }

    /// <summary>
    /// InboundAPI-003 / ConfigurationDatabase-012: Finds the key whose stored
    /// <see cref="ApiKey.KeyHash"/> matches <paramref name="candidateHash"/> — the
    /// HMAC hash of the presented key — using
    /// <see cref="CryptographicOperations.FixedTimeEquals"/> over the UTF-8 bytes.
    /// Every candidate row is compared so that the running time does not depend on the
    /// match position; length mismatches return false without leaking length timing.
    /// </summary>
    private static ApiKey? FindKeyConstantTime(IEnumerable<ApiKey> keys, string candidateHash)
    {
        var candidateBytes = Encoding.UTF8.GetBytes(candidateHash);
        ApiKey? match = null;

        foreach (var key in keys)
        {
            var keyBytes = Encoding.UTF8.GetBytes(key.KeyHash);
            if (CryptographicOperations.FixedTimeEquals(candidateBytes, keyBytes))
            {
                // Do not break — continuing keeps the loop's timing independent of
                // where (or whether) a match is found.
                match = key;
            }
        }

        return match;
    }
}

/// <summary>
/// Result of API key validation.
/// </summary>
public class ApiKeyValidationResult
{
    public bool IsValid { get; private init; }
    public int StatusCode { get; private init; }
    public string? ErrorMessage { get; private init; }
    public ApiKey? ApiKey { get; private init; }
    public ApiMethod? Method { get; private init; }

    public static ApiKeyValidationResult Valid(ApiKey apiKey, ApiMethod method) =>
        new() { IsValid = true, StatusCode = 200, ApiKey = apiKey, Method = method };

    public static ApiKeyValidationResult Unauthorized(string message) =>
        new() { IsValid = false, StatusCode = 401, ErrorMessage = message };

    public static ApiKeyValidationResult Forbidden(string message) =>
        new() { IsValid = false, StatusCode = 403, ErrorMessage = message };
}
