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

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;

    public ApiKeyValidator(IInboundApiRepository repository)
    {
        _repository = repository;
    }

    /// <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 KeyValue = @secret" early-exit
        // comparison — a timing side-channel). Fetch all keys and match the secret
        // in-process with a constant-time comparison so neither match position nor
        // secret length is observable to a network attacker.
        var apiKey = FindKeyConstantTime(
            await _repository.GetAllApiKeysAsync(cancellationToken),
            apiKeyValue);
        if (apiKey == null || !apiKey.IsEnabled)
        {
            return ApiKeyValidationResult.Unauthorized("Invalid or disabled API key");
        }

        var method = await _repository.GetMethodByNameAsync(methodName, cancellationToken);
        if (method == null)
        {
            return ApiKeyValidationResult.NotFound($"Method '{methodName}' not found");
        }

        // 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("API key not approved for this method");
        }

        return ApiKeyValidationResult.Valid(apiKey, method);
    }

    /// <summary>
    /// InboundAPI-003: Finds the key whose value matches <paramref name="candidate"/>
    /// 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 candidate)
    {
        var candidateBytes = Encoding.UTF8.GetBytes(candidate);
        ApiKey? match = null;

        foreach (var key in keys)
        {
            var keyBytes = Encoding.UTF8.GetBytes(key.KeyValue);
            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 };

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