using Extrudex.API.DTOs;
using Extrudex.API.DTOs.Filaments;
using Extrudex.Domain.Entities;
using Extrudex.Domain.Interfaces;
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.
/// Exposes spool data under the user-facing "filaments" route while mapping
/// to the Spool domain entity internally.
/// </summary>
[ApiController]
[Route("api/filaments")]
public class FilamentsController : ControllerBase
{
    private readonly ExtrudexDbContext _dbContext;
    private readonly ILowStockDetector _lowStockDetector;
    private readonly ILogger<FilamentsController> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="FilamentsController"/> class.
    /// </summary>
    /// <param name="dbContext">The database context for data access.</param>
    /// <param name="lowStockDetector">The low-stock detection service for filament alerts.</param>
    /// <param name="logger">The logger for diagnostic output.</param>
    public FilamentsController(
        ExtrudexDbContext dbContext,
        ILowStockDetector lowStockDetector,
        ILogger<FilamentsController> logger)
    {
        _dbContext = dbContext;
        _lowStockDetector = lowStockDetector;
        _logger = logger;
    }

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

        // 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.MaterialModifierId.HasValue)
            spoolQuery = spoolQuery.Where(s => s.MaterialModifierId == query.MaterialModifierId.Value);

        if (!string.IsNullOrWhiteSpace(query.Brand))
            spoolQuery = spoolQuery.Where(s =>
                s.Brand.ToLower().Contains(query.Brand.ToLower()));

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

        // Exclude archived spools by default; include when explicitly requested
        if (query.IncludeArchived != true)
            spoolQuery = spoolQuery.Where(s => !s.IsArchived);

        if (!string.IsNullOrWhiteSpace(query.StorageLocation))
            spoolQuery = spoolQuery.Where(s =>
                s.StorageLocation != null &&
                s.StorageLocation.ToLower().Contains(query.StorageLocation.ToLower()));

        var totalCount = await spoolQuery.CountAsync();

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

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

