using GestionIntegral.Api.Data;
using GestionIntegral.Api.Data.Repositories.Contables;
using GestionIntegral.Api.Data.Repositories.Distribucion;
using GestionIntegral.Api.Dtos.Auditoria;
using GestionIntegral.Api.Dtos.Distribucion;
using GestionIntegral.Api.Models.Distribucion;
using GestionIntegral.Api.Services.Contables;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;

namespace GestionIntegral.Api.Services.Distribucion
{
    public class EntradaSalidaDistService : IEntradaSalidaDistService
    {
        private readonly IEntradaSalidaDistRepository _esRepository;
        private readonly IPublicacionRepository _publicacionRepository;
        private readonly IDistribuidorRepository _distribuidorRepository;
        private readonly IPrecioRepository _precioRepository;
        private readonly IRecargoZonaRepository _recargoZonaRepository;
        private readonly IPorcPagoRepository _porcPagoRepository;
        private readonly ISaldoRepository _saldoRepository;
        private readonly IEmpresaRepository _empresaRepository; // Para obtener IdEmpresa de la publicación
        private readonly IPeriodoCerradoValidator _periodoCerrado;
        private readonly DbConnectionFactory _connectionFactory;
        private readonly ILogger<EntradaSalidaDistService> _logger;

        public EntradaSalidaDistService(
            IEntradaSalidaDistRepository esRepository,
            IPublicacionRepository publicacionRepository,
            IDistribuidorRepository distribuidorRepository,
            IPrecioRepository precioRepository,
            IRecargoZonaRepository recargoZonaRepository,
            IPorcPagoRepository porcPagoRepository,
            ISaldoRepository saldoRepository,
            IEmpresaRepository empresaRepository,
            IPeriodoCerradoValidator periodoCerrado,
            DbConnectionFactory connectionFactory,
            ILogger<EntradaSalidaDistService> logger)
        {
            _esRepository = esRepository;
            _publicacionRepository = publicacionRepository;
            _distribuidorRepository = distribuidorRepository;
            _precioRepository = precioRepository;
            _recargoZonaRepository = recargoZonaRepository;
            _porcPagoRepository = porcPagoRepository;
            _saldoRepository = saldoRepository;
            _empresaRepository = empresaRepository;
            _periodoCerrado = periodoCerrado;
            _connectionFactory = connectionFactory;
            _logger = logger;
        }

        private async Task<EntradaSalidaDistDto> MapToDto(EntradaSalidaDist es)
        {
            if (es == null) return null!;

            var publicacionData = await _publicacionRepository.GetByIdAsync(es.IdPublicacion);
            var distribuidorData = await _distribuidorRepository.GetByIdAsync(es.IdDistribuidor);

            // Obtener el valor bruto del movimiento
            decimal valorBrutoMovimiento = await CalcularMontoMovimiento(
                es.IdPublicacion, es.IdDistribuidor, es.Fecha, es.Cantidad,
                es.TipoMovimiento, // Pasamos el tipo de movimiento original aquí
                es.IdPrecio, es.IdRecargo, es.IdPorcentaje, distribuidorData.Distribuidor?.IdZona
            );

            decimal montoCalculadoParaDto = valorBrutoMovimiento;

            return new EntradaSalidaDistDto
            {
                IdParte = es.IdParte,
                IdPublicacion = es.IdPublicacion,
                NombrePublicacion = publicacionData.Publicacion?.Nombre ?? "N/A",
                IdEmpresaPublicacion = publicacionData.Publicacion?.IdEmpresa ?? 0,
                NombreEmpresaPublicacion = publicacionData.NombreEmpresa ?? "N/A",
                IdDistribuidor = es.IdDistribuidor,
                NombreDistribuidor = distribuidorData.Distribuidor?.Nombre ?? "N/A",
                Fecha = es.Fecha.ToString("yyyy-MM-dd"),
                TipoMovimiento = es.TipoMovimiento,
                Cantidad = es.Cantidad,
                Remito = es.Remito,
                Observacion = es.Observacion,
                MontoCalculado = montoCalculadoParaDto // Representa el valor de los N ejemplares
            };
        }

