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.Contables;
using GestionIntegral.Api.Models.Contables;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;

namespace GestionIntegral.Api.Services.Contables
{
    public class NotaCreditoDebitoService : INotaCreditoDebitoService
    {
        private readonly INotaCreditoDebitoRepository _notaRepo;
        private readonly IDistribuidorRepository _distribuidorRepo;
        private readonly ICanillaRepository _canillaRepo;
        private readonly IEmpresaRepository _empresaRepo;
        private readonly ISaldoRepository _saldoRepo;
        private readonly IPeriodoCerradoValidator _periodoCerrado;
        private readonly DbConnectionFactory _connectionFactory;
        private readonly ILogger<NotaCreditoDebitoService> _logger;

        public NotaCreditoDebitoService(
            INotaCreditoDebitoRepository notaRepo,
            IDistribuidorRepository distribuidorRepo,
            ICanillaRepository canillaRepo,
            IEmpresaRepository empresaRepo,
            ISaldoRepository saldoRepo,
            IPeriodoCerradoValidator periodoCerrado,
            DbConnectionFactory connectionFactory,
            ILogger<NotaCreditoDebitoService> logger)
        {
            _notaRepo = notaRepo;
            _distribuidorRepo = distribuidorRepo;
            _canillaRepo = canillaRepo;
            _empresaRepo = empresaRepo;
            _saldoRepo = saldoRepo;
            _periodoCerrado = periodoCerrado;
            _connectionFactory = connectionFactory;
            _logger = logger;
        }

        private async Task<NotaCreditoDebitoDto> MapToDto(NotaCreditoDebito nota)
        {
            if (nota == null) return null!;

            string nombreDestinatario = "N/A";
            if (nota.Destino == "Distribuidores")
            {
                var distData = await _distribuidorRepo.GetByIdAsync(nota.IdDestino);
                nombreDestinatario = distData.Distribuidor?.Nombre ?? "Distribuidor Desconocido";
            }
            else if (nota.Destino == "Canillas")
            {
                var canData = await _canillaRepo.GetByIdAsync(nota.IdDestino);
                nombreDestinatario = canData.Canilla?.NomApe ?? "Canillita Desconocido";
            }

            var empresa = await _empresaRepo.GetByIdAsync(nota.IdEmpresa);

            return new NotaCreditoDebitoDto
            {
                IdNota = nota.IdNota,
                Destino = nota.Destino,
                IdDestino = nota.IdDestino,
                NombreDestinatario = nombreDestinatario,
                Referencia = nota.Referencia,
                Tipo = nota.Tipo,
                Fecha = nota.Fecha.ToString("yyyy-MM-dd"),
                Monto = nota.Monto,
                Observaciones = nota.Observaciones,
                IdEmpresa = nota.IdEmpresa,
                NombreEmpresa = empresa?.Nombre ?? "Empresa Desconocida"
            };
        }

        public async Task<IEnumerable<NotaCreditoDebitoDto>> ObtenerTodosAsync(
            DateTime? fechaDesde, DateTime? fechaHasta,
            string? destino, int? idDestino, int? idEmpresa, string? tipoNota)
        {
            var notas = await _notaRepo.GetAllAsync(fechaDesde, fechaHasta, destino, idDestino, idEmpresa, tipoNota);
            var dtos = new List<NotaCreditoDebitoDto>();
            foreach (var nota in notas)
            {
                dtos.Add(await MapToDto(nota));
            }
            return dtos;
        }

        public async Task<NotaCreditoDebitoDto?> ObtenerPorIdAsync(int idNota)
        {
            var nota = await _notaRepo.GetByIdAsync(idNota);
            return nota == null ? null : await MapToDto(nota);
        }

