using System; using System.Net; using System.Threading.Tasks; using AutopilotMonitor.Functions.Helpers; using AutopilotMonitor.Functions.Services; using AutopilotMonitor.Functions.Services.Backup; using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models.Backup; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.Logging; namespace AutopilotMonitor.Functions.Functions.Backup { /// /// POST /api/global/backups/{backupId}/restore-row — single-row preview /// + commit endpoint for the critical-table backup restore (plan §PR2). /// GA-only via EndpointAccessPolicyCatalog. /// /// Synchronous on commit (1 row → Function timeout unproblematic), unlike the /// full-table restore which goes via the 202+queue pattern. PartitionKey and /// RowKey live in the body, not the URL: Azure Tables permits /, /// +, and % in PK/RK which the Functions router would mangle. /// /// public class RestoreRowFunction { private readonly RestoreTablePreflightValidator _preflight; private readonly CriticalTableRestoreService _restoreService; private readonly OpsEventService _opsEvents; private readonly ILogger _logger; public RestoreRowFunction( RestoreTablePreflightValidator preflight, CriticalTableRestoreService restoreService, OpsEventService opsEvents, ILogger logger) { _preflight = preflight; _restoreService = restoreService; _opsEvents = opsEvents; _logger = logger; } [Function("RestoreRow")] public async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "global/backups/{backupId}/restore-row")] HttpRequestData req, string backupId) { var ct = req.FunctionContext.CancellationToken; // 1. Parse body var read = await req.ReadAsync(); if (read.Error != null) return read.Error; var body = read.Value!; // 2. Lightweight preflight (no I/O) try { _preflight.ValidateRowRequest(backupId, body); } catch (BackupTerminalException ex) { return await req.BadRequestAsync(ex.Message, ex.Code).ConfigureAwait(false); } // 3. Dispatch on mode try { if (body.Mode == RestoreRowMode.Preview) { var preview = await _restoreService.PreviewRowAsync( backupId, body.TableName, body.PartitionKey, body.RowKey, ct).ConfigureAwait(false); return await req.OkAsync(preview).ConfigureAwait(false); } else { var actor = TenantHelper.GetUserIdentifier(req) ?? "GlobalAdmin"; var commit = await _restoreService.CommitRowAsync( backupId, body.TableName, body.PartitionKey, body.RowKey, body.IfSha256!, body.IfCurrentETag, ct).ConfigureAwait(false); // Fire-and-forget audit; OpsEventService catches its own writes. try { await _opsEvents.RecordBackupRowRestoredAsync( backupId, body.TableName, body.PartitionKey, body.RowKey, actor, commit.Outcome.ToString()).ConfigureAwait(false); } catch (Exception evtEx) { _logger.LogWarning(evtEx, "RestoreRow: ops event recording failed — write itself succeeded"); } return await req.OkAsync(commit).ConfigureAwait(false); } } catch (BackupTerminalException ex) { var status = MapErrorCodeToStatus(ex.Code); return await req.ErrorAsync(status, ex.Code, ex.Message).ConfigureAwait(false); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { // Client went away. throw; } catch (Exception ex) { // Sanitized envelope: the raw exception text stays in the log, the correlation id is the handle. return await req.InternalServerErrorAsync( _logger, ex, $"RestoreRow backupId={backupId} table={body.TableName}").ConfigureAwait(false); } } private static HttpStatusCode MapErrorCodeToStatus(string code) => code switch { Constants.BackupErrorCodes.BackupNotFound => HttpStatusCode.NotFound, Constants.BackupErrorCodes.RowNotInBackup => HttpStatusCode.NotFound, Constants.BackupErrorCodes.TableNotInBackup => HttpStatusCode.Conflict, Constants.BackupErrorCodes.ManifestCorrupt => HttpStatusCode.Conflict, Constants.BackupErrorCodes.ManifestSchemaUnsupported => HttpStatusCode.Conflict, Constants.BackupErrorCodes.IntegrityCheckFailed => HttpStatusCode.Conflict, Constants.BackupErrorCodes.BlobChangedSinceValidation => HttpStatusCode.Conflict, Constants.BackupErrorCodes.RowChangedSinceValidation => HttpStatusCode.Conflict, Constants.BackupErrorCodes.CurrentRowChanged => HttpStatusCode.Conflict, Constants.BackupErrorCodes.MaintenanceInProgress => HttpStatusCode.Conflict, Constants.BackupErrorCodes.MaintenanceLeaseLost => HttpStatusCode.Conflict, _ => HttpStatusCode.BadRequest, }; } }