        private async Task<decimal> CalcularMontoMovimiento(int idPublicacion, int idDistribuidor, DateTime fecha, int cantidad, string tipoMovimiento,
                                                            int idPrecio, int idRecargo, int idPorcentaje, int? idZonaDistribuidor)
        {
            var precioConfig = await _precioRepository.GetByIdAsync(idPrecio);
            // Es crucial que idPrecio sea válido y se haya determinado correctamente antes de llamar aquí.
            // Si precioConfig es null, se lanzará una excepción abajo, lo cual está bien si es un estado inesperado.
            if (precioConfig == null)
            {
                _logger.LogError("Configuración de precio ID {IdPrecio} no encontrada al calcular monto para Pub {IdPublicacion}, Dist {IdDistribuidor}, Fecha {Fecha}", idPrecio, idPublicacion, idDistribuidor, fecha);
                // Dependiendo de la regla de negocio, podrías devolver 0 o lanzar una excepción.
                // Si un precio es OBLIGATORIO para cualquier movimiento, lanzar excepción es más apropiado.
                // Si puede haber movimientos sin precio (ej. cortesía que no se factura), entonces 0.
                // En este contexto, un precio es fundamental para el cálculo.
                throw new InvalidOperationException($"Configuración de precio ID {idPrecio} no encontrada. No se puede calcular el monto.");
            }

            decimal precioDia = 0;
            DayOfWeek diaSemana = fecha.DayOfWeek;
            switch (diaSemana)
            {
                case DayOfWeek.Monday: precioDia = precioConfig.Lunes ?? 0; break;
                case DayOfWeek.Tuesday: precioDia = precioConfig.Martes ?? 0; break;
                case DayOfWeek.Wednesday: precioDia = precioConfig.Miercoles ?? 0; break;
                case DayOfWeek.Thursday: precioDia = precioConfig.Jueves ?? 0; break;
                case DayOfWeek.Friday: precioDia = precioConfig.Viernes ?? 0; break;
                case DayOfWeek.Saturday: precioDia = precioConfig.Sabado ?? 0; break;
                case DayOfWeek.Sunday: precioDia = precioConfig.Domingo ?? 0; break;
            }

            decimal valorRecargo = 0;
            if (idRecargo > 0 && idZonaDistribuidor.HasValue)
            {
                var recargoConfig = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(idPublicacion, idZonaDistribuidor.Value, fecha);
                if (recargoConfig != null)
                {
                    valorRecargo = recargoConfig.Valor;
                }
            }

            decimal precioConRecargo = precioDia + valorRecargo;
            decimal montoBase = precioConRecargo * cantidad;

            if (idPorcentaje > 0)
            {
                var porcConfig = await _porcPagoRepository.GetByIdAsync(idPorcentaje);
                if (porcConfig != null)
                {
                    // El porcentaje de pago del distribuidor es lo que ÉL PAGA a la editorial.
                    // Entonces, el monto es (precio_tapa_con_recargo * cantidad) * (porcentaje_pago_dist / 100)
                    return (montoBase * porcConfig.Porcentaje) / 100;
                }
            }
            // Si no hay porcentaje de pago específico, se asume que el distribuidor paga el 100% del monto base.
            return montoBase;
        }


        public async Task<IEnumerable<EntradaSalidaDistDto>> ObtenerTodosAsync(DateTime? fechaDesde, DateTime? fechaHasta, int? idPublicacion, int? idDistribuidor, string? tipoMovimiento)
        {
            var movimientos = await _esRepository.GetAllAsync(fechaDesde, fechaHasta, idPublicacion, idDistribuidor, tipoMovimiento);
            var dtos = new List<EntradaSalidaDistDto>();
            foreach (var mov in movimientos)
            {
                dtos.Add(await MapToDto(mov));
            }
            return dtos;
        }

