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<LocationsController> _logger;

    public LocationsController(LocationStore locations, IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<LocationsController> logger)
    {
        _locations = locations;
        _httpClientFactory = httpClientFactory;
        _configuration = configuration;
        _logger = logger;
    }

    [HttpPost]
    [Authorize(Roles = "SUPER")]
    public async Task<IActionResult> 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("biome-definitions")]
    [Authorize(Roles = "SUPER")]
    public async Task<IActionResult> ListBiomeDefinitions()
    {
        var definitions = await _locations.GetBiomeDefinitionsAsync();
        return Ok(definitions);
    }

    [HttpGet("biome-definitions/{biomeKey}")]
    [Authorize(Roles = "SUPER")]
    public async Task<IActionResult> GetBiomeDefinition(string biomeKey)
    {
        var normalizedBiomeKey = NormalizeBiomeKey(biomeKey);
        if (string.IsNullOrWhiteSpace(normalizedBiomeKey))
            return BadRequest("biomeKey required");

        var definition = await _locations.GetBiomeDefinitionAsync(normalizedBiomeKey);
        return definition is null ? NotFound() : Ok(definition);
    }

    [HttpPost("internal/visible-window")]
    public async Task<IActionResult> GetVisibleWindow([FromBody] InternalVisibleLocationsRequest req)
    {
        var configuredKey = (_configuration["InternalApi:Key"] ?? _configuration["Jwt:Key"] ?? string.Empty).Trim();
        var requestKey = (Request.Headers["X-Internal-Api-Key"].FirstOrDefault() ?? string.Empty).Trim();
        if (string.IsNullOrWhiteSpace(configuredKey) || !string.Equals(configuredKey, requestKey, StringComparison.Ordinal))
            return Unauthorized();
        if (req.Radius < 0)
            return BadRequest("radius must be non-negative");

        var result = await _locations.GetOrCreateVisibleLocationsAsync(req.X, req.Y, req.Radius);
        await PopulateFloorInventoriesAsync(result);
        return Ok(result);
    }

    [HttpPost("biome-definitions/{biomeKey}")]
    [Authorize(Roles = "SUPER")]
    public async Task<IActionResult> CreateBiomeDefinition(string biomeKey, [FromBody] UpsertBiomeDefinitionRequest req)
    {
        var validationError = ValidateBiomeDefinitionRequest(biomeKey, req);
        if (validationError is not null)
            return validationError;

        var definition = BuildBiomeDefinition(biomeKey, req);
        var created = await _locations.CreateBiomeDefinitionAsync(definition);
        if (!created)
            return Conflict("Biome definition already exists");

        return Ok(definition);
    }

    [HttpPut("biome-definitions/{biomeKey}")]
    [Authorize(Roles = "SUPER")]
    public async Task<IActionResult> UpsertBiomeDefinition(string biomeKey, [FromBody] UpsertBiomeDefinitionRequest req)
    {
        var validationError = ValidateBiomeDefinitionRequest(biomeKey, req);
        if (validationError is not null)
            return validationError;

        var definition = await _locations.UpsertBiomeDefinitionAsync(biomeKey, req);
        return Ok(definition);
    }

    [HttpGet]
    [Authorize(Roles = "SUPER")]
    public async Task<IActionResult> ListMine()
    {
        var locations = await _locations.GetAllAsync();
        return Ok(locations);
    }

    [HttpDelete("{id}")]
    [Authorize(Roles = "SUPER")]
    public async Task<IActionResult> Delete(string id)
    {
        var deleted = await _locations.DeleteAsync(id);
        if (!deleted)
            return NotFound();

        return Ok("Deleted");
    }

    [HttpPut("{id}")]
    [Authorize(Roles = "SUPER")]
    public async Task<IActionResult> 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<IActionResult> 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);
            }

            var characterGrantedQuantity = interact.QuantityGranted;
            var floorGrantedQuantity = 0;
            var overflowQuantity = 0;

            if (!string.IsNullOrWhiteSpace(responseBody))
            {
                using var grantJson = JsonDocument.Parse(responseBody);
                if (grantJson.RootElement.TryGetProperty("grantedQuantity", out var grantedElement) && grantedElement.ValueKind == JsonValueKind.Number)
                    characterGrantedQuantity = grantedElement.GetInt32();
                if (grantJson.RootElement.TryGetProperty("overflowQuantity", out var overflowElement) && overflowElement.ValueKind == JsonValueKind.Number)
                    overflowQuantity = overflowElement.GetInt32();
            }

            if (overflowQuantity > 0)
            {
                var floorGrantBody = JsonSerializer.Serialize(new
                {
                    itemKey = interact.ItemKey,
                    quantity = overflowQuantity
                });

                using var floorRequest = new HttpRequestMessage(
                    HttpMethod.Post,
                    $"{inventoryBaseUrl}/api/inventory/by-owner/location/{id}/grant");
                floorRequest.Content = new StringContent(floorGrantBody, Encoding.UTF8, "application/json");
                if (!string.IsNullOrWhiteSpace(token))
                    floorRequest.Headers.Authorization = AuthenticationHeaderValue.Parse(token);

                using var floorResponse = await client.SendAsync(floorRequest);
                var floorResponseBody = await floorResponse.Content.ReadAsStringAsync();
                if (!floorResponse.IsSuccessStatusCode)
                {
                    _logger.LogError(
                        "Character inventory overflow could not be redirected to location inventory. Location {LocationId}, character {CharacterId}, item {ItemKey}, quantity {Quantity}, response {StatusCode}: {Body}",
                        id,
                        req.CharacterId,
                        interact.ItemKey,
                        overflowQuantity,
                        (int)floorResponse.StatusCode,
                        floorResponseBody
                    );
                    return StatusCode((int)floorResponse.StatusCode, floorResponseBody);
                }

                if (!string.IsNullOrWhiteSpace(floorResponseBody))
                {
                    using var floorJson = JsonDocument.Parse(floorResponseBody);
                    if (floorJson.RootElement.TryGetProperty("grantedQuantity", out var floorGrantedElement) && floorGrantedElement.ValueKind == JsonValueKind.Number)
                        floorGrantedQuantity = floorGrantedElement.GetInt32();
                    else
                        floorGrantedQuantity = overflowQuantity;
                }
                else
                {
                    floorGrantedQuantity = overflowQuantity;
                }
            }

            return Ok(new InteractLocationObjectResponse
            {
                LocationId = id,
                CharacterId = req.CharacterId,
                ObjectId = interact.ObjectId,
                ObjectType = interact.ObjectType,
                ItemKey = interact.ItemKey,
                QuantityGranted = interact.QuantityGranted,
                CharacterGrantedQuantity = characterGrantedQuantity,
                FloorGrantedQuantity = floorGrantedQuantity,
                RemainingQuantity = interact.RemainingQuantity,
                Consumed = interact.Consumed,
                InventoryResponseJson = responseBody
            });
        }
    }

    private IActionResult? ValidateBiomeDefinitionRequest(string biomeKey, UpsertBiomeDefinitionRequest req)
    {
        if (string.IsNullOrWhiteSpace(NormalizeBiomeKey(biomeKey)))
            return BadRequest("biomeKey required");
        if (req.ContinuationWeight < 0)
            return BadRequest("continuationWeight must be non-negative");
        if (req.TransitionWeights.Any(weight => string.IsNullOrWhiteSpace(weight.TargetBiomeKey)))
            return BadRequest("transitionWeights require targetBiomeKey");
        if (req.TransitionWeights.Any(weight => weight.Weight < 0))
            return BadRequest("transitionWeights weight must be non-negative");
        if (req.ObjectSpawnRules.Count == 0)
            return BadRequest("objectSpawnRules required");
        if (req.ObjectSpawnRules.Any(rule => rule.Weight < 0))
            return BadRequest("objectSpawnRules weight must be non-negative");
        if (req.ObjectSpawnRules.Any(rule => string.IsNullOrWhiteSpace(rule.ResultType)))
            return BadRequest("objectSpawnRules resultType required");
        if (req.ObjectSpawnRules.Any(rule =>
                string.Equals(rule.ResultType, "gatherable", StringComparison.OrdinalIgnoreCase) &&
                string.IsNullOrWhiteSpace(rule.ItemKey)))
            return BadRequest("gatherable objectSpawnRules require itemKey");
        return null;
    }

    private static BiomeDefinition BuildBiomeDefinition(string biomeKey, UpsertBiomeDefinitionRequest req)
    {
        var normalizedBiomeKey = NormalizeBiomeKey(biomeKey);
        return new BiomeDefinition
        {
            BiomeKey = normalizedBiomeKey,
            ContinuationWeight = req.ContinuationWeight,
            TransitionWeights = req.TransitionWeights.Select(weight => new BiomeTransitionWeight
            {
                TargetBiomeKey = NormalizeBiomeKey(weight.TargetBiomeKey),
                Weight = weight.Weight
            }).ToList(),
            ObjectSpawnRules = req.ObjectSpawnRules.Select(rule => new BiomeObjectSpawnRule
            {
                ResultType = rule.ResultType.Trim().ToLowerInvariant(),
                ItemKey = string.IsNullOrWhiteSpace(rule.ItemKey) ? null : rule.ItemKey.Trim().ToLowerInvariant(),
                ObjectKey = string.IsNullOrWhiteSpace(rule.ObjectKey) ? null : rule.ObjectKey.Trim(),
                DisplayName = string.IsNullOrWhiteSpace(rule.DisplayName) ? null : rule.DisplayName.Trim(),
                RemainingQuantity = rule.RemainingQuantity,
                GatherQuantity = rule.GatherQuantity,
                Weight = rule.Weight
            }).ToList(),
            UpdatedUtc = DateTime.UtcNow
        };
    }

    private static string NormalizeBiomeKey(string biomeKey) => biomeKey.Trim().ToLowerInvariant();

    private async Task PopulateFloorInventoriesAsync(VisibleLocationWindowResponse result)
    {
        var locationIds = result.Locations
            .Where(location => !string.IsNullOrWhiteSpace(location.Id))
            .Select(location => location.Id!)
            .Distinct(StringComparer.Ordinal)
            .ToList();
        if (locationIds.Count == 0)
            return;

        var inventoryBaseUrl = (_configuration["Services:InventoryApiBaseUrl"] ?? "http://localhost:5003").TrimEnd('/');
        var internalApiKey = (_configuration["InternalApi:Key"] ?? _configuration["Jwt:Key"] ?? string.Empty).Trim();
        var body = JsonSerializer.Serialize(new { ownerIds = locationIds });

        var client = _httpClientFactory.CreateClient();
        using var request = new HttpRequestMessage(HttpMethod.Post, $"{inventoryBaseUrl}/api/inventory/internal/by-owner/location");
        request.Content = new StringContent(body, Encoding.UTF8, "application/json");
        request.Headers.Add("X-Internal-Api-Key", internalApiKey);

        using var response = await client.SendAsync(request);
        var responseBody = await response.Content.ReadAsStringAsync();
        if (!response.IsSuccessStatusCode)
        {
            _logger.LogWarning(
                "Failed to load batched floor inventories from InventoryApi. Status={StatusCode} Body={Body}",
                (int)response.StatusCode,
                responseBody);
            return;
        }

        var parsed = JsonSerializer.Deserialize<List<OwnerInventorySummaryEnvelope>>(
            responseBody,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? [];
        var byOwnerId = parsed.ToDictionary(entry => entry.OwnerId, entry => entry.Items, StringComparer.Ordinal);

        foreach (var location in result.Locations)
        {
            if (string.IsNullOrWhiteSpace(location.Id))
                continue;
            location.FloorItems = byOwnerId.GetValueOrDefault(location.Id!, []);
        }
    }

    private sealed class OwnerInventorySummaryEnvelope
    {
        public string OwnerId { get; set; } = string.Empty;

        public List<FloorInventoryItemResponse> Items { get; set; } = [];
    }
}
