using Domain.Constants;
using Domain.Dtos.Stock;
using Domain.Entities;
using Domain.Generics;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Models.Helpers;
using Models.Interfaces;
using Models.Models;
using System.Data;
using System.Globalization;
using System.Text;

namespace Models.Repositories.Stock
{
    public class PhLSMExpeditionRepository(
        PhronCareOperationsHubContext context,
        IPhSFormSeriesRepository formSeriesRepository) : IExpeditionRepository
    {
        private readonly PhronCareOperationsHubContext _context = context;
        private readonly IPhSFormSeriesRepository _formSeriesRepository = formSeriesRepository;

        /// <summary>
        /// Crea la expedición completa (header + details) con numeración de serie y estado emitido.
        /// </summary>
        public async Task<(int Id, string Expeditionnumber)> CreateFullExpeditionAsync(
            ELSExpeditionHeader expedition, int formSeriesId)
        {
            using var tx = await _context.Database.BeginTransactionAsync();
            try
            {
                // 1) Numeración (EX-00000000) – mismo patrón que Quotes
                var next = await _formSeriesRepository.GetNextInternalNumberAsync(formSeriesId);
                var series = await _formSeriesRepository.GetByIdAsync(formSeriesId)
                             ?? throw new InvalidOperationException("Serie no encontrada");
                var number = $"{series.Letter}-{next:D8}";
                var issuedAt = DateTime.Now;

                // 2) Completar datos de emisión en el agregado de dominio
                expedition.Expeditionnumber = number;
                expedition.Issuedate = issuedAt;
                expedition.Status = (int)ExpeditionStatus.Emitida;

                // 3) Mapear grafo completo Domain -> EF (Header + Details)
                //    Igual que haces en CreateFullQuoteAsync con EntityMapper.MapEntity(...)
                var headerEntity = EntityMapper.MapEntity<ELSExpeditionHeader, PhLsmExpeditionHeader>(expedition);

                // 4) Persistir de una (header + colecciones) y confirmar
                _context.PhLsmExpeditionHeaders.Add(headerEntity);
                await _context.SaveChangesAsync();
                await tx.CommitAsync();

                return (headerEntity.Id, headerEntity.Expeditionnumber);
            }
            catch
            {
                await tx.RollbackAsync();
                throw;
            }
        }
        /// <summary>
        /// Devuelve el DTO completo de Expedición (cabecera + ítems) listo para UI/impresión.
        /// </summary>
        public async Task<ExpeditionDto?> GetDtoByIdAsync(int id)
        {
            // 1) Header + detalles
            var header = await _context.PhLsmExpeditionHeaders
                .AsNoTracking()
                .Include(h => h.PhLsmExpeditionDetails)
                .FirstOrDefaultAsync(h => h.Id == id);

            if (header is null)
                return null;

            // 2) Resolver productos (un solo round-trip)
            var productIds = header.PhLsmExpeditionDetails
                .Select(d => d.ProductId)
                .Distinct()
                .ToList();

            var productMap = productIds.Count == 0
                ? new Dictionary<int, (string? Name, string? Descripcion, string? FactoryCode, string? ExternalCode, string? RegulatoryCode)>()
                : await _context.PhLsmProducts
                    .Where(p => productIds.Contains(p.Id))
                    .Select(p => new
                    {
                        p.Id,
                        p.Name,
                        p.Descripcion,
                        p.FactoryCode,     // código fábrica (preferido en impresión)
                        p.ExternalCode,    // GTIN
                        p.RegulatoryCode   // PM
                    })
                    .ToDictionaryAsync(
                        p => p.Id,
                        p => (p.Name, p.Descripcion, p.FactoryCode, p.ExternalCode, p.RegulatoryCode)
                    );

            //// 3) Resolver nombres de ubicaciones (si corresponde)
            //var locationIds = header.PhLsmExpeditionDetails
            //    .Select(d => d.LocationId)
            //    .Where(l => l.HasValue)
            //    .Select(l => l!.Value)
            //    .Distinct()
            //    .ToList();

            //var locationMap = locationIds.Count == 0
            //    ? new Dictionary<int, (string Name, string? Address)>()
            //    : await _context.PhLsmStockLocations
            //        .Where(l => locationIds.Contains(l.Id))
            //        .Select(l => new { l.Id, l.Name, l.Address }) // Address opcional
            //        .ToDictionaryAsync(l => l.Id, l => (l.Name, l.Address));

            // 4) Proyección a DTO (ítems)
            var items = header.PhLsmExpeditionDetails.Select(d =>
            {
                productMap.TryGetValue(d.ProductId, out var p);
                var productName = !string.IsNullOrWhiteSpace(p.Name) ? p.Name
                                 : (!string.IsNullOrWhiteSpace(p.Descripcion) ? p.Descripcion : string.Empty);

                //var locationName = (d.LocationId.HasValue && locationMap.TryGetValue(d.LocationId.Value, out var ln))
                //    ? ln.Name
                //    : null;

                return new ExpeditionItemDto
                {
                    Id = d.Id,
                    ProductId = d.ProductId,
                    FactoryCode = p.FactoryCode ?? string.Empty, // preferido para mostrar
                    ProductName = productName,
                    Quantity = d.Quantity,
                    Batch = d.Batch,
                    Serial = d.Serial,
                    Expiration = d.Expiration,
                    LocationId = d.LocationId,
                    LocationName = string.Empty, // locationName, // si lo querés mostrar
                };
            }).ToList();

            // 5) Completar cabecera del DTO
            var dto = new ExpeditionDto
            {
                Id = header.Id,
                Expeditionnumber = header.Expeditionnumber,
                Issuedate = header.Issuedate,
                Status = header.Status,
                StatusLabel = MapStatus(header.Status),
                ExtrainfoJson = header.ExtrainfoJson,   // se arma en el momento de imprimir, como definiste
                Observations = header.Observations,
                // Opcional: si el header tiene BusinessUnitId / SeriesId, podés resolver aquí sus códigos/nombres.
                Items = items
            };

            //// 6) Si todos los detalles comparten la misma ubicación, reflejarla en cabecera (útil para impresión)
            //var distinctLocs = items.Select(i => i.LocationId).Where(x => x.HasValue).Distinct().ToList();
            //if (distinctLocs.Count == 1)
            //{
            //    dto.LocationId = distinctLocs[0];
            //    if (dto.LocationId.HasValue && locationMap.TryGetValue(dto.LocationId.Value, out var ln))
            //    {
            //        dto.LocationName = ln.Name;
            //        // dto.LocationAddress = ln.Address; // si tu DTO lo contempla
            //    }
            //}

            return dto;
        }
        // ----- helpers -----
        /// <summary>
        /// Mapea el estado entero a etiqueta amigable (enum: Emitida=1, EnTransito=2, EnDestino=3, Retorno=4, Cerrada=5, Anulada=6).
        /// </summary>
        private static string MapStatus(int status) => status switch
        {
            1 => "Emitida",
            2 => "En tránsito",
            3 => "En destino",
            4 => "Retorno",
            5 => "Cerrada",
            6 => "Anulada",
            _ => $"Desconocido ({status})"
        };
        // ===========================
        // BÚSQUEDA + PAGINACIÓN
        // ===========================
        public async Task<PagedResult<ExpeditionDto>> SearchAsync(
            string? expeditionNumber,
            string? status,
            DateTime? issueDateFrom,
            DateTime? issueDateTo,
            int? locationId,
            int page,
            int pageSize)
        {
            if (page <= 0) page = 1;
            if (pageSize <= 0) pageSize = 10;

            // NOTE: ajustá el DbSet si tu entidad se llama distinto
            var q = _context.PhLsmExpeditionHeaders
                .AsNoTracking()
                .AsQueryable();

            // Número
            if (!string.IsNullOrWhiteSpace(expeditionNumber))
            {
                var num = expeditionNumber.Trim();
                q = q.Where(h => EF.Functions.Like(h.Expeditionnumber!, $"%{num}%"));
            }

            // Estado (acepta etiqueta o número)
            if (!string.IsNullOrWhiteSpace(status))
            {
                var st = MapStatusLabelToInt(status);
                if (st.HasValue) q = q.Where(h => h.Status == st.Value);
            }

            // Fechas (inclusivo)
            if (issueDateFrom.HasValue)
            {
                var from = issueDateFrom.Value.Date;
                q = q.Where(h => h.Issuedate >= from);
            }
            if (issueDateTo.HasValue)
            {
                var toExclusive = issueDateTo.Value.Date.AddDays(1);
                q = q.Where(h => h.Issuedate < toExclusive);
            }

            // Ubicación (si la cabecera no la tiene, filtramos por detalles)
            if (locationId.HasValue)
            {
                q = q.Where(h => h.PhLsmExpeditionDetails.Any(d => d.LocationId == locationId.Value));
            }

            var total = await q.CountAsync();

            // Página (más recientes primero)
            var headers = await q
                .OrderByDescending(h => h.Issuedate)
                .ThenByDescending(h => h.Id)
                .Skip((page - 1) * pageSize)
                .Take(pageSize)
                .Select(h => new ExpeditionDto
                {
                    Id = h.Id,
                    Expeditionnumber = h.Expeditionnumber!,
                    Issuedate = h.Issuedate,                  // no-nullable
                    Status = h.Status,
                    StatusLabel = MapStatus(h.Status),
                    ExternalReference=h.ExternalReference,
                    ExtrainfoJson = h.ExtrainfoJson,
                    Observations = h.Observations,
                    Printcount = h.Printcount,                // ← ajustá si tu entidad usa otro nombre
                    Createdat = h.Createdat,                  // ← idem
                    Modifiedat = h.Modifiedat,                // ← idem
                    LocationId = 0,                           // por defecto (se resuelve si todos los ítems comparten)
                    LocationName = null
                })
                .ToListAsync();

            if (headers.Count > 0)
            {
                var headerIds = headers.Select(x => x.Id).ToList();

                // Distintas ubicaciones por header (en base a detalles)
                var locsPerHeader = await _context.PhLsmExpeditionDetails
                    .AsNoTracking()
                    .Where(d => headerIds.Contains(d.ExpeditionId))   // ← ajustá si es HeaderId
                    .GroupBy(d => d.ExpeditionId)
                    .Select(g => new
                    {
                        HeaderId = g.Key,
                        DistinctLocs = g.Select(x => x.LocationId).Distinct().ToList()
                    })
                    .ToListAsync();

                // Diccionario de nombres de ubicación
                //var allLocIds = locsPerHeader.SelectMany(x => x.DistinctLocs).Distinct().ToList();
                //var locNames = await _context.PhLsmStockLocations
                //    .AsNoTracking()
                //    .Where(l => allLocIds.Contains(l.Id))
                //    .Select(l => new { l.Id, l.Name })
                //    .ToDictionaryAsync(x => x.Id, x => x.Name);

                //// Asignar LocationId/Name sólo si TODOS los items comparten la misma
                //foreach (var h in headers)
                //{
                //    var entry = locsPerHeader.FirstOrDefault(x => x.HeaderId == h.Id);
                //    if (entry is null) continue;

                //    if (entry.DistinctLocs.Count == 1)
                //    {
                //        var lid = entry.DistinctLocs[0];
                //        h.LocationId = lid;
                //        if (locNames.TryGetValue(lid, out var lname))
                //            h.LocationName = lname;
                //    }
                //    else
                //    {
                //        // varias ubicaciones: dejamos LocationId=0 y LocationName=null
                //    }
                //}
            }

            return new PagedResult<ExpeditionDto>
            {
                Items = headers,
                TotalItems = total,
                Page = page,
                PageSize = pageSize
            };
        }
        public async Task<List<StockItemExpeditionConflictDto>> CheckStockItemConflictsAsync(
            IEnumerable<int> stockItemIds,
            int? ignoreExpeditionId)
        {
            // Normalización defensiva
            var ids = (stockItemIds ?? Enumerable.Empty<int>())
                .Where(x => x > 0)
                .Distinct()
                .ToList();

            if (ids.Count == 0)
                return new List<StockItemExpeditionConflictDto>();

            // TVP: dbo.PhLSM_StockItemIdList(stockitem_id int not null)
            var tvp = new DataTable();
            tvp.Columns.Add("stockitem_id", typeof(int));

            foreach (var id in ids)
                tvp.Rows.Add(id);

            var results = new List<StockItemExpeditionConflictDto>();

            // Usamos la conexión del DbContext (no creamos otra)
            var conn = _context.Database.GetDbConnection();
            if (conn.State != ConnectionState.Open)
                await _context.Database.OpenConnectionAsync();

            await using var cmd = conn.CreateCommand();
            cmd.CommandText = "dbo.PhLSM_Expedition_CheckStockItemConflicts";
            cmd.CommandType = CommandType.StoredProcedure;

            // Param TVP
            var pIds = new SqlParameter("@StockItemIds", SqlDbType.Structured)
            {
                TypeName = "dbo.PhLSM_StockItemIdList",
                Value = tvp
            };
            cmd.Parameters.Add(pIds);

            // Param opcional para edición
            var pIgnore = new SqlParameter("@IgnoreExpeditionId", SqlDbType.Int)
            {
                Value = ignoreExpeditionId.HasValue ? ignoreExpeditionId.Value : DBNull.Value
            };
            cmd.Parameters.Add(pIgnore);

            await using var reader = await cmd.ExecuteReaderAsync();

            // Ordinals por nombre (más robusto ante cambios de orden)
            var ordStockItemId = reader.GetOrdinal("StockitemId");
            var ordExpId = reader.GetOrdinal("ExpeditionId");
            var ordExpNum = reader.GetOrdinal("Expeditionnumber");
            var ordStatus = reader.GetOrdinal("Status");

            while (await reader.ReadAsync())
            {
                results.Add(new StockItemExpeditionConflictDto
                {
                    StockitemId = reader.GetInt32(ordStockItemId),
                    ExpeditionId = reader.GetInt32(ordExpId),
                    Expeditionnumber = reader.IsDBNull(ordExpNum) ? string.Empty : reader.GetString(ordExpNum),
                    Status = reader.GetInt32(ordStatus)
                });
            }

            return results;
        }
        private static int? MapStatusLabelToInt(string labelOrNumber)
        {
            if (string.IsNullOrWhiteSpace(labelOrNumber)) return null;

            if (int.TryParse(labelOrNumber, out var n) && n is >= 1 and <= 6)
                return n;

            var key = NormalizeKey(labelOrNumber);
            return key switch
            {
                "emitida" => 1,
                "entransito" => 2,  // cubre "en transito" y "en tránsito"
                "endestino" => 3,
                "retorno" => 4,
                "cerrada" => 5,
                "anulada" => 6,
                _ => (int?)null
            };
        }
        private static string NormalizeKey(string s)
        {
            var norm = s.Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD);
            var sb = new StringBuilder(norm.Length);
            foreach (var ch in norm)
                if (CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark)
                    sb.Append(ch);
            return sb.ToString().Normalize(NormalizationForm.FormC).Replace(" ", "");
        }
        public async Task MarkInTransitAsync(int expeditionId)
        {
            const byte expeditionReservationSourceType = 1;
            const int reservedStatus = 1;

            var header = await _context.PhLsmExpeditionHeaders
                .Include(x => x.PhLsmExpeditionDetails)
                .FirstOrDefaultAsync(x => x.Id == expeditionId);

            if (header == null)
                throw new KeyNotFoundException($"No se encontró la expedición con ID {expeditionId}.");

            if (header.Status != (int)ExpeditionStatus.Emitida)
                throw new InvalidOperationException("Solo las expediciones en estado 'Emitida' pueden pasar a 'En tránsito'.");

            var details = header.PhLsmExpeditionDetails?.ToList() ?? new List<PhLsmExpeditionDetail>();

            if (details.Count == 0)
                throw new InvalidOperationException("No se puede pasar la expedición a 'En tránsito' porque no tiene ítems para reservar.");

            var invalidStockItems = details
                .Where(d => d.StockitemId <= 0)
                .Select(d => d.Id)
                .OrderBy(x => x)
                .ToList();
            if (invalidStockItems.Count > 0)
            {
                throw new InvalidOperationException(
                    "No se puede pasar la expedición a 'En tránsito' porque existen detalles sin stockitem_id válido. " +
                    $"Detalle(s): {string.Join(", ", invalidStockItems)}");
            }

            var duplicateStockItems = details
                .GroupBy(d => d.StockitemId)
                .Where(g => g.Count() > 1)
                .Select(g => g.Key)
                .OrderBy(x => x)
                .ToList();

            if (duplicateStockItems.Count > 0)
            {
                throw new InvalidOperationException(
                    "No se puede pasar la expedición a 'En tránsito' porque el mismo StockItem aparece más de una vez en la expedición: " +
                    string.Join(", ", duplicateStockItems));
            }

            var detailByStockItem = details
                .Select(d => new
                {
                    DetailId = d.Id,
                    StockitemId = d.StockitemId,
                    Quantity = d.Quantity
                })
                .ToList();

            var stockItemIds = detailByStockItem
                .Select(x => x.StockitemId)
                .Distinct()
                .ToList();

            var duplicatedReservations = await _context.PhLsmStockReservations
                .AsNoTracking()
                .Where(r =>
                    r.SourceType == expeditionReservationSourceType &&
                    r.SourceId == expeditionId &&
                    r.Status == reservedStatus &&
                    stockItemIds.Contains(r.StockitemId))
                .Select(r => r.StockitemId)
                .Distinct()
                .OrderBy(x => x)
                .ToListAsync();

            if (duplicatedReservations.Count > 0)
            {
                throw new InvalidOperationException(
                    "La expedición ya posee reservas activas para los siguientes StockItem: " +
                    string.Join(", ", duplicatedReservations));
            }

            var stockItems = await _context.PhLsmStockItems
                .Where(x => stockItemIds.Contains(x.Id))
                .ToListAsync();

            var stockItemsById = stockItems.ToDictionary(x => x.Id);

            var missingStockItems = stockItemIds
                .Where(id => !stockItemsById.ContainsKey(id))
                .OrderBy(x => x)
                .ToList();

            if (missingStockItems.Count > 0)
            {
                throw new InvalidOperationException(
                    "No se puede pasar la expedición a 'En tránsito' porque algunos StockItem no existen: " +
                    string.Join(", ", missingStockItems));
            }

            var insufficientAvailability = new List<string>();

            foreach (var item in detailByStockItem.OrderBy(x => x.StockitemId))
            {
                var stockItem = stockItemsById[item.StockitemId];
                var availableQuantity = stockItem.Quantity - stockItem.ReservedQuantity;

                if (item.Quantity > availableQuantity)
                {
                    insufficientAvailability.Add(
                        $"• StockItem {item.StockitemId} → solicitado: {item.Quantity}, disponible: {availableQuantity}.");
                }
            }

            if (insufficientAvailability.Count > 0)
            {
                var lines = new List<string>
                {
                    "No se puede pasar la expedición a 'En tránsito' porque algunos StockItem no tienen cantidad disponible suficiente para reservar."
                };

                lines.AddRange(insufficientAvailability);

                throw new InvalidOperationException(string.Join(Environment.NewLine, lines));
            }

            using var tx = await _context.Database.BeginTransactionAsync();

            try
            {
                var now = DateTime.Now;

                var reservations = detailByStockItem.Select(item => new PhLsmStockReservation
                {
                    SourceType = expeditionReservationSourceType,
                    SourceId = expeditionId,
                    StockitemId = item.StockitemId,
                    ReservedQuantity = item.Quantity,
                    Status = reservedStatus,
                    Createdat = now
                }).ToList();

                _context.PhLsmStockReservations.AddRange(reservations);

                foreach (var item in detailByStockItem)
                {
                    var stockItem = stockItemsById[item.StockitemId];
                    stockItem.ReservedQuantity += item.Quantity;
                    stockItem.Modifiedat = now;
                }

                header.Status = (int)ExpeditionStatus.EnTransito;
                header.Modifiedat = now;

                await _context.SaveChangesAsync();
                await tx.CommitAsync();
            }
            catch
            {
                await tx.RollbackAsync();
                throw;
            }
        }
    }
}