        public async Task<EntradaSalidaDistDto?> ObtenerPorIdAsync(int idParte)
        {
            var movimiento = await _esRepository.GetByIdAsync(idParte);
            return movimiento == null ? null : await MapToDto(movimiento);
        }

        public async Task<(EntradaSalidaDistDto? Movimiento, string? Error)> CrearMovimientoAsync(CreateEntradaSalidaDistDto createDto, int idUsuario)
        {
            var publicacion = await _publicacionRepository.GetByIdSimpleAsync(createDto.IdPublicacion);
            if (publicacion == null || publicacion.Habilitada != true) return (null, "Publicación no válida o no habilitada.");

            var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(createDto.IdDistribuidor);
            if (distribuidor == null) return (null, "Distribuidor no válido.");

            // Bloqueo por período cerrado: la fecha del movimiento no puede caer dentro de un cierre vigente del distribuidor en la empresa de la publicación.
            var bloqueoCrear = await _periodoCerrado.EstaCerradoAsync("Distribuidores", createDto.IdDistribuidor, publicacion.IdEmpresa, createDto.Fecha);
            if (bloqueoCrear.EstaCerrado)
                throw new BloqueoPorPeriodoCerradoException(bloqueoCrear.IdCierre!.Value, bloqueoCrear.FechaCorte!.Value);

            /*
            if (await _esRepository.ExistsByRemitoAndTipoForPublicacionAsync(createDto.Remito, createDto.TipoMovimiento, createDto.IdPublicacion))
            {
                return (null, $"Ya existe un movimiento de '{createDto.TipoMovimiento}' con el remito N°{createDto.Remito} para esta publicación.");
            }*/

            // Determinar IDs de Precio, Recargo y Porcentaje activos en la fecha del movimiento
            var precioActivo = await _precioRepository.GetActiveByPublicacionAndDateAsync(createDto.IdPublicacion, createDto.Fecha.Date);
            if (precioActivo == null) return (null, $"No hay un precio definido para la publicación '{publicacion.Nombre}' en la fecha {createDto.Fecha:dd/MM/yyyy}.");

            RecargoZona? recargoActivo = null;
            if (distribuidor.IdZona.HasValue)
            {
                recargoActivo = await _recargoZonaRepository.GetActiveByPublicacionZonaAndDateAsync(createDto.IdPublicacion, distribuidor.IdZona.Value, createDto.Fecha.Date);
            }

            var porcPagoActivo = await _porcPagoRepository.GetActiveByPublicacionDistribuidorAndDateAsync(createDto.IdPublicacion, createDto.IdDistribuidor, createDto.Fecha.Date);

            var nuevoES = new EntradaSalidaDist
            {
                IdPublicacion = createDto.IdPublicacion,
                IdDistribuidor = createDto.IdDistribuidor,
                Fecha = createDto.Fecha.Date,
                TipoMovimiento = createDto.TipoMovimiento,
                Cantidad = createDto.Cantidad,
                Remito = createDto.Remito,
                Observacion = createDto.Observacion,
                IdPrecio = precioActivo.IdPrecio,
                IdRecargo = recargoActivo?.IdRecargo ?? 0, // 0 si no aplica
                IdPorcentaje = porcPagoActivo?.IdPorcentaje ?? 0 // 0 si no aplica
            };

            using var connection = _connectionFactory.CreateConnection();
            if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
            using var transaction = connection.BeginTransaction();
            try
            {
                var esCreada = await _esRepository.CreateAsync(nuevoES, idUsuario, transaction);
                if (esCreada == null) throw new DataException("Error al registrar el movimiento.");

                // Afectar Saldo
                decimal montoAfectacion = await CalcularMontoMovimiento(
                    esCreada.IdPublicacion, esCreada.IdDistribuidor, esCreada.Fecha, esCreada.Cantidad, esCreada.TipoMovimiento,
                    esCreada.IdPrecio, esCreada.IdRecargo, esCreada.IdPorcentaje, distribuidor.IdZona);

                // Si es Salida, montoAfectacion es positivo (aumenta deuda). Si es Entrada, es 0 por CalcularMontoMovimiento,
                // pero para el saldo, una Entrada (devolución) debería restar del saldo deudor.
                // Por lo tanto, el monto real a restar del saldo si es Entrada sería el valor de esos ejemplares devueltos.
                if (esCreada.TipoMovimiento == "Entrada")
                {
                    // Recalcular como si fuera salida para obtener el valor a acreditar/restar del saldo
                    montoAfectacion = await CalcularMontoMovimiento(
                        esCreada.IdPublicacion, esCreada.IdDistribuidor, esCreada.Fecha, esCreada.Cantidad, "Salida", // Forzar tipo Salida para cálculo de valor
                        esCreada.IdPrecio, esCreada.IdRecargo, esCreada.IdPorcentaje, distribuidor.IdZona);
                    montoAfectacion *= -1; // Hacerlo negativo para restar del saldo
                }


                bool saldoActualizado = await _saldoRepository.ModificarSaldoAsync("Distribuidores", esCreada.IdDistribuidor, publicacion.IdEmpresa, montoAfectacion, transaction);
                if (!saldoActualizado) throw new DataException("Error al actualizar el saldo del distribuidor.");

                transaction.Commit();
                _logger.LogInformation("Movimiento ID {Id} creado y saldo afectado por Usuario ID {UserId}.", esCreada.IdParte, idUsuario);
                return (await MapToDto(esCreada), null);
            }
            catch (Exception ex)
            {
                try { transaction.Rollback(); } catch { }
                _logger.LogError(ex, "Error CrearMovimientoAsync para Pub ID {IdPub}", createDto.IdPublicacion);
                return (null, $"Error interno: {ex.Message}");
            }
        }

