using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace JobTrackerApi.Controllers; [ApiController] [Route("api/jobapplications/{jobId:int}/focus-plan")] [Authorize(AuthenticationSchemes = "local", Policy = ProEntitlement.Policy)] public sealed class StrategySnapshotController( JobTrackerContext db, AiOperationAdmission admission, StrategySnapshotService snapshots) : ControllerBase { [HttpGet] public async Task> Get(int jobId, [FromQuery] string? attachmentIds, CancellationToken cancellationToken) { IReadOnlyList ids; try { ids = StrategySnapshotService.ParseAttachmentIds(attachmentIds); } catch (StrategySnapshotValidationException exception) { return Problem(exception); } var result = await snapshots.GetCachedAsync(jobId, StrategySnapshotService.NormalizeAttachmentIds(ids), cancellationToken); return result is null ? NotFound(new { code = "strategy_not_generated", message = "No strategy snapshot has been generated for this context." }) : Ok(result); } [HttpPost("operations")] public async Task Enqueue(int jobId, [FromBody] StrategySnapshotRequest? request, CancellationToken cancellationToken) { try { var ids = StrategySnapshotService.ParseAttachmentIds(request?.AttachmentIds); await snapshots.ValidateRequestAsync(jobId, ids, cancellationToken); var signature = StrategySnapshotService.NormalizeAttachmentIds(ids); var subject = StrategySnapshotService.EncodeSubject(jobId, ids); var activeStatuses = new[] { OperationStatuses.Queued, OperationStatuses.Running, OperationStatuses.WaitingForRetry, OperationStatuses.WaitingForExternalFallback }; var active = await db.UserOperations.AsNoTracking() .Where(item => item.TaskType == StrategySnapshotService.TaskType && item.SubjectId == subject && activeStatuses.Contains(item.Status)) .OrderByDescending(item => item.CreatedAtUtc) .FirstOrDefaultAsync(cancellationToken); if (active is not null) { var statusUrl = $"/api/operations/{active.Id:D}"; return Accepted(statusUrl, new StrategySnapshotOperationResponse(OperationDto.From(active), statusUrl, false)); } var key = await snapshots.BuildIdempotencyKeyAsync(jobId, signature, cancellationToken); var result = await admission.EnqueueAsync( StrategySnapshotService.TaskType, key, "job_strategy", subject, AiOperationPriorities.Interactive, cancellationToken); return Accepted(result.StatusUrl, new StrategySnapshotOperationResponse(OperationDto.From(result.Operation), result.StatusUrl, result.Created)); } catch (StrategySnapshotValidationException exception) { return Problem(exception); } catch (AiOperationAdmissionException exception) { if (exception.RetryAfterSeconds is int seconds) Response.Headers.RetryAfter = seconds.ToString(); return StatusCode(exception.StatusCode, new { code = exception.Code, message = exception.Message }); } } [HttpGet("operation")] public async Task> LatestOperation(int jobId, [FromQuery] string? attachmentIds, CancellationToken cancellationToken) { IReadOnlyList ids; try { ids = StrategySnapshotService.ParseAttachmentIds(attachmentIds); } catch (StrategySnapshotValidationException exception) { return Problem(exception); } var subject = StrategySnapshotService.EncodeSubject(jobId, ids); var operation = await db.UserOperations.AsNoTracking() .Where(item => item.TaskType == StrategySnapshotService.TaskType && item.SubjectId == subject) .OrderByDescending(item => item.CreatedAtUtc) .FirstOrDefaultAsync(cancellationToken); return operation is null ? NotFound() : Ok(OperationDto.From(operation)); } private ObjectResult Problem(StrategySnapshotValidationException exception) => StatusCode(exception.StatusCode, new { code = exception.Code, message = exception.Message }); } public sealed record StrategySnapshotRequest(string? AttachmentIds); public sealed record StrategySnapshotOperationResponse(OperationDto Operation, string StatusUrl, bool Created);