using LocationsApi.Models; using LocationsApi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MongoDB.Driver; using System.Net.Http.Headers; using System.Security.Claims; using System.Text; using System.Text.Json; namespace LocationsApi.Controllers; [ApiController] [Route("api/[controller]")] public class LocationsController : ControllerBase { private readonly LocationStore _locations; private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly ILogger _logger; public LocationsController(LocationStore locations, IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger logger) { _locations = locations; _httpClientFactory = httpClientFactory; _configuration = configuration; _logger = logger; } [HttpPost] [Authorize(Roles = "SUPER")] public async Task Create([FromBody] CreateLocationRequest req) { if (string.IsNullOrWhiteSpace(req.Name)) return BadRequest("Name required"); if (req.Coord is null) return BadRequest("Coord required"); var location = new Location { Name = req.Name.Trim(), Coord = req.Coord, CreatedUtc = DateTime.UtcNow }; try { await _locations.CreateAsync(location); } catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { return Conflict("Coord must be unique"); } catch (MongoWriteException ex) when (ex.WriteError.Code == 121) { return BadRequest("Location document failed validation"); } return Ok(location); } [HttpGet] [Authorize(Roles = "SUPER")] public async Task ListMine() { var locations = await _locations.GetAllAsync(); return Ok(locations); } [HttpDelete("{id}")] [Authorize(Roles = "SUPER")] public async Task Delete(string id) { var deleted = await _locations.DeleteAsync(id); if (!deleted) return NotFound(); return Ok("Deleted"); } [HttpPut("{id}")] [Authorize(Roles = "SUPER")] public async Task Update(string id, [FromBody] UpdateLocationRequest req) { if (string.IsNullOrWhiteSpace(req.Name)) return BadRequest("Name required"); if (req.Coord is not null) return BadRequest("Coord cannot be updated"); var updated = await _locations.UpdateNameAsync(id, req.Name.Trim()); if (!updated) return NotFound(); return Ok("Updated"); } [HttpPost("{id}/interact")] [Authorize(Roles = "USER,SUPER")] public async Task Interact(string id, [FromBody] InteractLocationObjectRequest req) { if (string.IsNullOrWhiteSpace(req.CharacterId)) return BadRequest("characterId required"); if (string.IsNullOrWhiteSpace(req.ObjectId)) return BadRequest("objectId required"); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrWhiteSpace(userId)) return Unauthorized(); var allowAnyOwner = User.IsInRole("SUPER"); var interact = await _locations.InteractWithObjectAsync(id, req.CharacterId, req.ObjectId, userId, allowAnyOwner); if (interact.Status == InteractStatus.LocationNotFound) return NotFound("Location not found"); if (interact.Status == InteractStatus.CharacterNotFound) return NotFound("Character not found"); if (interact.Status == InteractStatus.Forbidden) return Forbid(); if (interact.Status == InteractStatus.Invalid) return BadRequest("Character is not at the target location"); if (interact.Status == InteractStatus.ObjectNotFound) return NotFound("Location object not found"); if (interact.Status == InteractStatus.UnsupportedObjectType) return BadRequest("Location object type is not supported"); if (interact.Status == InteractStatus.ObjectConsumed) return Conflict("Location object is consumed"); var inventoryBaseUrl = (_configuration["Services:InventoryApiBaseUrl"] ?? "http://localhost:5003").TrimEnd('/'); var token = Request.Headers.Authorization.ToString(); var grantBody = JsonSerializer.Serialize(new { itemKey = interact.ItemKey, quantity = interact.QuantityGranted }); var client = _httpClientFactory.CreateClient(); using var request = new HttpRequestMessage( HttpMethod.Post, $"{inventoryBaseUrl}/api/inventory/by-owner/character/{req.CharacterId}/grant"); request.Content = new StringContent(grantBody, Encoding.UTF8, "application/json"); if (!string.IsNullOrWhiteSpace(token)) request.Headers.Authorization = AuthenticationHeaderValue.Parse(token); HttpResponseMessage response; try { response = await client.SendAsync(request); } catch (HttpRequestException ex) { if (interact.PreviousObject is not null) await _locations.RestoreObjectInteractionAsync(id, interact.PreviousObject); _logger.LogError( ex, "Failed to reach InventoryApi while processing location interaction for location {LocationId}, character {CharacterId}, object {ObjectId}", id, req.CharacterId, req.ObjectId ); return StatusCode(StatusCodes.Status502BadGateway, new { type = "https://httpstatuses.com/502", title = "Bad Gateway", status = 502, detail = $"Failed to reach InventoryApi at {inventoryBaseUrl}.", traceId = HttpContext.TraceIdentifier }); } using (response) { var responseBody = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { if (interact.PreviousObject is not null) await _locations.RestoreObjectInteractionAsync(id, interact.PreviousObject); return StatusCode((int)response.StatusCode, responseBody); } return Ok(new InteractLocationObjectResponse { LocationId = id, CharacterId = req.CharacterId, ObjectId = interact.ObjectId, ObjectType = interact.ObjectType, ItemKey = interact.ItemKey, QuantityGranted = interact.QuantityGranted, RemainingQuantity = interact.RemainingQuantity, Consumed = interact.Consumed, InventoryResponseJson = responseBody }); } } }