        public async Task<(bool Exito, string? Error)> ActualizarMovimientoAsync(int idParte, UpdateEntradaSalidaDistDto updateDto, int idUsuario)
        {
            // La actualización de un movimiento que afecta saldos es compleja.
            // Si cambia la cantidad, el monto original y el nuevo deben calcularse,
            // y la diferencia debe aplicarse al saldo.
            // Por ahora, este DTO solo permite cambiar Cantidad y Observacion.
            // Cambiar otros campos como Fecha, Publicacion, Distribuidor implicaría recalcular todo
            // y posiblemente anular el movimiento original y crear uno nuevo.

            using var connection = _connectionFactory.CreateConnection();
            if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
            using var transaction = connection.BeginTransaction();
            try
            {
                var esExistente = await _esRepository.GetByIdAsync(idParte);
                if (esExistente == null) return (false, "Movimiento no encontrado.");

                var publicacion = await _publicacionRepository.GetByIdSimpleAsync(esExistente.IdPublicacion);
                if (publicacion == null) return (false, "Publicación asociada no encontrada."); // Muy raro
                var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor);
                if (distribuidor == null) return (false, "Distribuidor asociado no encontrado.");

                // Bloqueo por período cerrado sobre la fecha original del movimiento (el DTO de update no permite cambiar Fecha).
                var bloqueoUpd = await _periodoCerrado.EstaCerradoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, esExistente.Fecha);
                if (bloqueoUpd.EstaCerrado)
                {
                    transaction.Rollback();
                    throw new BloqueoPorPeriodoCerradoException(bloqueoUpd.IdCierre!.Value, bloqueoUpd.FechaCorte!.Value);
                }


                // 1. Calcular monto del movimiento original (antes de la actualización)
                decimal montoOriginal = await CalcularMontoMovimiento(
                    esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, esExistente.TipoMovimiento,
                    esExistente.IdPrecio, esExistente.IdRecargo, esExistente.IdPorcentaje, distribuidor.IdZona);
                if (esExistente.TipoMovimiento == "Entrada") montoOriginal *= -1; // Para revertir

