using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using LittleShop.Data; using LittleShop.Models; using LittleShop.DTOs; using LittleShop.Enums; namespace LittleShop.Services; public interface IMessageDeliveryService { Task QueueRecoveryMessageAsync(long telegramUserId, string message); } public interface IBotContactService { Task RecordContactAsync(Guid botId, TelegramUserDto user); Task> GetBotContactsAsync(Guid botId, bool activeOnly = true); Task> GetOrphanedContactsAsync(Guid failedBotId); Task MigrateContactsAsync(Guid fromBotId, Guid toBotId); Task PrepareContactRecoveryAsync(Guid failedBotId); Task NotifyContactOfBotChangeAsync(long telegramUserId, string newBotUsername); Task CreateContactBackupAsync(Guid botId); Task RestoreContactsFromBackupAsync(Guid toBotId, ContactBackupDto backup); } public class BotContactService : IBotContactService { private readonly LittleShopContext _context; private readonly ILogger _logger; private readonly IMessageDeliveryService _messageService; public BotContactService( LittleShopContext context, ILogger logger, IMessageDeliveryService messageService) { _context = context; _logger = logger; _messageService = messageService; } public async Task RecordContactAsync(Guid botId, TelegramUserDto user) { var existingContact = await _context.BotContacts .FirstOrDefaultAsync(c => c.BotId == botId && c.TelegramUserId == user.Id); if (existingContact != null) { // Update existing contact existingContact.TelegramUsername = user.Username ?? string.Empty; existingContact.DisplayName = $"{user.FirstName} {user.LastName}".Trim(); existingContact.FirstName = user.FirstName ?? string.Empty; existingContact.LastName = user.LastName ?? string.Empty; existingContact.LastContactDate = DateTime.UtcNow; existingContact.TotalInteractions++; existingContact.UpdatedAt = DateTime.UtcNow; _context.BotContacts.Update(existingContact); } else { // Create new contact record existingContact = new BotContact { Id = Guid.NewGuid(), BotId = botId, TelegramUserId = user.Id, TelegramUsername = user.Username ?? string.Empty, DisplayName = $"{user.FirstName} {user.LastName}".Trim(), FirstName = user.FirstName ?? string.Empty, LastName = user.LastName ?? string.Empty, FirstContactDate = DateTime.UtcNow, LastContactDate = DateTime.UtcNow, TotalInteractions = 1, LastKnownLanguage = user.LanguageCode ?? "en", Status = ContactStatus.Active, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, IsActive = true }; await _context.BotContacts.AddAsync(existingContact); } await _context.SaveChangesAsync(); _logger.LogInformation("Recorded contact {UserId} for bot {BotId}", user.Id, botId); return existingContact; } public async Task> GetBotContactsAsync(Guid botId, bool activeOnly = true) { var query = _context.BotContacts.Where(c => c.BotId == botId); if (activeOnly) query = query.Where(c => c.IsActive && c.Status == ContactStatus.Active); return await query .OrderByDescending(c => c.LastContactDate) .ToListAsync(); } public async Task> GetOrphanedContactsAsync(Guid failedBotId) { // Get contacts from failed bot that haven't been recovered yet return await _context.BotContacts .Where(c => c.BotId == failedBotId && c.Status == ContactStatus.Active && !c.IsRecovered) .OrderByDescending(c => c.TotalInteractions) // Prioritize most active users .ToListAsync(); } public async Task MigrateContactsAsync(Guid fromBotId, Guid toBotId) { try { var contactsToMigrate = await GetOrphanedContactsAsync(fromBotId); var toBot = await _context.Bots.FindAsync(toBotId); if (toBot == null) { _logger.LogError("Target bot {BotId} not found", toBotId); return false; } foreach (var contact in contactsToMigrate) { // Create new contact record for new bot var newContact = new BotContact { Id = Guid.NewGuid(), BotId = toBotId, TelegramUserId = contact.TelegramUserId, TelegramUsername = contact.TelegramUsername, DisplayName = contact.DisplayName, FirstName = contact.FirstName, LastName = contact.LastName, FirstContactDate = DateTime.UtcNow, LastContactDate = DateTime.UtcNow, TotalInteractions = 0, // Reset for new bot LastKnownLanguage = contact.LastKnownLanguage, Status = ContactStatus.Recovered, CustomerId = contact.CustomerId, IsRecovered = true, RecoveredFromBotId = fromBotId, RecoveredAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, IsActive = true, Preferences = contact.Preferences, Notes = $"Migrated from bot {fromBotId} on {DateTime.UtcNow:yyyy-MM-dd}" }; await _context.BotContacts.AddAsync(newContact); // Mark original contact as migrated contact.Status = ContactStatus.Migrated; contact.IsRecovered = true; contact.UpdatedAt = DateTime.UtcNow; _context.BotContacts.Update(contact); _logger.LogInformation("Migrated contact {UserId} from bot {FromBot} to {ToBot}", contact.TelegramUserId, fromBotId, toBotId); } await _context.SaveChangesAsync(); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to migrate contacts from {FromBot} to {ToBot}", fromBotId, toBotId); return false; } } public async Task PrepareContactRecoveryAsync(Guid failedBotId) { var orphanedContacts = await GetOrphanedContactsAsync(failedBotId); var failedBot = await _context.Bots.FindAsync(failedBotId); var availableBots = await _context.Bots .Where(b => b.Id != failedBotId && b.Status == BotStatus.Active) .ToListAsync(); return new BotContactRecoveryDto { FailedBotId = failedBotId, FailedBotName = failedBot?.Name ?? "Unknown", OrphanedContactCount = orphanedContacts.Count(), HighValueContacts = orphanedContacts .Where(c => c.TotalInteractions > 10 || c.CustomerId != null) .Count(), AvailableRecoveryBots = availableBots.Select(b => new BotInfoDto { Id = b.Id, Name = b.Name, Status = b.Status.ToString(), CurrentContactCount = _context.BotContacts.Count(c => c.BotId == b.Id && c.IsActive) }).ToList(), ContactDetails = orphanedContacts.Select(c => new ContactSummaryDto { TelegramUserId = c.TelegramUserId, Username = c.TelegramUsername, DisplayName = c.DisplayName, TotalInteractions = c.TotalInteractions, LastContactDate = c.LastContactDate, IsCustomer = c.CustomerId != null }).ToList() }; } public async Task NotifyContactOfBotChangeAsync(long telegramUserId, string newBotUsername) { try { // This would integrate with the message delivery service var message = $"Hello! Your previous bot is temporarily unavailable. " + $"Please continue your conversation with our new bot: @{newBotUsername} " + $"All your order history and preferences have been preserved."; // Queue message for delivery when user contacts new bot await _messageService.QueueRecoveryMessageAsync(telegramUserId, message); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to notify user {UserId} of bot change", telegramUserId); return false; } } public async Task CreateContactBackupAsync(Guid botId) { var contacts = await GetBotContactsAsync(botId, activeOnly: false); var bot = await _context.Bots.FindAsync(botId); return new ContactBackupDto { BackupId = Guid.NewGuid(), BotId = botId, BotName = bot?.Name ?? "Unknown", BackupDate = DateTime.UtcNow, ContactCount = contacts.Count(), Contacts = contacts.Select(c => new ContactExportDto { TelegramUserId = c.TelegramUserId, TelegramUsername = c.TelegramUsername, DisplayName = c.DisplayName, FirstName = c.FirstName, LastName = c.LastName, FirstContactDate = c.FirstContactDate, LastContactDate = c.LastContactDate, TotalInteractions = c.TotalInteractions, LastKnownLanguage = c.LastKnownLanguage, Status = c.Status.ToString(), CustomerId = c.CustomerId, Preferences = c.Preferences, Notes = c.Notes }).ToList() }; } public async Task RestoreContactsFromBackupAsync(Guid toBotId, ContactBackupDto backup) { try { foreach (var contactData in backup.Contacts) { // Check if contact already exists for this bot var existingContact = await _context.BotContacts .FirstOrDefaultAsync(c => c.BotId == toBotId && c.TelegramUserId == contactData.TelegramUserId); if (existingContact == null) { var newContact = new BotContact { Id = Guid.NewGuid(), BotId = toBotId, TelegramUserId = contactData.TelegramUserId, TelegramUsername = contactData.TelegramUsername, DisplayName = contactData.DisplayName, FirstName = contactData.FirstName, LastName = contactData.LastName, FirstContactDate = contactData.FirstContactDate, LastContactDate = contactData.LastContactDate, TotalInteractions = contactData.TotalInteractions, LastKnownLanguage = contactData.LastKnownLanguage, Status = Enum.Parse(contactData.Status), CustomerId = contactData.CustomerId, IsRecovered = true, RecoveredFromBotId = backup.BotId, RecoveredAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow, IsActive = true, Preferences = contactData.Preferences, Notes = $"Restored from backup {backup.BackupId} on {DateTime.UtcNow:yyyy-MM-dd}" }; await _context.BotContacts.AddAsync(newContact); } } await _context.SaveChangesAsync(); _logger.LogInformation("Restored {Count} contacts from backup to bot {BotId}", backup.Contacts.Count, toBotId); return true; } catch (Exception ex) { _logger.LogError(ex, "Failed to restore contacts from backup"); return false; } } } // DTOs for contact management public class TelegramUserDto { public long Id { get; set; } public string? Username { get; set; } public string? FirstName { get; set; } public string? LastName { get; set; } public string? LanguageCode { get; set; } } public class BotContactRecoveryDto { public Guid FailedBotId { get; set; } public string FailedBotName { get; set; } = string.Empty; public int OrphanedContactCount { get; set; } public int HighValueContacts { get; set; } public List AvailableRecoveryBots { get; set; } = new(); public List ContactDetails { get; set; } = new(); } public class BotInfoDto { public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public string Status { get; set; } = string.Empty; public int CurrentContactCount { get; set; } } public class ContactSummaryDto { public long TelegramUserId { get; set; } public string Username { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; public int TotalInteractions { get; set; } public DateTime LastContactDate { get; set; } public bool IsCustomer { get; set; } } public class ContactBackupDto { public Guid BackupId { get; set; } public Guid BotId { get; set; } public string BotName { get; set; } = string.Empty; public DateTime BackupDate { get; set; } public int ContactCount { get; set; } public List Contacts { get; set; } = new(); } public class ContactExportDto { public long TelegramUserId { get; set; } public string TelegramUsername { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; public DateTime FirstContactDate { get; set; } public DateTime LastContactDate { get; set; } public int TotalInteractions { get; set; } public string LastKnownLanguage { get; set; } = string.Empty; public string Status { get; set; } = string.Empty; public Guid? CustomerId { get; set; } public string? Preferences { get; set; } public string? Notes { get; set; } }