using Extrudex.API.DTOs;
using Extrudex.API.DTOs.Spools;
using Extrudex.Domain.Entities;
using Extrudex.Infrastructure.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Extrudex.API.Controllers;

/// <summary>
/// Controller for managing filament spools — the core inventory unit of Extrudex.
/// Supports full CRUD with pagination, filtering by material and active status,
/// and soft-delete semantics (DELETE sets IsActive = false).
/// </summary>
[ApiController]
[Route("api/spools")]
public class SpoolsController : ControllerBase
{
    private readonly ExtrudexDbContext _dbContext;
    private readonly ILogger<SpoolsController> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="SpoolsController"/> class.
    /// </summary>
    /// <param name="dbContext">The database context for data access.</param>
    /// <param name="logger">The logger for diagnostic output.</param>
    public SpoolsController(ExtrudexDbContext dbContext, ILogger<SpoolsController> logger)
    {
        _dbContext = dbContext;
        _logger = logger;
    }

    /// <summary>
    /// Gets a paginated list of spools with optional filtering by material and active status.
    /// Results are ordered by creation date (newest first) and include denormalized material names.
    /// </summary>
    /// <param name="query">Optional query parameters for pagination and filtering.</param>
    /// <returns>A paginated list of spools matching the filter criteria.</returns>
    /// <response code="200">Returns the paginated list of spools.</response>
    [HttpGet]
    [ProducesResponseType(typeof(PagedResponse<SpoolResponse>), StatusCodes.Status200OK)]
    public async Task<ActionResult<PagedResponse<SpoolResponse>>> GetSpools(
        [FromQuery] SpoolQueryParameters query)
    {
        _logger.LogDebug(
            "Getting spools: pageNumber={PageNumber}, pageSize={PageSize}, " +
            "materialBaseId={MaterialBaseId}, materialFinishId={MaterialFinishId}, isActive={IsActive}",
            query.PageNumber, query.PageSize, query.MaterialBaseId, query.MaterialFinishId, query.IsActive);

        // Clamp pagination values
        var pageNumber = Math.Max(1, query.PageNumber);
        var pageSize = Math.Clamp(query.PageSize, 1, 100);

        var spoolQuery = _dbContext.Spools
            .Include(s => s.MaterialBase)
            .Include(s => s.MaterialFinish)
            .Include(s => s.MaterialModifier)
            .AsQueryable();

        // Apply filters
        if (query.MaterialBaseId.HasValue)
            spoolQuery = spoolQuery.Where(s => s.MaterialBaseId == query.MaterialBaseId.Value);

        if (query.MaterialFinishId.HasValue)
            spoolQuery = spoolQuery.Where(s => s.MaterialFinishId == query.MaterialFinishId.Value);

        if (query.IsActive.HasValue)
            spoolQuery = spoolQuery.Where(s => s.IsActive == query.IsActive.Value);

        var totalCount = await spoolQuery.CountAsync();

        var items = await spoolQuery
            .OrderByDescending(s => s.CreatedAt)
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .Select(s => MapToSpoolResponse(s))
            .ToListAsync();

        var response = new PagedResponse<SpoolResponse>
        {
            Items = items,
            TotalCount = totalCount,
            PageNumber = pageNumber,
            PageSize = pageSize
        };

        return Ok(response);
    }