                // 2. Actualizar la entidad en la BD (esto también guarda en historial)
                var esParaActualizar = new EntradaSalidaDist
                {
                    IdParte = esExistente.IdParte,
                    IdPublicacion = esExistente.IdPublicacion, // No cambia
                    IdDistribuidor = esExistente.IdDistribuidor, // No cambia
                    Fecha = esExistente.Fecha, // No cambia
                    TipoMovimiento = esExistente.TipoMovimiento, // No cambia
                    Cantidad = updateDto.Cantidad, // Nuevo valor
                    Remito = esExistente.Remito, // No cambia
                    Observacion = updateDto.Observacion, // Nuevo valor
                    IdPrecio = esExistente.IdPrecio, // No cambia
                    IdRecargo = esExistente.IdRecargo, // No cambia
                    IdPorcentaje = esExistente.IdPorcentaje // No cambia
                };
                var actualizado = await _esRepository.UpdateAsync(esParaActualizar, idUsuario, transaction);
                if (!actualizado) throw new DataException("Error al actualizar el movimiento.");

                // 3. Calcular monto del movimiento nuevo (después de la actualización)
                decimal montoNuevo = await CalcularMontoMovimiento(
                    esParaActualizar.IdPublicacion, esParaActualizar.IdDistribuidor, esParaActualizar.Fecha, esParaActualizar.Cantidad, esParaActualizar.TipoMovimiento,
                    esParaActualizar.IdPrecio, esParaActualizar.IdRecargo, esParaActualizar.IdPorcentaje, distribuidor.IdZona);
                if (esParaActualizar.TipoMovimiento == "Entrada") montoNuevo *= -1;


                // 4. Ajustar saldo con la diferencia
                decimal diferenciaAjusteSaldo = montoNuevo - montoOriginal;
                if (diferenciaAjusteSaldo != 0)
                {
                    bool saldoActualizado = await _saldoRepository.ModificarSaldoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, diferenciaAjusteSaldo, transaction);
                    if (!saldoActualizado) throw new DataException("Error al ajustar el saldo del distribuidor tras la actualización.");
                }

