using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Options;
using ViewEngine.Client.Configuration;
using ViewEngine.Client.Models;
namespace ViewEngine.Client;
///
/// Client for interacting with the ViewEngine REST API
///
public class ViewEngineClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly ViewEngineOptions _options;
private readonly bool _disposeHttpClient;
///
/// Creates a new ViewEngineClient with the specified API key
///
/// Your ViewEngine API key
/// Optional base URL (defaults to https://www.viewengine.io)
public ViewEngineClient(string apiKey, string? baseUrl = null)
{
if (string.IsNullOrWhiteSpace(apiKey))
throw new ArgumentNullException(nameof(apiKey));
_options = new ViewEngineOptions
{
ApiKey = apiKey,
BaseUrl = baseUrl ?? "https://www.viewengine.io"
};
_httpClient = new HttpClient
{
BaseAddress = new Uri(_options.BaseUrl),
Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds)
};
ConfigureHttpClient();
_disposeHttpClient = true;
}
///
/// Creates a new ViewEngineClient with the specified options
///
/// Configuration options
public ViewEngineClient(IOptions options) : this(options.Value)
{
}
///
/// Creates a new ViewEngineClient with the specified options
///
/// Configuration options
public ViewEngineClient(ViewEngineOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(_options.ApiKey))
throw new ArgumentException("API key is required", nameof(options));
_httpClient = new HttpClient
{
BaseAddress = new Uri(_options.BaseUrl),
Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds)
};
ConfigureHttpClient();
_disposeHttpClient = true;
}
///
/// Creates a new ViewEngineClient with a custom HttpClient (for dependency injection)
///
/// Configured HttpClient instance
/// Configuration options
public ViewEngineClient(HttpClient httpClient, IOptions options)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(_options.ApiKey))
throw new ArgumentException("API key is required", nameof(options));
_httpClient.BaseAddress = new Uri(_options.BaseUrl);
_httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds);
ConfigureHttpClient();
_disposeHttpClient = false; // Don't dispose injected HttpClient
}
private void ConfigureHttpClient()
{
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-API-Key", _options.ApiKey);
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
#region MCP Retrieval API
///
/// Submit a web page retrieval request
///
/// Retrieval request parameters
/// Cancellation token
/// Retrieval response with request ID
public async Task SubmitRetrievalAsync(SubmitRetrievalRequest request, CancellationToken cancellationToken = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (string.IsNullOrWhiteSpace(request.Url))
throw new ArgumentException("URL is required", nameof(request));
var response = await ExecuteWithRetryAsync(async () =>
{
return await _httpClient.PostAsJsonAsync("/mcp/retrieve", request, cancellationToken);
}, "SubmitRetrieval", cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
return result ?? throw new InvalidOperationException("Failed to deserialize retrieval response");
}
///
/// Get the status of a retrieval request
///
/// The request ID returned from SubmitRetrievalAsync
/// Cancellation token
/// Current status of the retrieval request
public async Task GetRetrievalStatusAsync(Guid requestId, CancellationToken cancellationToken = default)
{
if (requestId == Guid.Empty)
throw new ArgumentException("Invalid request ID", nameof(requestId));
var response = await ExecuteWithRetryAsync(async () =>
{
return await _httpClient.GetAsync($"/mcp/retrieve/{requestId}", cancellationToken);
}, "GetRetrievalStatus", cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
return result ?? throw new InvalidOperationException("Failed to deserialize status response");
}
///
/// Download the decrypted page content for a completed retrieval
///
/// The request ID of the completed retrieval
/// Cancellation token
/// Page content data
public async Task GetPageContentAsync(Guid requestId, CancellationToken cancellationToken = default)
{
if (requestId == Guid.Empty)
throw new ArgumentException("Invalid request ID", nameof(requestId));
var response = await ExecuteWithRetryAsync(async () =>
{
return await _httpClient.GetAsync($"/mcp/retrieve/{requestId}/content", cancellationToken);
}, "GetPageContent", cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken);
return result ?? throw new InvalidOperationException("Failed to deserialize page content");
}
///
/// Submit a retrieval request and wait for completion
///
/// Retrieval request parameters
/// How often to check status in milliseconds (default: 2000)
/// Cancellation token
/// Page content data when complete
public async Task RetrieveAndWaitAsync(SubmitRetrievalRequest request, int? pollingIntervalMs = null, CancellationToken cancellationToken = default)
{
// Submit the request
var submitResponse = await SubmitRetrievalAsync(request, cancellationToken);
// Poll for completion
var interval = pollingIntervalMs ?? _options.DefaultPollingIntervalMs;
while (!cancellationToken.IsCancellationRequested)
{
var status = await GetRetrievalStatusAsync(submitResponse.RequestId, cancellationToken);
if (status.Status.Equals("complete", StringComparison.OrdinalIgnoreCase))
{
// Download and return the content
return await GetPageContentAsync(submitResponse.RequestId, cancellationToken);
}
else if (status.Status.Equals("failed", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException($"Retrieval failed: {status.Error ?? status.Message}");
}
else if (status.Status.Equals("canceled", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Retrieval was canceled");
}
// Wait before polling again
await Task.Delay(interval, cancellationToken);
}
throw new OperationCanceledException("Operation was canceled", cancellationToken);
}
///
/// List available MCP tools
///
/// Cancellation token
/// List of available tools
public async Task