        return Ok(response);
    }

    /// <summary>
    /// Gets a specific filament spool by its unique identifier.
    /// Includes denormalized material names for display.
    /// </summary>
    /// <param name="id">The unique identifier of the filament spool.</param>
    /// <returns>The filament spool details.</returns>
    /// <response code="200">Returns the filament spool details.</response>
    /// <response code="404">If the filament spool with the given ID is not found.</response>
    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof(FilamentResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<FilamentResponse>> GetFilament(Guid id)
    {
        _logger.LogDebug("Getting filament {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("Filament {Id} not found", id);
            return NotFound(new { error = $"Filament with ID '{id}' not found." });
        }

        return Ok(MapToFilamentResponse(spool, _lowStockDetector));
    }

    /// <summary>
    /// Creates a new filament spool. Validates that all foreign keys reference
    /// existing material entities and that the SpoolSerial is unique.
    /// WeightRemainingGrams must not exceed WeightTotalGrams.
    /// </summary>
    /// <param name="request">The filament creation request with all required fields.</param>
    /// <returns>The newly created filament spool with generated ID and timestamps.</returns>
    /// <response code="201">Returns the created filament spool with location header.</response>
    /// <response code="400">If the request is invalid or foreign keys don't exist.</response>
    [HttpPost]
    [ProducesResponseType(typeof(FilamentResponse), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<FilamentResponse>> CreateFilament(
        [FromBody] CreateFilamentRequest request)
    {
        _logger.LogInformation("Creating filament: {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 = $"Filament 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,
            IsArchived = request.IsArchived,
            StorageLocation = request.StorageLocation
        };

        _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 = MapToFilamentResponse(entity, _lowStockDetector);
        return CreatedAtAction(nameof(GetFilament), new { id = entity.Id }, response);
    }

    /// <summary>
    /// Updates an existing filament spool. Validates foreign keys and serial uniqueness
    /// (excluding the current spool from the uniqueness check).
    /// WeightRemainingGrams must not exceed WeightTotalGrams.
    /// </summary>
    /// <param name="id">The unique identifier of the filament spool to update.</param>
    /// <param name="request">The filament update request with all required fields.</param>
    /// <returns>The updated filament spool with current timestamps.</returns>
    /// <response code="200">Returns the updated filament spool.</response>
    /// <response code="404">If the filament 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(FilamentResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<FilamentResponse>> UpdateFilament(
        Guid id, [FromBody] UpdateFilamentRequest request)
    {
        _logger.LogInformation("Updating filament {Id}", id);

        var entity = await _dbContext.Spools.FindAsync(id);
        if (entity is null)
        {
            _logger.LogWarning("Filament {Id} not found for update", id);
            return NotFound(new { error = $"Filament 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 = $"Filament 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;
        entity.IsArchived = request.IsArchived;
        entity.StorageLocation = request.StorageLocation;

        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(MapToFilamentResponse(entity, _lowStockDetector));
    }

    /// <summary>
    /// Gets only the filament spools that are flagged as low stock.
    /// A spool is considered low stock when its remaining weight percentage
    /// is at or below the configured threshold.
    /// </summary>
    /// <returns>A list of low-stock filament spools with alert metadata.</returns>
    /// <response code="200">Returns the list of low-stock filament spools.</response>
    [HttpGet("low-stock")]
    [ProducesResponseType(typeof(List<FilamentResponse>), StatusCodes.Status200OK)]
    public async Task<ActionResult<List<FilamentResponse>>> GetLowStockFilaments()
    {
        _logger.LogDebug("Getting low-stock filaments (threshold: {Threshold}%)",
            _lowStockDetector.LowStockThresholdPercent);

        var spools = await _dbContext.Spools
            .Include(s => s.MaterialBase)
            .Include(s => s.MaterialFinish)
            .Include(s => s.MaterialModifier)
            .Where(s => s.IsActive)
            .OrderByDescending(s => s.CreatedAt)
            .ToListAsync();

        var lowStockItems = spools
            .Where(s => _lowStockDetector.IsLowStock(s.WeightRemainingGrams, s.WeightTotalGrams))
            .Select(s => MapToFilamentResponse(s, _lowStockDetector))
            .ToList();

        return Ok(lowStockItems);
    }

    /// <summary>
    /// Deletes a filament spool by its unique identifier.
    /// If the spool has associated print jobs, the deletion is rejected with a 409 Conflict
    /// to preserve COGS and print history — the caller should archive the spool instead.
    /// Associated filament usage records are removed before the spool is deleted.
    /// AMS slots referencing this spool will have their SpoolId set to null by the database.
    /// </summary>
    /// <param name="id">The unique identifier of the filament spool to delete.</param>
    /// <returns>No content on successful deletion.</returns>
    /// <response code="204">The filament spool was successfully deleted.</response>
    /// <response code="404">If the filament spool with the given ID is not found.</response>
    /// <response code="409">If the spool has associated print jobs and cannot be deleted.</response>
    [HttpDelete("{id:guid}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<IActionResult> DeleteFilament(Guid id)
    {
        _logger.LogInformation("Deleting filament {Id}", id);

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

        // Check for associated print jobs — these cannot be orphaned
        var hasPrintJobs = await _dbContext.PrintJobs.AnyAsync(pj => pj.SpoolId == id);
        if (hasPrintJobs)
        {
            _logger.LogWarning(
                "Cannot delete filament {Id}: associated print jobs exist. Suggest archiving instead.", id);
            return Conflict(new
            {
                error = $"Cannot delete filament '{id}' because it has associated print jobs. " +
                        "Archive the filament instead to preserve print history and COGS data."
            });
        }

        // Remove associated filament usage records (usage tracking data for this spool)
        var usageRecords = await _dbContext.FilamentUsages
            .Where(fu => fu.SpoolId == id)
            .ToListAsync();

        if (usageRecords.Count > 0)
        {
            _logger.LogInformation(
                "Removing {Count} filament usage records for spool {Id}",
                usageRecords.Count, id);
            _dbContext.FilamentUsages.RemoveRange(usageRecords);
        }

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

        _logger.LogInformation("Filament {Id} deleted successfully", id);
        return NoContent();
    }

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

    /// <summary>
    /// Maps a Spool domain entity to a FilamentResponse DTO.
    /// Denormalizes material names for display convenience.
    /// Populates the QrCodeUrl for easy frontend access to the spool's QR code.
    /// Calculates low-stock status and remaining weight percentage.
    /// </summary>
    /// <param name="s">The spool entity to map.</param>
    /// <param name="lowStockDetector">The low-stock detection service for computing alert flags.</param>
    /// <returns>A FilamentResponse DTO with denormalized material names, QR code URL, and low-stock metadata.</returns>
    private static FilamentResponse MapToFilamentResponse(Spool s, ILowStockDetector lowStockDetector) => 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,
        IsArchived = s.IsArchived,
        StorageLocation = s.StorageLocation,
        CreatedAt = s.CreatedAt,
        UpdatedAt = s.UpdatedAt,
        QrCodeUrl = $"/api/qr/spool/{s.Id}",
        IsLowStock = lowStockDetector.IsLowStock(s.WeightRemainingGrams, s.WeightTotalGrams),
        RemainingWeightPercent = lowStockDetector.GetRemainingWeightPercent(s.WeightRemainingGrams, s.WeightTotalGrams)
    };
}