                transaction.Commit();
                _logger.LogInformation("Movimiento ID {Id} actualizado y saldo ajustado por Usuario ID {UserId}.", idParte, idUsuario);
                return (true, null);
            }
            catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); }
            catch (BloqueoPorPeriodoCerradoException)
            {
                try { transaction.Rollback(); } catch { }
                throw;
            }
            catch (Exception ex)
            {
                try { transaction.Rollback(); } catch { }
                _logger.LogError(ex, "Error ActualizarMovimientoAsync ID: {Id}", idParte);
                return (false, $"Error interno: {ex.Message}");
            }
        }

        public async Task<(bool Exito, string? Error)> EliminarMovimientoAsync(int idParte, int idUsuario)
        {
            using var connection = _connectionFactory.CreateConnection();
            if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
            using var transaction = connection.BeginTransaction();
            try
            {
                var esExistente = await _esRepository.GetByIdAsync(idParte);
                if (esExistente == null) return (false, "Movimiento no encontrado.");

                var publicacion = await _publicacionRepository.GetByIdSimpleAsync(esExistente.IdPublicacion);
                if (publicacion == null) return (false, "Publicación asociada no encontrada.");
                var distribuidor = await _distribuidorRepository.GetByIdSimpleAsync(esExistente.IdDistribuidor);
                if (distribuidor == null) return (false, "Distribuidor asociado no encontrado.");

                // Bloqueo por período cerrado: no se puede eliminar un movimiento cuya fecha cae en un cierre vigente.
                var bloqueoDel = await _periodoCerrado.EstaCerradoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, esExistente.Fecha);
                if (bloqueoDel.EstaCerrado)
                {
                    transaction.Rollback();
                    throw new BloqueoPorPeriodoCerradoException(bloqueoDel.IdCierre!.Value, bloqueoDel.FechaCorte!.Value);
                }

                // 1. Calcular el monto del movimiento a eliminar para revertir el saldo
                decimal montoReversion = await CalcularMontoMovimiento(
                    esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, esExistente.TipoMovimiento,
                    esExistente.IdPrecio, esExistente.IdRecargo, esExistente.IdPorcentaje, distribuidor.IdZona);

                // Si es Salida, el monto es positivo, al revertir restamos.
                // Si es Entrada, el monto es 0 (por Calcular...), pero su efecto en saldo fue negativo, al revertir sumamos el valor.
                if (esExistente.TipoMovimiento == "Salida")
                {
                    montoReversion *= -1; // Para restar del saldo lo que se sumó
                }
                else if (esExistente.TipoMovimiento == "Entrada")
                {
                    // Recalcular el valor como si fuera salida para saber cuánto se restó del saldo
                    montoReversion = await CalcularMontoMovimiento(
                       esExistente.IdPublicacion, esExistente.IdDistribuidor, esExistente.Fecha, esExistente.Cantidad, "Salida",
                       esExistente.IdPrecio, esExistente.IdRecargo, esExistente.IdPorcentaje, distribuidor.IdZona);
                    // No se multiplica por -1 aquí, porque el saldo ya lo tiene restado, al eliminar revertimos sumando.
                }


                // 2. Eliminar el registro (esto también guarda en historial)
                var eliminado = await _esRepository.DeleteAsync(idParte, idUsuario, transaction);
                if (!eliminado) throw new DataException("Error al eliminar el movimiento.");

                // 3. Ajustar Saldo
                bool saldoActualizado = await _saldoRepository.ModificarSaldoAsync("Distribuidores", esExistente.IdDistribuidor, publicacion.IdEmpresa, montoReversion, transaction);
                if (!saldoActualizado) throw new DataException("Error al revertir el saldo del distribuidor tras la eliminación.");

                transaction.Commit();
                _logger.LogInformation("Movimiento ID {Id} eliminado y saldo revertido por Usuario ID {UserId}.", idParte, idUsuario);
                return (true, null);
            }
            catch (KeyNotFoundException) { try { transaction.Rollback(); } catch { } return (false, "Movimiento no encontrado."); }
            catch (BloqueoPorPeriodoCerradoException)
            {
                try { transaction.Rollback(); } catch { }
                throw;
            }
            catch (Exception ex)
            {
                try { transaction.Rollback(); } catch { }
                _logger.LogError(ex, "Error EliminarMovimientoAsync ID: {Id}", idParte);
                return (false, $"Error interno: {ex.Message}");
            }
        }

        public async Task<IEnumerable<EntradaSalidaDistHistorialDto>> ObtenerHistorialAsync(
        DateTime? fechaDesde, DateTime? fechaHasta,
        int? idUsuarioModifico, string? tipoModificacion,
        int? idParteAfectada)
        {
            var historialData = await _esRepository.GetHistorialAsync(fechaDesde, fechaHasta, idUsuarioModifico, tipoModificacion, idParteAfectada);

            return historialData.Select(h => new EntradaSalidaDistHistorialDto
            {
                Id_Parte = h.Historial.Id_Parte,
                Id_Publicacion = h.Historial.Id_Publicacion,
                Id_Distribuidor = h.Historial.Id_Distribuidor,
                Fecha = h.Historial.Fecha,
                TipoMovimiento = h.Historial.TipoMovimiento,
                Cantidad = h.Historial.Cantidad,
                Remito = h.Historial.Remito,
                Observacion = h.Historial.Observacion,
                Id_Precio = h.Historial.Id_Precio,
                Id_Recargo = h.Historial.Id_Recargo,
                Id_Porcentaje = h.Historial.Id_Porcentaje,
                Id_Usuario = h.Historial.Id_Usuario,
                NombreUsuarioModifico = h.NombreUsuarioModifico,
                FechaMod = h.Historial.FechaMod,
                TipoMod = h.Historial.TipoMod
            }).ToList();
        }
    }
}