        public async Task<(NotaCreditoDebitoDto? Nota, string? Error)> CrearAsync(CreateNotaDto createDto, int idUsuario)
        {
            if (createDto.Destino == "Distribuidores")
            {
                if (await _distribuidorRepo.GetByIdSimpleAsync(createDto.IdDestino) == null)
                    return (null, "El distribuidor especificado no existe.");
            }
            else if (createDto.Destino == "Canillas")
            {
                if (await _canillaRepo.GetByIdSimpleAsync(createDto.IdDestino) == null)
                    return (null, "El canillita especificado no existe.");
            }
            else { return (null, "Tipo de destino inválido."); }

            if (await _empresaRepo.GetByIdAsync(createDto.IdEmpresa) == null)
                return (null, "La empresa especificada no existe.");

            // Bloqueo por período cerrado (sólo aplica si Destino="Distribuidores"; el validator filtra internamente).
            var bloqueoCrear = await _periodoCerrado.EstaCerradoAsync(createDto.Destino, createDto.IdDestino, createDto.IdEmpresa, createDto.Fecha);
            if (bloqueoCrear.EstaCerrado)
                throw new BloqueoPorPeriodoCerradoException(bloqueoCrear.IdCierre!.Value, bloqueoCrear.FechaCorte!.Value);

            var nuevaNota = new NotaCreditoDebito
            {
                Destino = createDto.Destino,
                IdDestino = createDto.IdDestino,
                Referencia = createDto.Referencia,
                Tipo = createDto.Tipo,
                Fecha = createDto.Fecha.Date,
                Monto = createDto.Monto,
                Observaciones = createDto.Observaciones,
                IdEmpresa = createDto.IdEmpresa
            };

            using var connection = _connectionFactory.CreateConnection();
            IDbTransaction? transaction = null;
            try
            {
                if (connection.State != ConnectionState.Open)
                {
                    if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
                }
                transaction = connection.BeginTransaction();

                var notaCreada = await _notaRepo.CreateAsync(nuevaNota, idUsuario, transaction);
                if (notaCreada == null) throw new DataException("Error al registrar la nota.");

                decimal montoParaSaldo;
                if (createDto.Tipo == "Credito")
                {
                    montoParaSaldo = -createDto.Monto;
                }
                else
                {
                    montoParaSaldo = createDto.Monto;
                }

                bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaCreada.Destino, notaCreada.IdDestino, notaCreada.IdEmpresa, montoParaSaldo, transaction);
                if (!saldoActualizado) throw new DataException($"Error al actualizar el saldo para {notaCreada.Destino} ID {notaCreada.IdDestino}.");

                transaction.Commit();
                _logger.LogInformation("NotaC/D ID {Id} creada y saldo afectado por Usuario ID {UserId}.", notaCreada.IdNota, idUsuario);
                return (await MapToDto(notaCreada), null);
            }
            catch (Exception ex)
            {
                try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de CrearAsync NotaCreditoDebito."); }
                _logger.LogError(ex, "Error CrearAsync NotaCreditoDebito.");
                return (null, $"Error interno: {ex.Message}");
            }
            finally
            {
                if (connection.State == ConnectionState.Open)
                {
                    if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
                }
            }
        }