    /// <summary>
    /// Gets a specific spool by its unique identifier.
    /// Includes denormalized material names for display.
    /// </summary>
    /// <param name="id">The unique identifier of the spool.</param>
    /// <returns>The spool details.</returns>
    /// <response code="200">Returns the spool details.</response>
    /// <response code="404">If the spool with the given ID is not found.</response>
    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof(SpoolResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<SpoolResponse>> GetSpool(Guid id)
    {
        _logger.LogDebug("Getting spool {Id}", id);

        var spool = await _dbContext.Spools
            .Include(s => s.MaterialBase)
            .Include(s => s.MaterialFinish)
            .Include(s => s.MaterialModifier)
            .FirstOrDefaultAsync(s => s.Id == id);

        if (spool is null)
        {
            _logger.LogWarning("Spool {Id} not found", id);
            return NotFound(new { error = $"Spool with ID '{id}' not found." });
        }

        return Ok(MapToSpoolResponse(spool));
    }

    /// <summary>
    /// Creates a new spool record. Validates that all foreign keys reference existing entities
    /// and that the SpoolSerial is unique across all spools.
    /// </summary>
    /// <param name="request">The spool creation request with all required fields.</param>
    /// <returns>The newly created spool with generated ID and timestamps.</returns>
    /// <response code="201">Returns the created spool with location header.</response>
    /// <response code="400">If the request is invalid or foreign keys don't exist.</response>
    [HttpPost]
    [ProducesResponseType(typeof(SpoolResponse), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<SpoolResponse>> CreateSpool([FromBody] CreateSpoolRequest request)
    {
        _logger.LogInformation("Creating spool: {Serial} - {Brand} {Color}",
            request.SpoolSerial, request.Brand, request.ColorName);

        // Validate foreign keys exist
        var materialBase = await _dbContext.MaterialBases.FindAsync(request.MaterialBaseId);
        if (materialBase is null)
            return BadRequest(new { error = $"MaterialBase with ID '{request.MaterialBaseId}' not found." });

        var materialFinish = await _dbContext.MaterialFinishes.FindAsync(request.MaterialFinishId);
        if (materialFinish is null)
            return BadRequest(new { error = $"MaterialFinish with ID '{request.MaterialFinishId}' not found." });

        if (request.MaterialModifierId.HasValue)
        {
            var modifier = await _dbContext.MaterialModifiers.FindAsync(request.MaterialModifierId.Value);
            if (modifier is null)
                return BadRequest(new { error = $"MaterialModifier with ID '{request.MaterialModifierId}' not found." });
        }

        // Validate serial uniqueness
        var serialExists = await _dbContext.Spools
            .AnyAsync(s => s.SpoolSerial == request.SpoolSerial);
        if (serialExists)
            return BadRequest(new { error = $"Spool with serial '{request.SpoolSerial}' already exists." });

        // Validate remaining weight doesn't exceed total weight
        if (request.WeightRemainingGrams > request.WeightTotalGrams)
            return BadRequest(new { error = "WeightRemainingGrams cannot exceed WeightTotalGrams." });

        var entity = new Spool
        {
            MaterialBaseId = request.MaterialBaseId,
            MaterialFinishId = request.MaterialFinishId,
            MaterialModifierId = request.MaterialModifierId,
            Brand = request.Brand,
            ColorName = request.ColorName,
            ColorHex = request.ColorHex,
            WeightTotalGrams = request.WeightTotalGrams,
            WeightRemainingGrams = request.WeightRemainingGrams,
            FilamentDiameterMm = request.FilamentDiameterMm,
            SpoolSerial = request.SpoolSerial,
            PurchasePrice = request.PurchasePrice,
            PurchaseDate = request.PurchaseDate,
            IsActive = request.IsActive
        };

        _dbContext.Spools.Add(entity);
        await _dbContext.SaveChangesAsync();

        // Reload navigation properties for the response
        await _dbContext.Entry(entity).Reference(s => s.MaterialBase).LoadAsync();
        await _dbContext.Entry(entity).Reference(s => s.MaterialFinish).LoadAsync();
        if (entity.MaterialModifierId.HasValue)
            await _dbContext.Entry(entity).Reference(s => s.MaterialModifier).LoadAsync();

        var response = MapToSpoolResponse(entity);
        return CreatedAtAction(nameof(GetSpool), new { id = entity.Id }, response);
    }

    /// <summary>
    /// Updates an existing spool. Validates foreign keys and serial uniqueness
    /// (excluding the current spool from the uniqueness check).
    /// </summary>
    /// <param name="id">The unique identifier of the spool to update.</param>
    /// <param name="request">The spool update request with all required fields.</param>
    /// <returns>The updated spool with current timestamps.</returns>
    /// <response code="200">Returns the updated spool.</response>
    /// <response code="404">If the spool with the given ID is not found.</response>
    /// <response code="400">If the request is invalid or foreign keys don't exist.</response>
    [HttpPut("{id:guid}")]
    [ProducesResponseType(typeof(SpoolResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<SpoolResponse>> UpdateSpool(
        Guid id, [FromBody] UpdateSpoolRequest request)
    {
        _logger.LogInformation("Updating spool {Id}", id);

        var entity = await _dbContext.Spools.FindAsync(id);
        if (entity is null)
        {
            _logger.LogWarning("Spool {Id} not found for update", id);
            return NotFound(new { error = $"Spool with ID '{id}' not found." });
        }

        // Validate foreign keys exist
        var materialBase = await _dbContext.MaterialBases.FindAsync(request.MaterialBaseId);
        if (materialBase is null)
            return BadRequest(new { error = $"MaterialBase with ID '{request.MaterialBaseId}' not found." });

        var materialFinish = await _dbContext.MaterialFinishes.FindAsync(request.MaterialFinishId);
        if (materialFinish is null)
            return BadRequest(new { error = $"MaterialFinish with ID '{request.MaterialFinishId}' not found." });

        if (request.MaterialModifierId.HasValue)
        {
            var modifier = await _dbContext.MaterialModifiers.FindAsync(request.MaterialModifierId.Value);
            if (modifier is null)
                return BadRequest(new { error = $"MaterialModifier with ID '{request.MaterialModifierId}' not found." });
        }

        // Check serial uniqueness (excluding current spool)
        var serialExists = await _dbContext.Spools
            .AnyAsync(s => s.SpoolSerial == request.SpoolSerial && s.Id != id);
        if (serialExists)
            return BadRequest(new { error = $"Spool with serial '{request.SpoolSerial}' already exists." });

        // Validate remaining weight doesn't exceed total weight
        if (request.WeightRemainingGrams > request.WeightTotalGrams)
            return BadRequest(new { error = "WeightRemainingGrams cannot exceed WeightTotalGrams." });

        entity.MaterialBaseId = request.MaterialBaseId;
        entity.MaterialFinishId = request.MaterialFinishId;
        entity.MaterialModifierId = request.MaterialModifierId;
        entity.Brand = request.Brand;
        entity.ColorName = request.ColorName;
        entity.ColorHex = request.ColorHex;
        entity.WeightTotalGrams = request.WeightTotalGrams;
        entity.WeightRemainingGrams = request.WeightRemainingGrams;
        entity.FilamentDiameterMm = request.FilamentDiameterMm;
        entity.SpoolSerial = request.SpoolSerial;
        entity.PurchasePrice = request.PurchasePrice;
        entity.PurchaseDate = request.PurchaseDate;
        entity.IsActive = request.IsActive;

        await _dbContext.SaveChangesAsync();

        // Reload navigation properties
        await _dbContext.Entry(entity).Reference(s => s.MaterialBase).LoadAsync();
        await _dbContext.Entry(entity).Reference(s => s.MaterialFinish).LoadAsync();
        if (entity.MaterialModifierId.HasValue)
            await _dbContext.Entry(entity).Reference(s => s.MaterialModifier).LoadAsync();

        return Ok(MapToSpoolResponse(entity));
    }

    /// <summary>
    /// Soft-deletes a spool by setting IsActive = false. The spool is retained
    /// in the database for historical COGS and print job records.
    /// This is NOT a hard delete — the data is preserved.
    /// </summary>
    /// <param name="id">The unique identifier of the spool to soft-delete.</param>
    /// <returns>No content on success.</returns>
    /// <response code="204">The spool was successfully soft-deleted.</response>
    /// <response code="404">If the spool with the given ID is not found.</response>
    [HttpDelete("{id:guid}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> DeleteSpool(Guid id)
    {
        _logger.LogInformation("Soft-deleting spool {Id}", id);

        var entity = await _dbContext.Spools.FindAsync(id);
        if (entity is null)
        {
            _logger.LogWarning("Spool {Id} not found for soft-delete", id);
            return NotFound(new { error = $"Spool with ID '{id}' not found." });
        }

        if (!entity.IsActive)
        {
            _logger.LogDebug("Spool {Id} is already inactive — idempotent no-op", id);
            return NoContent();
        }

        entity.IsActive = false;
        await _dbContext.SaveChangesAsync();

        _logger.LogInformation("Spool {Id} soft-deleted (IsActive = false)", id);
        return NoContent();
    }

    // ── Mapping helper ─────────────────────────────────────────

    private static SpoolResponse MapToSpoolResponse(Spool s) => new()
    {
        Id = s.Id,
        MaterialBaseId = s.MaterialBaseId,
        MaterialBaseName = s.MaterialBase?.Name ?? string.Empty,
        MaterialFinishId = s.MaterialFinishId,
        MaterialFinishName = s.MaterialFinish?.Name ?? string.Empty,
        MaterialModifierId = s.MaterialModifierId,
        MaterialModifierName = s.MaterialModifier?.Name,
        Brand = s.Brand,
        ColorName = s.ColorName,
        ColorHex = s.ColorHex,
        WeightTotalGrams = s.WeightTotalGrams,
        WeightRemainingGrams = s.WeightRemainingGrams,
        FilamentDiameterMm = s.FilamentDiameterMm,
        SpoolSerial = s.SpoolSerial,
        PurchasePrice = s.PurchasePrice,
        PurchaseDate = s.PurchaseDate,
        IsActive = s.IsActive,
        CreatedAt = s.CreatedAt,
        UpdatedAt = s.UpdatedAt
    };
}