        public async Task<(bool Exito, string? Error)> ActualizarAsync(int idNota, UpdateNotaDto updateDto, int idUsuario)
        {
            using var connection = _connectionFactory.CreateConnection();
            IDbTransaction? transaction = null;
            try
            {
                if (connection.State != ConnectionState.Open)
                {
                    if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
                }
                transaction = connection.BeginTransaction();

                var notaExistente = await _notaRepo.GetByIdAsync(idNota);
                if (notaExistente == null)
                {
                    transaction.Rollback();
                    return (false, "Nota no encontrada.");
                }

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

                decimal impactoOriginalSaldo = notaExistente.Tipo == "Credito" ? -notaExistente.Monto : notaExistente.Monto;
                decimal impactoNuevoSaldo = notaExistente.Tipo == "Credito" ? -updateDto.Monto : updateDto.Monto;
                decimal diferenciaAjusteSaldo = impactoNuevoSaldo - impactoOriginalSaldo;

                var notaParaActualizarEnRepo = new NotaCreditoDebito
                {
                    IdNota = notaExistente.IdNota,
                    Destino = notaExistente.Destino,
                    IdDestino = notaExistente.IdDestino,
                    Referencia = notaExistente.Referencia,
                    Tipo = notaExistente.Tipo,
                    Fecha = notaExistente.Fecha,
                    Monto = updateDto.Monto,
                    Observaciones = updateDto.Observaciones,
                    IdEmpresa = notaExistente.IdEmpresa
                };

                var actualizado = await _notaRepo.UpdateAsync(notaParaActualizarEnRepo, idUsuario, transaction);
                if (!actualizado) throw new DataException("Error al actualizar la nota en la base de datos.");

                if (diferenciaAjusteSaldo != 0)
                {
                    bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, diferenciaAjusteSaldo, transaction);
                    if (!saldoActualizado) throw new DataException("Error al ajustar el saldo tras la actualización de la nota.");
                }

                transaction.Commit();
                _logger.LogInformation("NotaC/D ID {Id} actualizada por Usuario ID {UserId}.", idNota, idUsuario);
                return (true, null);
            }
            catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync NotaCreditoDebito (KeyNotFound)."); } return (false, "Nota no encontrada."); }
            catch (Exception ex)
            {
                try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de ActualizarAsync NotaCreditoDebito."); }
                _logger.LogError(ex, "Error ActualizarAsync Nota C/D ID: {Id}", idNota);
                return (false, $"Error interno: {ex.Message}");
            }
            finally
            {
                if (connection.State == ConnectionState.Open)
                {
                    if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
                }
            }
        }

        public async Task<(bool Exito, string? Error)> EliminarAsync(int idNota, int idUsuario)
        {
            using var connection = _connectionFactory.CreateConnection();
            IDbTransaction? transaction = null;
            try
            {
                if (connection.State != ConnectionState.Open)
                {
                    if (connection is System.Data.Common.DbConnection dbConn) await dbConn.OpenAsync(); else connection.Open();
                }
                transaction = connection.BeginTransaction();

                var notaExistente = await _notaRepo.GetByIdAsync(idNota);
                if (notaExistente == null)
                {
                    transaction.Rollback();
                    return (false, "Nota no encontrada.");
                }

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

                decimal montoReversion = notaExistente.Tipo == "Credito" ? notaExistente.Monto : -notaExistente.Monto;

                var eliminado = await _notaRepo.DeleteAsync(idNota, idUsuario, transaction);
                if (!eliminado) throw new DataException("Error al eliminar la nota de la base de datos.");

                bool saldoActualizado = await _saldoRepo.ModificarSaldoAsync(notaExistente.Destino, notaExistente.IdDestino, notaExistente.IdEmpresa, montoReversion, transaction);
                if (!saldoActualizado) throw new DataException("Error al revertir el saldo tras la eliminación de la nota.");

                transaction.Commit();
                _logger.LogInformation("NotaC/D ID {Id} eliminada y saldo revertido por Usuario ID {UserId}.", idNota, idUsuario);
                return (true, null);
            }
            catch (KeyNotFoundException) { try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync NotaCreditoDebito (KeyNotFound)."); } return (false, "Nota no encontrada."); }
            catch (Exception ex)
            {
                try { transaction?.Rollback(); } catch (Exception rbEx) { _logger.LogError(rbEx, "Error en Rollback de EliminarAsync NotaCreditoDebito."); }
                _logger.LogError(ex, "Error EliminarAsync NotaC/D ID: {Id}", idNota);
                return (false, $"Error interno: {ex.Message}");
            }
            finally
            {
                if (connection.State == ConnectionState.Open)
                {
                    if (connection is System.Data.Common.DbConnection dbConn) await dbConn.CloseAsync(); else connection.Close();
                }
            }
        }

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

            return historialData.Select(h => new NotaCreditoDebitoHistorialDto
            {
                Id_Nota = h.Historial.Id_Nota,
                Destino = h.Historial.Destino,
                Id_Destino = h.Historial.Id_Destino,
                Referencia = h.Historial.Referencia,
                Tipo = h.Historial.Tipo,
                Fecha = h.Historial.Fecha, // Fecha original de la nota
                Monto = h.Historial.Monto,
                Observaciones = h.Historial.Observaciones,
                Id_Empresa = h.Historial.Id_Empresa,
                Id_Usuario = h.Historial.Id_Usuario,
                NombreUsuarioModifico = h.NombreUsuarioModifico,
                FechaMod = h.Historial.FechaMod, // Fecha de la auditoría
                TipoMod = h.Historial.TipoMod
            }).ToList();
        }
    }
}