< Summary

Information
Class: OzonOrdersWeb.Areas.Studio2.Controllers.SendMessageRequest
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OzonChatController.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 3
Coverable lines: 3
Total lines: 1996
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_ClientId()100%210%
get_ChatId()100%210%
get_Text()100%210%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Areas/Studio2/Controllers/OzonChatController.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authorization;
 2using Microsoft.AspNetCore.Mvc;
 3using Microsoft.Extensions.Caching.Memory;
 4using Microsoft.Extensions.Http;
 5using Microsoft.Extensions.Logging;
 6using OzonOrdersWeb.Observability;
 7using System.Text.RegularExpressions;
 8using AppRepository.ApiServices.OzonApi;
 9using AppRepository.ApiServices.OzonApi.Filters;
 10using OzonDomains;
 11using OzonDomains.Models;
 12using OzonOrdersWeb.Areas.Studio2.ViewModels.OzonChatViewModels;
 13using Services.DataServices;
 14using OzonRepositories.Data;
 15
 16namespace OzonOrdersWeb.Areas.Studio2.Controllers
 17{
 18    [Authorize(Roles = "User,Admin,SuperAdmin")]
 19    [Area("Studio2")]
 20    public class OzonChatController : Controller
 21    {
 22        private readonly OzonClientServices _ozonClientServices;
 23        private readonly IHttpClientFactory _httpClientFactory;
 24        private readonly IServiceScopeFactory _serviceScopeFactory;
 25        private readonly OrderRepository _orderRepository;
 26        private readonly OzonChatOrderDataServices _chatOrderService;
 27        private readonly OzonChatResponseStatisticDataServices _chatStatisticService;
 28        private readonly OzonChatResponseTimeDataServices _chatTimeService;
 29        private readonly IMemoryCache _memoryCache;
 30        private readonly ILogger<OzonChatController>? _logger;
 31
 32        // Ключ и TTL кэша суммарного счётчика непрочитанных: бейдж в шапке опрашивается
 33        // каждые несколько секунд с каждой вкладки, а под капотом — живые вызовы Ozon Chat API
 34        // по всем клиентам. Кэш ограничивает частоту обращений к Ozon одним разом в минуту.
 35        private const string TotalUnreadCacheKey = "OzonChat:TotalUnreadCount";
 36        private static readonly TimeSpan TotalUnreadCacheTtl = TimeSpan.FromSeconds(60);
 37
 38        // Часовой пояс Москвы (UTC+3)
 39        private static readonly TimeZoneInfo MoscowTimeZone = TimeZoneInfo.FindSystemTimeZoneById(
 40            OperatingSystem.IsWindows() ? "Russian Standard Time" : "Europe/Moscow");
 41
 42        // Регулярные выражения для извлечения артикула и названия товара из текста сообщения
 43        private static readonly Regex ArticleRegex = new Regex(
 44            @"артикул[\u044c]?\s+([\w\d][\w\d\-\.=]+)",
 45            RegexOptions.IgnoreCase | RegexOptions.Compiled);
 46
 47        private static readonly Regex ProductNameRegex = new Regex(
 48            @"""([^""]{5,120})""",
 49            RegexOptions.Compiled);
 50
 51        // Примеры формата номера отправления: 0159318549-0007, 0127136132-0231-1
 52        private static readonly Regex PostingNumberRegex = new Regex(
 53            @"^\d{4,12}-\d{1,6}(-\d{1,6})?$",
 54            RegexOptions.Compiled);
 55
 56        public OzonChatController(
 57            OzonClientServices ozonClientServices,
 58            IHttpClientFactory httpClientFactory,
 59            IServiceScopeFactory serviceScopeFactory,
 60            OrderRepository orderRepository,
 61            OzonChatOrderDataServices chatOrderService,
 62            OzonChatResponseStatisticDataServices chatStatisticService,
 63            OzonChatResponseTimeDataServices chatTimeService,
 64            IMemoryCache memoryCache,
 65            ILogger<OzonChatController>? logger = null)
 66        {
 67            _ozonClientServices = ozonClientServices;
 68            _httpClientFactory = httpClientFactory;
 69            _serviceScopeFactory = serviceScopeFactory;
 70            _orderRepository = orderRepository;
 71            _chatOrderService = chatOrderService;
 72            _chatStatisticService = chatStatisticService;
 73            _chatTimeService = chatTimeService;
 74            _memoryCache = memoryCache;
 75            _logger = logger;
 76        }
 77
 78        /// <summary>
 79        /// Единственная страница — выбор клиента + чаты с пагинацией через AJAX
 80        /// </summary>
 81        public async Task<IActionResult> Index()
 82        {
 83            try
 84            {
 85                var ozonClients = await _ozonClientServices.GetOzonClients();
 86                var viewModel = new OzonChatIndexViewModel
 87                {
 88                    OzonClients = ozonClients
 89                        .Where(c => !string.IsNullOrEmpty(c.ClientId) && !string.IsNullOrEmpty(c.ApiKey) &&
 90                                    c.ClientType == ClientType.OZON).ToList()
 91                };
 92
 93                return View(viewModel);
 94            }
 95            catch (Exception ex)
 96            {
 97                TempData["ErrorMessage"] = $"Ошибка при загрузке клиентов: {ex.Message}";
 98                return View(new OzonChatIndexViewModel());
 99            }
 100        }
 101
 102        /// <summary>
 103        /// AJAX: Получение списка чатов с пагинацией (JSON)
 104        /// Если clientId == 0, загружает чаты по ВСЕМ клиентам
 105        /// </summary>
 106        [HttpGet]
 107        public async Task<IActionResult> GetChatsJson(int clientId = 0, string status = "All", string chatType = "",
 108            bool unreadOnly = false, int page = 1, int pageSize = 30, string sortField = "createdAt", string sortOrder =
 109        {
 110            try
 111            {
 112                if (page < 1) page = 1;
 113                pageSize = Math.Clamp(pageSize, 1, 100);
 114
 115                // Режим «все клиенты»
 116                if (clientId == 0)
 117                {
 118                    return await GetAllClientsChatsJson(status, chatType, unreadOnly, page, pageSize, sortField, sortOrd
 119                }
 120
 121                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 122                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 123                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 124                {
 125                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 126                }
 127
 128                long offset = (long)(page - 1) * pageSize;
 129
 130                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 131                    ozonClient.DecryptApiKey);
 132
 133                var request = new ChatListRequest
 134                {
 135                    Filter = new ChatFilter
 136                    {
 137                        ChatStatus = status,
 138                        UnreadOnly = unreadOnly
 139                    },
 140                    Limit = pageSize,
 141                    Offset = offset
 142                };
 143
 144                var response = await chatManager.GetChatListAsync(request);
 145
 146                var chats = response?.Chats ?? new List<ChatItem>();
 147
 148                // Фильтрация по типу чата на стороне клиента (API не поддерживает этот фильтр)
 149                if (!string.IsNullOrEmpty(chatType))
 150                {
 151                    chats = chats.Where(c =>
 152                        string.Equals(c.Chat.ChatType, chatType, StringComparison.OrdinalIgnoreCase)).ToList();
 153                }
 154
 155                // Сортировка
 156                if (sortField == "lastMessage")
 157                {
 158                    chats = chats.OrderByDescending(c => c.LastMessageId).ToList();
 159                    if (sortOrder == "asc") chats.Reverse();
 160                }
 161                else // createdAt
 162                {
 163                    chats = chats.OrderByDescending(c => c.Chat.CreatedAt).ToList();
 164                    if (sortOrder == "asc") chats.Reverse();
 165                }
 166
 167                var totalChats = response?.TotalChatsCount ?? 0;
 168                var totalPages = totalChats > 0 ? (int)Math.Ceiling((double)totalChats / pageSize) : 1;
 169
 170                // Обновляем метаданные чатов в фоне (не блокируем ответ)
 171                _ = Task.Run(async () =>
 172                {
 173                    using var scope = _serviceScopeFactory.CreateScope();
 174                    var chatOrderService = scope.ServiceProvider.GetRequiredService<OzonChatOrderDataServices>();
 175                    var orderRepository = scope.ServiceProvider.GetRequiredService<OrderRepository>();
 176                    var ozonClientService = scope.ServiceProvider.GetRequiredService<OzonClientServices>();
 177
 178                    try
 179                    {
 180                        await UpdateChatMetadataAsync(clientId, chats, chatOrderService, orderRepository,
 181                            ozonClientService);
 182                    }
 183                    catch (Exception ex)
 184                    {
 185                        Console.WriteLine($"Ошибка обновления метаданных чатов: {ex.Message}");
 186                    }
 187                });
 188
 189                return Json(new
 190                {
 191                    success = true,
 192                    clientName = ozonClient.Name,
 193                    chats = chats.Select(c => new
 194                    {
 195                        chatId = c.Chat.ChatId,
 196                        chatStatus = c.Chat.ChatStatus,
 197                        chatType = c.Chat.ChatType,
 198                        createdAt = ToMoscowTime(c.Chat.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 199                        createdAtFormatted = ToMoscowTime(c.Chat.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 200                        unreadCount = c.UnreadCount,
 201                        lastMessageId = c.LastMessageId,
 202                        clientId = clientId,
 203                        clientName = ozonClient.Name
 204                    }),
 205                    totalChatsCount = totalChats,
 206                    totalUnreadCount = response?.TotalUnreadCount ?? 0,
 207                    currentPage = page,
 208                    pageSize,
 209                    totalPages,
 210                    hasNext = response?.HasNext ?? false
 211                });
 212            }
 213            catch (Exception ex)
 214            {
 215                return Json(new { success = false, message = $"Ошибка при загрузке чатов: {ex.Message}" });
 216            }
 217        }
 218
 219        /// <summary>
 220        /// Загрузка чатов по всем клиентам, объединение и сортировка по дате
 221        /// </summary>
 222        private async Task<IActionResult> GetAllClientsChatsJson(string status, string chatType,
 223            bool unreadOnly, int page, int pageSize, string sortField = "createdAt", string sortOrder = "desc")
 224        {
 225            try
 226            {
 227                var ozonClients = await _ozonClientServices.GetOzonClients();
 228                var validClients = ozonClients
 229                    .Where(c => !string.IsNullOrEmpty(c.DecryptClientId) && !string.IsNullOrEmpty(c.DecryptApiKey) &&
 230                                c.ClientType == ClientType.OZON).ToList();
 231
 232                if (!validClients.Any())
 233                    return Json(new { success = false, message = "Нет настроенных клиентов Ozon" });
 234
 235                // Параллельно загружаем чаты от всех клиентов
 236                var tasks = validClients.Select(async client =>
 237                {
 238                    try
 239                    {
 240                        var chatManager = new OzonChatManager(_httpClientFactory, client.DecryptClientId!,
 241                            client.DecryptApiKey!);
 242
 243                        var request = new ChatListRequest
 244                        {
 245                            Filter = new ChatFilter
 246                            {
 247                                ChatStatus = status,
 248                                UnreadOnly = unreadOnly
 249                            },
 250                            Limit = 100 // берём максимум от каждого клиента
 251                        };
 252
 253                        var response = await chatManager.GetChatListAsync(request);
 254                        var chats = response?.Chats ?? new List<ChatItem>();
 255
 256                        return (Client: client, Chats: (IList<ChatItem>)chats,
 257                            TotalChats: response?.TotalChatsCount ?? 0L,
 258                            TotalUnread: response?.TotalUnreadCount ?? 0L);
 259                    }
 260                    catch
 261                    {
 262                        return (Client: client, Chats: (IList<ChatItem>)new List<ChatItem>(),
 263                            TotalChats: 0L, TotalUnread: 0L);
 264                    }
 265                });
 266
 267                var results = await Task.WhenAll(tasks);
 268
 269                // Объединяем все чаты с информацией о клиенте
 270                var allChats = results.SelectMany(r => r.Chats.Select(c => new
 271                {
 272                    Chat = c,
 273                    ClientId = r.Client.Id,
 274                    ClientName = r.Client.Name ?? $"Клиент {r.Client.Id}"
 275                })).ToList();
 276
 277                // Фильтрация по типу чата
 278                if (!string.IsNullOrEmpty(chatType))
 279                {
 280                    allChats = allChats.Where(c =>
 281                        string.Equals(c.Chat.Chat.ChatType, chatType, StringComparison.OrdinalIgnoreCase)).ToList();
 282                }
 283
 284                // Сортировка
 285                if (sortField == "lastMessage")
 286                {
 287                    allChats = allChats.OrderByDescending(c => c.Chat.LastMessageId).ToList();
 288                    if (sortOrder == "asc") allChats.Reverse();
 289                }
 290                else // createdAt
 291                {
 292                    allChats = allChats.OrderByDescending(c => c.Chat.Chat.CreatedAt).ToList();
 293                    if (sortOrder == "asc") allChats.Reverse();
 294                }
 295
 296                var totalAllChats = allChats.Count;
 297                var totalAllUnread = results.Sum(r => r.TotalUnread);
 298                var totalPages = totalAllChats > 0 ? (int)Math.Ceiling((double)totalAllChats / pageSize) : 1;
 299
 300                // Пагинация на объединённом списке
 301                var pagedChats = allChats.Skip((page - 1) * pageSize).Take(pageSize).ToList();
 302
 303                // Обновляем метаданные чатов в фоне (не блокируем ответ)
 304                _ = Task.Run(async () =>
 305                {
 306                    using var scope = _serviceScopeFactory.CreateScope();
 307                    var chatOrderService = scope.ServiceProvider.GetRequiredService<OzonChatOrderDataServices>();
 308                    var orderRepository = scope.ServiceProvider.GetRequiredService<OrderRepository>();
 309                    var ozonClientService = scope.ServiceProvider.GetRequiredService<OzonClientServices>();
 310
 311                    try
 312                    {
 313                        foreach (var result in results)
 314                        {
 315                            await UpdateChatMetadataAsync(result.Client.Id, result.Chats.ToList(), chatOrderService,
 316                                orderRepository, ozonClientService);
 317                        }
 318                    }
 319                    catch (Exception ex)
 320                    {
 321                        Console.WriteLine($"Ошибка обновления метаданных чатов (все клиенты): {ex.Message}");
 322                    }
 323                });
 324
 325                return Json(new
 326                {
 327                    success = true,
 328                    clientName = "Все клиенты",
 329                    chats = pagedChats.Select(c => new
 330                    {
 331                        chatId = c.Chat.Chat.ChatId,
 332                        chatStatus = c.Chat.Chat.ChatStatus,
 333                        chatType = c.Chat.Chat.ChatType,
 334                        createdAt = ToMoscowTime(c.Chat.Chat.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 335                        createdAtFormatted = ToMoscowTime(c.Chat.Chat.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 336                        unreadCount = c.Chat.UnreadCount,
 337                        lastMessageId = c.Chat.LastMessageId,
 338                        clientId = c.ClientId,
 339                        clientName = c.ClientName
 340                    }),
 341                    totalChatsCount = (long)totalAllChats,
 342                    totalUnreadCount = totalAllUnread,
 343                    currentPage = page,
 344                    pageSize,
 345                    totalPages,
 346                    hasNext = page < totalPages
 347                });
 348            }
 349            catch (Exception ex)
 350            {
 351                return Json(new { success = false, message = $"Ошибка при загрузке чатов: {ex.Message}" });
 352            }
 353        }
 354
 355        /// <summary>
 356        /// API для получения обновлённого количества непрочитанных сообщений
 357        /// </summary>
 358        [HttpGet]
 359        public async Task<IActionResult> GetUnreadCount(int clientId)
 360        {
 361            try
 362            {
 363                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 364                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 365                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 366                {
 367                    return Json(new { success = false, message = "Клиент не найден или некорректные данные API" });
 368                }
 369
 370                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 371                    ozonClient.DecryptApiKey);
 372                var response =
 373                    await chatManager.GetUnreadOpenChatsAsync(1); // Запрашиваем минимум для получения общего счётчика
 374
 375                return Json(new
 376                {
 377                    success = true,
 378                    unreadCount = response?.TotalUnreadCount ?? 0
 379                });
 380            }
 381            catch (Exception ex)
 382            {
 383                return Json(new
 384                {
 385                    success = false,
 386                    message = ex.Message
 387                });
 388            }
 389        }
 390
 391        /// <summary>
 392        /// API для получения общего количества непрочитанных сообщений по всем клиентам
 393        /// Оптимизированная версия для периодического опроса
 394        /// </summary>
 395        [HttpGet]
 396        public async Task<IActionResult> GetTotalUnreadCount()
 397        {
 398            try
 399            {
 400                var totalUnread = await _memoryCache.GetOrCreateAsync(TotalUnreadCacheKey, async entry =>
 401                {
 402                    entry.AbsoluteExpirationRelativeToNow = TotalUnreadCacheTtl;
 403                    return await FetchTotalUnreadFromOzonAsync();
 404                });
 405
 406                return Json(new { success = true, totalUnread });
 407            }
 408            catch (Exception ex)
 409            {
 410                HttpContext.Items[BusinessFailureLoggingFilter.LoggedItemKey] = true;
 411                _logger?.LogInformation(ex, "GetTotalUnreadCount: не удалось получить количество непрочитанных сообщений
 412                return Json(new { success = false, message = ex.Message, totalUnread = 0 });
 413            }
 414        }
 415
 416        private async Task<long> FetchTotalUnreadFromOzonAsync()
 417        {
 418            var ozonClients = await _ozonClientServices.GetOzonClients();
 419            var validClients = ozonClients
 420                .Where(c => !string.IsNullOrEmpty(c.DecryptClientId) && !string.IsNullOrEmpty(c.DecryptApiKey) &&
 421                            c.ClientType == ClientType.OZON).ToList();
 422
 423            if (validClients.Count == 0)
 424                return 0L;
 425
 426            // Параллельно получаем количество непрочитанных от всех клиентов
 427            var tasks = validClients.Select(async client =>
 428            {
 429                try
 430                {
 431                    var chatManager = new OzonChatManager(_httpClientFactory, client.DecryptClientId!,
 432                        client.DecryptApiKey!);
 433                    var response = await chatManager.GetUnreadOpenChatsAsync(1);
 434                    return response?.TotalUnreadCount ?? 0L;
 435                }
 436                catch
 437                {
 438                    return 0L;
 439                }
 440            });
 441
 442            var results = await Task.WhenAll(tasks);
 443            return results.Sum();
 444        }
 445
 446        /// <summary>
 447        /// AJAX: Получение последнего сообщения чата
 448        /// </summary>
 449        [HttpGet]
 450        public async Task<IActionResult> GetLastMessageJson(int clientId, string chatId)
 451        {
 452            try
 453            {
 454                if (string.IsNullOrEmpty(chatId))
 455                    return Json(new { success = false, message = "Не указан ID чата" });
 456
 457                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 458                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 459                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 460                {
 461                    return Json(new { success = false, message = "Клиент Ozon не найден" });
 462                }
 463
 464                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 465                    ozonClient.DecryptApiKey);
 466
 467                var response = await chatManager.GetChatHistoryAsync(chatId, 1, null, "Backward");
 468
 469                if (response == null || response.Messages == null || !response.Messages.Any())
 470                    return Json(new { success = true, hasMessage = false });
 471
 472                var lastMessage = response.Messages.First();
 473                var senderType = lastMessage.User?.Type ?? "unknown";
 474                var senderLabel = senderType.ToLower() switch
 475                {
 476                    "customer" => "Покупатель",
 477                    "seller" => "Продавец",
 478                    "system" => "Система",
 479                    "support" => "Поддержка",
 480                    _ => "Неизвестно"
 481                };
 482
 483                // Определяем текст сообщения
 484                var messageText = "";
 485                if (lastMessage.IsImage)
 486                {
 487                    messageText = "[Изображение]";
 488                }
 489                else if (lastMessage.Data != null && lastMessage.Data.Any())
 490                {
 491                    var fullText = string.Join(" ", lastMessage.Data);
 492                    messageText = fullText.Length > 100
 493                        ? fullText.Substring(0, 100) + "..."
 494                        : fullText;
 495                }
 496
 497                return Json(new
 498                {
 499                    success = true,
 500                    hasMessage = true,
 501                    sender = senderLabel,
 502                    senderType = senderType.ToLower(),
 503                    text = messageText,
 504                    createdAt = ToMoscowTime(lastMessage.CreatedAt).ToString("yyyy-MM-ddTHH:mm:ss"),
 505                    createdAtFormatted = ToMoscowTime(lastMessage.CreatedAt).ToString("dd.MM HH:mm")
 506                });
 507            }
 508            catch (Exception ex)
 509            {
 510                return Json(new { success = false, message = ex.Message });
 511            }
 512        }
 513
 514        /// <summary>
 515        /// AJAX: Получение истории сообщений чата (JSON)
 516        /// </summary>
 517        [HttpGet]
 518        public async Task<IActionResult> GetHistoryJson(int clientId, string chatId, int limit = 50,
 519            long? fromMessageId = null, string direction = "Backward")
 520        {
 521            try
 522            {
 523                if (string.IsNullOrEmpty(chatId))
 524                    return Json(new { success = false, message = "Не указан ID чата" });
 525
 526                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 527                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 528                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 529                {
 530                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 531                }
 532
 533                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 534                    ozonClient.DecryptApiKey);
 535
 536                var response = await chatManager.GetChatHistoryAsync(chatId, limit, fromMessageId, direction);
 537
 538                if (response == null)
 539                    return Json(new { success = false, message = "Не удалось получить историю чата" });
 540
 541                if (response.IsPremiumRequired)
 542                {
 543                    var ozonUrl = $"https://seller.ozon.ru/app/messenger/?group=customers_v2&id={chatId}";
 544                    // Формируем сообщение с ссылкой на сайте Ozon
 545                    var message = $"Для работы с чатами в данной системе необходима подписка Premium Plus. Но вы можете 
 546                    return Json(new {
 547                        success = false,
 548                        premiumRequired = true,
 549                        message,
 550                        ozonUrl
 551                    });
 552                }
 553
 554                // Синхронизация чатов с заказами в фоне (не блокируем ответ)
 555                _ = Task.Run(async () =>
 556                {
 557                    using var scope = _serviceScopeFactory.CreateScope();
 558                    var chatOrderService = scope.ServiceProvider.GetRequiredService<OzonChatOrderDataServices>();
 559                    var orderRepository = scope.ServiceProvider.GetRequiredService<OrderRepository>();
 560                    var chatStatisticService =
 561                        scope.ServiceProvider.GetRequiredService<OzonChatResponseStatisticDataServices>();
 562
 563                    try
 564                    {
 565                        await SyncChatWithOrdersAsync(clientId, chatId, response.Messages ?? new List<ChatMessage>(), ch
 566                            orderRepository);
 567
 568                        // Обработка статистики ответов
 569                        await ProcessChatStatisticsAsync(clientId, chatId, response.Messages ?? new List<ChatMessage>(),
 570                            chatOrderService);
 571                    }
 572                    catch (Exception ex)
 573                    {
 574                        // Логируем ошибку, но не прерываем основной поток
 575                        Console.WriteLine($"Ошибка синхронизации чата {chatId}: {ex.Message}");
 576                    }
 577                });
 578
 579                return Json(new
 580                {
 581                    success = true,
 582                    hasNext = response.HasNext,
 583                    messages = (response.Messages ?? new List<ChatMessage>()).Select(m => new
 584                    {
 585                        messageId = m.MessageId,
 586                        createdAt = ToMoscowTime(m.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 587                        createdAtFormatted = ToMoscowTime(m.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 588                        data = m.Data,
 589                        isImage = m.IsImage,
 590                        isRead = m.IsRead,
 591                        moderateImageStatus = m.ModerateImageStatus,
 592                        userId = m.User?.Id,
 593                        userType = m.User?.Type,
 594                        orderNumber = m.Context?.OrderNumber,
 595                        sku = m.Context?.Sku
 596                    })
 597                });
 598            }
 599            catch (Exception ex)
 600            {
 601                return Json(new { success = false, message = $"Ошибка при загрузке истории: {ex.Message}" });
 602            }
 603        }
 604
 605        /// <summary>
 606        /// AJAX: Отметить сообщения как прочитанные
 607        /// </summary>
 608        [HttpPost]
 609        public async Task<IActionResult> MarkAsRead([FromBody] MarkAsReadRequest request)
 610        {
 611            try
 612            {
 613                if (request == null || string.IsNullOrEmpty(request.ChatId))
 614                    return Json(new { success = false, message = "Не указан ID чата" });
 615
 616                var ozonClient = await _ozonClientServices.GetOzonClientAsync(request.ClientId);
 617                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 618                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 619                {
 620                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 621                }
 622
 623                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 624                    ozonClient.DecryptApiKey);
 625
 626                var response = await chatManager.MarkAsReadAsync(request.ChatId, request.FromMessageId);
 627
 628                if (response == null)
 629                    return Json(new { success = false, message = "Не удалось отметить сообщения как прочитанные" });
 630
 631                return Json(new
 632                {
 633                    success = true,
 634                    message = "Сообщения отмечены как прочитанные",
 635                    unreadCount = response.UnreadCount
 636                });
 637            }
 638            catch (Exception ex)
 639            {
 640                return Json(new { success = false, message = $"Ошибка: {ex.Message}" });
 641            }
 642        }
 643
 644        /// <summary>
 645        /// Обновление метаданных чатов (статус, тип) и создание новых записей для несвязанных чатов
 646        /// </summary>
 647        private async Task UpdateChatMetadataAsync(int clientId, List<ChatItem> chats,
 648            OzonChatOrderDataServices chatOrderService, OrderRepository orderRepository,
 649            OzonClientServices ozonClientService)
 650        {
 651            var ozonClient = await ozonClientService.GetOzonClientAsync(clientId);
 652            if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 653                string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 654                return;
 655
 656            var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 657                ozonClient.DecryptApiKey);
 658
 659            foreach (var chatItem in chats)
 660            {
 661                try
 662                {
 663                    if (string.IsNullOrWhiteSpace(chatItem.Chat.ChatId))
 664                        continue;
 665
 666                    // Получить все записи OzonChatOrder для этого чата
 667                    var chatOrders = await chatOrderService.GetByChatIdAsync(chatItem.Chat.ChatId);
 668
 669                    // Если есть существующие связи - обновляем их
 670                    if (chatOrders.Any())
 671                    {
 672                        foreach (var chatOrder in chatOrders)
 673                        {
 674                            // Обновить статус и тип чата
 675                            chatOrder.ChatStatus = chatItem.Chat.ChatStatus;
 676                            chatOrder.ChatType = chatItem.Chat.ChatType;
 677                            chatOrder.UnreadMessagesCount = chatItem.UnreadCount;
 678
 679                            await chatOrderService.Update(chatOrder);
 680                        }
 681                    }
 682                    // Если нет связей - пытаемся создать новую запись
 683                    else
 684                    {
 685                        // Получаем последнее сообщение чата для извлечения order_number
 686                        // Делаем это только для открытых чатов или с непрочитанными сообщениями
 687                        if (chatItem.Chat.ChatType == "BUYER_SELLER" || chatItem.UnreadCount > 0)
 688                        {
 689                            var history = await chatManager.GetChatHistoryAsync(
 690                                chatItem.Chat.ChatId,
 691                                limit: 50, // Берем несколько последних, чтобы найти с order_number
 692                                fromMessageId: null
 693                            );
 694
 695                            if (history?.Messages != null && history.Messages.Any())
 696                            {
 697                                // Ищем первое сообщение с order_number
 698                                var messageWithOrder = history.Messages
 699                                    .FirstOrDefault(m => !string.IsNullOrEmpty(m.Context?.OrderNumber));
 700
 701                                if (messageWithOrder != null)
 702                                {
 703                                    var orderNumber = messageWithOrder.Context!.OrderNumber;
 704                                    var sku = messageWithOrder.Context!.Sku;
 705
 706                                    var ordersToLink = new List<Order>();
 707
 708                                    // Сначала пытаемся найти заказ по номеру отправления
 709                                    if (!string.IsNullOrEmpty(orderNumber))
 710                                    {
 711                                        var order = await orderRepository.GetOrderByShipmentNumberAsync(orderNumber!,
 712                                            clientId);
 713                                        if (order != null)
 714                                            ordersToLink.Add(order);
 715                                    }
 716
 717                                    // Если не нашли по номеру, пытаемся найти по SKU (может быть несколько заказов)
 718                                    if (!ordersToLink.Any() && !string.IsNullOrEmpty(sku))
 719                                    {
 720                                        var ordersBySku = await orderRepository.GetOrdersBySkuAsync(sku!, clientId);
 721                                        ordersToLink.AddRange(ordersBySku);
 722                                    }
 723
 724                                    if (ordersToLink.Any())
 725                                    {
 726                                        foreach (var order in ordersToLink)
 727                                        {
 728                                            // postingNumber берем из orderNumber если есть, иначе из ShipmentNumber зак
 729                                            var postingNumber = !string.IsNullOrEmpty(orderNumber)
 730                                                ? orderNumber
 731                                                : order.ShipmentNumber;
 732
 733                                            // Создаем новую связь
 734                                            await chatOrderService.CreateOrUpdateAsync(
 735                                                chatId: chatItem.Chat.ChatId,
 736                                                orderId: order.Id,
 737                                                clientId: clientId,
 738                                                postingNumber: postingNumber,
 739                                                unreadCount: chatItem.UnreadCount,
 740                                                chatStatus: chatItem.Chat.ChatStatus,
 741                                                chatType: chatItem.Chat.ChatType
 742                                            );
 743
 744                                            // Обновляем информацию о последнем сообщении
 745                                            var lastMessage = history.Messages.First();
 746                                            var messageText = lastMessage.Data != null && lastMessage.Data.Any()
 747                                                ? string.Join(" ", lastMessage.Data)
 748                                                : string.Empty;
 749
 750                                            var lastMessageFromCustomer = lastMessage.User?.Type?
 751                                                .Equals("Customer", StringComparison.OrdinalIgnoreCase) == true;
 752
 753                                            await chatOrderService.UpdateLastMessageAsync(
 754                                                chatId: chatItem.Chat.ChatId,
 755                                                orderId: order.Id,
 756                                                messageDate: lastMessage.CreatedAt.ToUniversalTime(),
 757                                                messageText: messageText,
 758                                                fromCustomer: lastMessageFromCustomer
 759                                            );
 760
 761                                            Console.WriteLine(
 762                                                $"Создана связь: чат {chatItem.Chat.ChatId} → заказ {order.Id} ({order.S
 763                                        }
 764                                    }
 765                                    else
 766                                    {
 767                                        Console.WriteLine(
 768                                            $"Заказ с номером {orderNumber} / SKU {sku} не найден для чата {chatItem.Cha
 769                                    }
 770                                }
 771                            }
 772                        }
 773                    }
 774                }
 775                catch (Exception ex)
 776                {
 777                    Console.WriteLine($"Ошибка обработки чата {chatItem.Chat.ChatId}: {ex.Message}");
 778                }
 779            }
 780        }
 781
 782        /// <summary>
 783        /// Синхронизация чата с заказами на основе сообщений
 784        /// </summary>
 785        private async Task SyncChatWithOrdersAsync(int clientId, string chatId, List<ChatMessage> messages,
 786            OzonChatOrderDataServices chatOrderService, OrderRepository orderRepository)
 787        {
 788            if (messages == null || !messages.Any())
 789                return;
 790
 791            // Группируем сообщения по orderNumber или SKU
 792            var messagesWithContext = messages
 793                .Where(m => m.Context != null &&
 794                            (!string.IsNullOrEmpty(m.Context.OrderNumber) || !string.IsNullOrEmpty(m.Context.Sku)))
 795                .ToList();
 796
 797            // Создаем словарь для группировки: ключ - OrderNumber или SKU
 798            var messageGroups = new Dictionary<string, List<ChatMessage>>();
 799
 800            foreach (var message in messagesWithContext)
 801            {
 802                var key = !string.IsNullOrEmpty(message.Context!.OrderNumber)
 803                    ? message.Context.OrderNumber
 804                    : message.Context.Sku;
 805
 806                if (!string.IsNullOrEmpty(key))
 807                {
 808                    if (!messageGroups.ContainsKey(key))
 809                        messageGroups[key] = new List<ChatMessage>();
 810
 811                    messageGroups[key].Add(message);
 812                }
 813            }
 814
 815            foreach (var kvp in messageGroups)
 816            {
 817                var identifier = kvp.Key;
 818                var orderMessages = kvp.Value.OrderByDescending(m => m.CreatedAt).ToList();
 819                var lastMessage = orderMessages.First();
 820
 821                var orderNumber = lastMessage.Context?.OrderNumber;
 822                var sku = lastMessage.Context?.Sku;
 823
 824                var ordersToLink = new List<Order>();
 825
 826                // Сначала пытаемся найти заказ по номеру отправления
 827                if (!string.IsNullOrEmpty(orderNumber))
 828                {
 829                    var order = await orderRepository.GetOrderByShipmentNumberAsync(orderNumber!, clientId);
 830                    if (order != null)
 831                        ordersToLink.Add(order);
 832                }
 833
 834                // Если не нашли по номеру, пытаемся найти по SKU (может быть несколько заказов)
 835                if (!ordersToLink.Any() && !string.IsNullOrEmpty(sku))
 836                {
 837                    var ordersBySku = await orderRepository.GetOrdersBySkuAsync(sku!, clientId);
 838                    ordersToLink.AddRange(ordersBySku);
 839                }
 840
 841                if (!ordersToLink.Any())
 842                {
 843                    Console.WriteLine(
 844                        $"Заказ с идентификатором {identifier} (orderNumber: {orderNumber}, sku: {sku}) не найден в БД д
 845                    continue;
 846                }
 847
 848                // Подсчитать непрочитанные сообщения от покупателя
 849                var unreadCount = orderMessages.Count(m =>
 850                    !m.IsRead &&
 851                    m.User?.Type?.Equals("Customer", StringComparison.OrdinalIgnoreCase) == true);
 852
 853                // Определить тип последнего сообщения
 854                var lastMessageFromCustomer =
 855                    lastMessage.User?.Type?.Equals("Customer", StringComparison.OrdinalIgnoreCase) == true;
 856
 857                foreach (var order in ordersToLink)
 858                {
 859                    // Создать или обновить запись OzonChatOrder
 860                    // postingNumber берем из orderNumber если есть, иначе из ShipmentNumber заказа
 861                    var postingNumber = !string.IsNullOrEmpty(orderNumber) ? orderNumber : order.ShipmentNumber;
 862
 863                    await chatOrderService.CreateOrUpdateAsync(
 864                        chatId: chatId,
 865                        orderId: order.Id,
 866                        clientId: clientId,
 867                        postingNumber: postingNumber,
 868                        unreadCount: unreadCount,
 869                        chatStatus: null, // Статус чата будет обновлен отдельно
 870                        chatType: null
 871                    );
 872
 873                    // Обновить информацию о последнем сообщении
 874                    var messageText = lastMessage.Data != null && lastMessage.Data.Any()
 875                        ? string.Join(" ", lastMessage.Data)
 876                        : string.Empty;
 877
 878                    await chatOrderService.UpdateLastMessageAsync(
 879                        chatId: chatId,
 880                        orderId: order.Id,
 881                        messageDate: lastMessage.CreatedAt.ToUniversalTime(),
 882                        messageText: messageText,
 883                        fromCustomer: lastMessageFromCustomer
 884                    );
 885
 886                    Console.WriteLine($"Создана связь: чат {chatId} → заказ {order.Id} ({order.ShipmentNumber})");
 887                }
 888            }
 889        }
 890
 891        /// <summary>
 892        /// Обработка статистики ответов в чате
 893        /// </summary>
 894        private async Task ProcessChatStatisticsAsync(int clientId, string chatId, List<ChatMessage> messages,
 895            OzonChatResponseStatisticDataServices statisticService, OzonChatOrderDataServices chatOrderService)
 896        {
 897            if (messages == null || !messages.Any())
 898                return;
 899
 900            try
 901            {
 902                // Получаем информацию о чате (orderId, postingNumber, chatType, chatStatus)
 903                var chatOrders = await chatOrderService.GetByChatIdAsync(chatId);
 904                var chatOrder = chatOrders.FirstOrDefault();
 905
 906                var orderId = chatOrder?.OrderId;
 907                var postingNumber = chatOrder?.PostingNumber;
 908                var chatType = chatOrder?.ChatType;
 909                var chatStatus = chatOrder?.ChatStatus;
 910
 911                // Сортируем сообщения по дате (от старых к новым) для правильной обработки диалога
 912                var sortedMessages = messages.OrderBy(m => m.CreatedAt).ToList();
 913
 914                // Словарь для отслеживания последнего необработанного сообщения от покупателя
 915                long? lastCustomerMessageId = null;
 916                DateTime? lastCustomerMessageDate = null;
 917                string? lastCustomerMessageText = null;
 918
 919                foreach (var message in sortedMessages)
 920                {
 921                    var userType = message.User?.Type;
 922                    var messageText = message.Data != null && message.Data.Any()
 923                        ? string.Join(" ", message.Data)
 924                        : string.Empty;
 925
 926                    // Если это сообщение от покупателя (Customer)
 927                    if (userType?.Equals("Customer", StringComparison.OrdinalIgnoreCase) == true)
 928                    {
 929                        // Проверяем, есть ли уже запись для этого сообщения
 930                        var existingStatistic =
 931                            await statisticService.GetByClientMessageIdAsync(chatId, message.MessageId);
 932
 933                        if (existingStatistic == null)
 934                        {
 935                            // Создаем новую запись статистики для сообщения клиента
 936                            await statisticService.CreateOrUpdateClientMessageAsync(
 937                                chatId: chatId,
 938                                clientMessageId: message.MessageId,
 939                                clientMessageText: messageText,
 940                                clientMessageDate: message.CreatedAt.ToUniversalTime(),
 941                                clientId: clientId,
 942                                orderId: orderId,
 943                                postingNumber: postingNumber,
 944                                chatType: chatType,
 945                                chatStatus: chatStatus
 946                            );
 947                        }
 948
 949                        // Сохраняем как последнее сообщение от покупателя для потенциального ответа
 950                        lastCustomerMessageId = message.MessageId;
 951                        lastCustomerMessageDate = message.CreatedAt.ToUniversalTime();
 952                        lastCustomerMessageText = messageText;
 953                    }
 954                    // Если это сообщение от продавца (Seller)
 955                    else if (userType?.Equals("Seller", StringComparison.OrdinalIgnoreCase) == true ||
 956                             userType?.Equals("System", StringComparison.OrdinalIgnoreCase) == true)
 957                    {
 958                        // Если есть необработанное сообщение от покупателя, это может быть ответом на него
 959                        if (lastCustomerMessageId.HasValue && lastCustomerMessageDate.HasValue)
 960                        {
 961                            // Проверяем, не обработан ли уже ответ
 962                            var statistic =
 963                                await statisticService.GetByClientMessageIdAsync(chatId, lastCustomerMessageId.Value);
 964
 965                            if (statistic != null && !statistic.IsAnswered)
 966                            {
 967                                // Получаем имя оператора (если доступно)
 968                                var responderName = message.User?.Type ?? "Seller";
 969
 970                                // Обновляем статистику с информацией об ответе
 971                                await statisticService.UpdateResponseAsync(
 972                                    chatId: chatId,
 973                                    clientMessageId: lastCustomerMessageId.Value,
 974                                    sellerResponseMessageId: message.MessageId,
 975                                    sellerResponseText: messageText,
 976                                    sellerResponseDate: message.CreatedAt.ToUniversalTime(),
 977                                    responderName: responderName
 978                                );
 979
 980                                // Сбрасываем последнее сообщение покупателя, т.к. на него уже ответили
 981                                lastCustomerMessageId = null;
 982                                lastCustomerMessageDate = null;
 983                                lastCustomerMessageText = null;
 984                            }
 985                        }
 986                    }
 987                }
 988
 989                Console.WriteLine($"Статистика обработана для чата {chatId}: {sortedMessages.Count} сообщений");
 990            }
 991            catch (Exception ex)
 992            {
 993                Console.WriteLine($"Ошибка обработки статистики для чата {chatId}: {ex.Message}");
 994            }
 995        }
 996
 997        /// <summary>
 998        /// AJAX: Отправить сообщение в чат
 999        /// </summary>
 1000        [HttpPost]
 1001        public async Task<IActionResult> SendMessage([FromBody] SendMessageRequest request)
 1002        {
 1003            try
 1004            {
 1005                if (request == null || string.IsNullOrEmpty(request.ChatId) || string.IsNullOrWhiteSpace(request.Text))
 1006                    return Json(new { success = false, message = "Не указан ID чата или текст сообщения" });
 1007
 1008                if (request.Text.Length > 1000)
 1009                    return Json(new { success = false, message = "Текст сообщения не должен превышать 1000 символов" });
 1010
 1011                var ozonClient = await _ozonClientServices.GetOzonClientAsync(request.ClientId);
 1012                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 1013                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 1014                {
 1015                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 1016                }
 1017
 1018                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 1019                    ozonClient.DecryptApiKey);
 1020
 1021                var response = await chatManager.SendMessageAsync(request.ChatId, request.Text);
 1022
 1023                if (response == null || response.Result != "success")
 1024                    return Json(new { success = false, message = "Не удалось отправить сообщение" });
 1025
 1026                // Получаем имя пользователя
 1027                var responderName = !string.IsNullOrEmpty(User.Identity?.Name)
 1028                    ? User.Identity.Name
 1029                    : User.Identity?.IsAuthenticated == true
 1030                        ? "Authenticated User"
 1031                        : "System";
 1032
 1033                // Сохраняем данные для фоновой задачи
 1034                var chatIdCopy = request.ChatId;
 1035                var textCopy = request.Text;
 1036                var clientIdCopy = request.ClientId;
 1037                var responderNameCopy = responderName;
 1038
 1039                // Записываем ответ менеджера в статистику в фоне (не блокируем ответ)
 1040                _ = Task.Run(async () =>
 1041                {
 1042                    try
 1043                    {
 1044                        using var scope = _serviceScopeFactory.CreateScope();
 1045                        var chatStatisticService = scope.ServiceProvider
 1046                            .GetRequiredService<OzonChatResponseStatisticDataServices>();
 1047                        var chatOrderService = scope.ServiceProvider
 1048                            .GetRequiredService<OzonChatOrderDataServices>();
 1049
 1050                        // Получаем данные чата для orderId и postingNumber
 1051                        var chatOrders = await chatOrderService.GetByChatIdAsync(chatIdCopy);
 1052                        var chatOrder = chatOrders.FirstOrDefault();
 1053
 1054                        await chatStatisticService.AddManagerMessageAsync(
 1055                            chatId: chatIdCopy,
 1056                            clientId: clientIdCopy,
 1057                            messageText: textCopy,
 1058                            responderName: responderNameCopy,
 1059                            orderId: chatOrder?.OrderId,
 1060                            postingNumber: chatOrder?.PostingNumber,
 1061                            chatType: chatOrder?.ChatType,
 1062                            chatStatus: chatOrder?.ChatStatus
 1063                        );
 1064
 1065                        Console.WriteLine($"Записан ответ менеджера {responderNameCopy} в чате {chatIdCopy}");
 1066                    }
 1067                    catch (Exception ex)
 1068                    {
 1069                        Console.WriteLine($"Ошибка записи ответа менеджера: {ex.Message}");
 1070                    }
 1071                });
 1072
 1073                return Json(new
 1074                {
 1075                    success = true,
 1076                    message = "Сообщение отправлено успешно"
 1077                });
 1078            }
 1079            catch (Exception ex)
 1080            {
 1081                return Json(new { success = false, message = $"Ошибка: {ex.Message}" });
 1082            }
 1083        }
 1084
 1085        /// <summary>
 1086        /// AJAX: Отправить файл в чат
 1087        /// </summary>
 1088        [HttpPost]
 1089        public async Task<IActionResult> SendFile([FromBody] SendFileRequest request)
 1090        {
 1091            try
 1092            {
 1093                if (request == null || string.IsNullOrEmpty(request.ChatId) ||
 1094                    string.IsNullOrWhiteSpace(request.Base64Content) || string.IsNullOrWhiteSpace(request.FileName))
 1095                    return Json(new { success = false, message = "Не указан ID чата, содержимое файла или имя файла" });
 1096
 1097                var ozonClient = await _ozonClientServices.GetOzonClientAsync(request.ClientId);
 1098                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 1099                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 1100                {
 1101                    return Json(new { success = false, message = "Клиент Ozon не найден или некорректные данные API" });
 1102                }
 1103
 1104                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 1105                    ozonClient.DecryptApiKey);
 1106
 1107                var response = await chatManager.SendFileAsync(request.ChatId, request.Base64Content, request.FileName);
 1108
 1109                if (response == null || response.Result != "success")
 1110                    return Json(new { success = false, message = "Не удалось отправить файл" });
 1111
 1112                // Получаем имя пользователя из Identity до запуска фонового Task
 1113                var currentUserName = !string.IsNullOrEmpty(User.Identity?.Name)
 1114                    ? User.Identity.Name
 1115                    : User.Identity?.IsAuthenticated == true
 1116                        ? "Authenticated User"
 1117                        : "System";
 1118
 1119                // Сохраняем параметры для фонового Task
 1120                var chatIdForTask = request.ChatId;
 1121                var fileNameForTask = request.FileName;
 1122                var clientIdForTask = request.ClientId;
 1123
 1124                // Обновление статистики ответов в фоне (не блокируем ответ)
 1125                _ = Task.Run(async () =>
 1126                {
 1127                    // Создаем новый scope для scoped сервисов (важно! иначе сервис может быть disposed)
 1128                    using var scope = _serviceScopeFactory.CreateScope();
 1129                    var chatStatisticService =
 1130                        scope.ServiceProvider.GetRequiredService<OzonChatResponseStatisticDataServices>();
 1131                    var ozonClientServices = scope.ServiceProvider.GetRequiredService<OzonClientServices>();
 1132
 1133                    try
 1134                    {
 1135                        // Небольшая задержка для того чтобы файл точно появился в истории
 1136                        await Task.Delay(500);
 1137
 1138                        // Найти последнее неотвеченное сообщение от покупателя в этом чате
 1139                        var unansweredMessages = await chatStatisticService.GetByChatIdAsync(chatIdForTask);
 1140                        var lastUnanswered = unansweredMessages
 1141                            .Where(m => !m.IsAnswered)
 1142                            .OrderByDescending(m => m.ClientMessageDate)
 1143                            .FirstOrDefault();
 1144
 1145                        if (lastUnanswered != null)
 1146                        {
 1147                            // Получаем клиента для создания нового chatManager
 1148                            var ozonClientForTask = await ozonClientServices.GetOzonClientAsync(clientIdForTask);
 1149                            if (ozonClientForTask == null || string.IsNullOrEmpty(ozonClientForTask.DecryptClientId) ||
 1150                                string.IsNullOrEmpty(ozonClientForTask.DecryptApiKey))
 1151                            {
 1152                                Console.WriteLine($"Не удалось получить клиента Ozon для обновления статистики");
 1153                                return;
 1154                            }
 1155
 1156                            var httpClientFactory = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>();
 1157                            var newChatManager = new OzonChatManager(httpClientFactory,
 1158                                ozonClientForTask.DecryptClientId,
 1159                                ozonClientForTask.DecryptApiKey);
 1160
 1161                            // Получаем историю чата для получения messageId отправленного файла
 1162                            var history = await newChatManager.GetChatHistoryAsync(chatIdForTask, 10, null, "Backward");
 1163                            if (history?.Messages != null && history.Messages.Any())
 1164                            {
 1165                                // Находим последнее сообщение от продавца (это должен быть наш файл)
 1166                                var sentMessage = history.Messages
 1167                                    .Where(m => m.User?.Type?.Equals("Seller", StringComparison.OrdinalIgnoreCase) ==
 1168                                                true)
 1169                                    .OrderByDescending(m => m.CreatedAt)
 1170                                    .FirstOrDefault();
 1171
 1172                                if (sentMessage != null)
 1173                                {
 1174                                    var fileText = $"[Файл: {fileNameForTask}]";
 1175
 1176                                    await chatStatisticService.UpdateResponseAsync(
 1177                                        chatId: chatIdForTask,
 1178                                        clientMessageId: lastUnanswered.ClientMessageId,
 1179                                        sellerResponseMessageId: sentMessage.MessageId,
 1180                                        sellerResponseText: fileText,
 1181                                        sellerResponseDate: sentMessage.CreatedAt.ToUniversalTime(),
 1182                                        responderName: currentUserName
 1183                                    );
 1184
 1185                                    Console.WriteLine(
 1186                                        $"Статистика обновлена: ответ (файл) от {currentUserName} на сообщение {lastUnan
 1187                                }
 1188                                else
 1189                                {
 1190                                    Console.WriteLine(
 1191                                        $"Не найдено сообщение от продавца в истории чата {chatIdForTask}");
 1192                                }
 1193                            }
 1194                            else
 1195                            {
 1196                                Console.WriteLine($"История чата {chatIdForTask} пуста или не загружена");
 1197                            }
 1198                        }
 1199                        else
 1200                        {
 1201                            Console.WriteLine($"Нет неотвеченных сообщений в чате {chatIdForTask}");
 1202                        }
 1203                    }
 1204                    catch (Exception ex)
 1205                    {
 1206                        Console.WriteLine($"Ошибка обновления статистики ответа (файл): {ex.Message}");
 1207                    }
 1208                });
 1209
 1210                return Json(new
 1211                {
 1212                    success = true,
 1213                    message = "Файл отправлен успешно"
 1214                });
 1215            }
 1216            catch (Exception ex)
 1217            {
 1218                return Json(new { success = false, message = $"Ошибка: {ex.Message}" });
 1219            }
 1220        }
 1221
 1222        /// <summary>
 1223        /// Экспорт чатов в Excel
 1224        /// </summary>
 1225        [HttpPost]
 1226        public async Task<IActionResult> ExportToExcel(int clientId, string status = "All", bool unreadOnly = false)
 1227        {
 1228            try
 1229            {
 1230                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 1231                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 1232                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 1233                {
 1234                    TempData["ErrorMessage"] = "Клиент Ozon не найден или некорректные данные API";
 1235                    return RedirectToAction("Index");
 1236                }
 1237
 1238                var chatManager = new OzonChatManager(_httpClientFactory, ozonClient.DecryptClientId,
 1239                    ozonClient.DecryptApiKey);
 1240                var allChats = await chatManager.GetAllChatsAsync(status, unreadOnly);
 1241
 1242                // Создаём простой CSV для экспорта
 1243                var csvData = "ID чата,Статус,Тип,Дата создания,Непрочитанных сообщений\n";
 1244                foreach (var chat in allChats)
 1245                {
 1246                    csvData += $"{chat.Chat.ChatId},{chat.Chat.ChatStatus},{chat.Chat.ChatType}," +
 1247                               $"{chat.Chat.CreatedAt:yyyy-MM-dd HH:mm:ss},{chat.UnreadCount}\n";
 1248                }
 1249
 1250                var bytes = System.Text.Encoding.UTF8.GetBytes(csvData);
 1251                var fileName = $"ozon_chats_{ozonClient.Name}_{DateTime.Now:yyyyMMdd_HHmmss}.csv";
 1252
 1253                return File(bytes, "text/csv", fileName);
 1254            }
 1255            catch (Exception ex)
 1256            {
 1257                TempData["ErrorMessage"] = $"Ошибка при экспорте: {ex.Message}";
 1258                return RedirectToAction("Index");
 1259            }
 1260        }
 1261
 1262        /// <summary>
 1263        /// API: Получить все чаты для конкретного заказа
 1264        /// </summary>
 1265        [HttpGet]
 1266        public async Task<IActionResult> GetOrderChats(int orderId)
 1267        {
 1268            try
 1269            {
 1270                if (orderId <= 0)
 1271                    return Json(new { success = false, message = "Некорректный ID заказа" });
 1272
 1273                var chatOrders = await _chatOrderService.GetByOrderIdAsync(orderId);
 1274
 1275                return Json(new
 1276                {
 1277                    success = true,
 1278                    orderId,
 1279                    chats = chatOrders.Select(co => new
 1280                    {
 1281                        id = co.Id,
 1282                        chatId = co.ChatId,
 1283                        clientId = co.ClientId,
 1284                        clientName = co.Client?.Name,
 1285                        postingNumber = co.PostingNumber,
 1286                        unreadMessagesCount = co.UnreadMessagesCount,
 1287                        lastMessageDate = ToMoscowTime(co.LastMessageDate)?.ToString("yyyy-MM-dd HH:mm:ss"),
 1288                        lastMessageText = co.LastMessageText,
 1289                        lastMessageFromCustomer = co.LastMessageFromCustomer,
 1290                        chatStatus = co.ChatStatus,
 1291                        chatType = co.ChatType,
 1292                        createdAt = ToMoscowTime(co.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 1293                        updatedAt = ToMoscowTime(co.UpdatedAt).ToString("yyyy-MM-dd HH:mm:ss")
 1294                    }),
 1295                    totalUnreadCount = chatOrders.Sum(co => co.UnreadMessagesCount)
 1296                });
 1297            }
 1298            catch (Exception ex)
 1299            {
 1300                return Json(new { success = false, message = $"Ошибка при получении чатов заказа: {ex.Message}" });
 1301            }
 1302        }
 1303
 1304        /// <summary>
 1305        /// API: Получить все заказы для конкретного чата
 1306        /// </summary>
 1307        [HttpGet]
 1308        public async Task<IActionResult> GetChatOrders(string chatId)
 1309        {
 1310            try
 1311            {
 1312                if (string.IsNullOrEmpty(chatId))
 1313                    return Json(new { success = false, message = "Не указан ID чата" });
 1314
 1315                var chatOrders = await _chatOrderService.GetByChatIdAsync(chatId);
 1316
 1317                return Json(new
 1318                {
 1319                    success = true,
 1320                    chatId,
 1321                    orders = chatOrders.Select(co => new
 1322                    {
 1323                        id = co.Id,
 1324                        orderId = co.OrderId,
 1325                        orderShipmentNumber = co.Order?.ShipmentNumber,
 1326                        orderProcessingDate = ToMoscowTime(co.Order?.ProcessingDate)?.ToString("yyyy-MM-dd HH:mm:ss"),
 1327                        orderShippingDate = ToMoscowTime(co.Order?.ShippingDate)?.ToString("yyyy-MM-dd HH:mm:ss"),
 1328                        orderAppStatus = co.Order?.AppStatus?.Name,
 1329                        orderClientStatus = co.Order?.ClientStatus?.Name,
 1330                        clientId = co.ClientId,
 1331                        clientName = co.Client?.Name,
 1332                        postingNumber = co.PostingNumber,
 1333                        unreadMessagesCount = co.UnreadMessagesCount,
 1334                        lastMessageDate = ToMoscowTime(co.LastMessageDate)?.ToString("yyyy-MM-dd HH:mm:ss"),
 1335                        lastMessageText = co.LastMessageText,
 1336                        lastMessageFromCustomer = co.LastMessageFromCustomer,
 1337                        chatStatus = co.ChatStatus,
 1338                        chatType = co.ChatType
 1339                    }),
 1340                    totalUnreadCount = chatOrders.Sum(co => co.UnreadMessagesCount)
 1341                });
 1342            }
 1343            catch (Exception ex)
 1344            {
 1345                return Json(new { success = false, message = $"Ошибка при получении заказов чата: {ex.Message}" });
 1346            }
 1347        }
 1348
 1349        /// <summary>
 1350        /// API: Связать чат с заказом вручную
 1351        /// </summary>
 1352        [HttpPost]
 1353        public async Task<IActionResult> LinkChatToOrder([FromBody] LinkChatToOrderRequest request)
 1354        {
 1355            try
 1356            {
 1357                if (string.IsNullOrEmpty(request.ChatId) || request.OrderId <= 0 || request.ClientId <= 0)
 1358                    return Json(new { success = false, message = "Некорректные параметры запроса" });
 1359
 1360                var chatOrder = await _chatOrderService.CreateOrUpdateAsync(
 1361                    chatId: request.ChatId,
 1362                    orderId: request.OrderId,
 1363                    clientId: request.ClientId,
 1364                    postingNumber: request.PostingNumber,
 1365                    unreadCount: 0
 1366                );
 1367
 1368                return Json(new
 1369                {
 1370                    success = true,
 1371                    message = "Чат успешно связан с заказом",
 1372                    chatOrder = new
 1373                    {
 1374                        id = chatOrder.Id,
 1375                        chatId = chatOrder.ChatId,
 1376                        orderId = chatOrder.OrderId,
 1377                        clientId = chatOrder.ClientId,
 1378                        postingNumber = chatOrder.PostingNumber
 1379                    }
 1380                });
 1381            }
 1382            catch (Exception ex)
 1383            {
 1384                return Json(new { success = false, message = $"Ошибка при связывании чата с заказом: {ex.Message}" });
 1385            }
 1386        }
 1387
 1388        /// <summary>
 1389        /// API: Подсказки для поиска (артикулы, товары, номера заказов)
 1390        /// </summary>
 1391        [HttpGet]
 1392        public async Task<IActionResult> GetChatSearchSuggestions(int clientId = 0, string query = "")
 1393        {
 1394            try
 1395            {
 1396                if (string.IsNullOrWhiteSpace(query))
 1397                    return Json(new { success = true, suggestions = new object[0] });
 1398
 1399                // Если запрос совпадает с форматом номера отправления — попробуем вернуть точную запись сразу
 1400                if (PostingNumberRegex.IsMatch(query))
 1401                {
 1402                    var exact = await _chatOrderService.GetByPostingNumberAsync(query);
 1403                    if (exact != null)
 1404                    {
 1405                        // Отдаем сразу одну запись, без массива suggestions
 1406                        return Json(new
 1407                        {
 1408                            success = true,
 1409                            found = true,
 1410                            record = new
 1411                            {
 1412                                type = "order",
 1413                                label = exact.PostingNumber,
 1414                                icon = "fas fa-box",
 1415                                hint = "Номер отправления",
 1416                                chatId = exact.ChatId,
 1417                                clientId = exact.ClientId
 1418                            }
 1419                        });
 1420                    }
 1421                }
 1422
 1423                var suggestions = new List<object>();
 1424
 1425                // 1. Номера заказов (PostingNumber) из таблицы OzonChatOrders (подсказки)
 1426                var orderNumbers = await _chatOrderService.GetDistinctPostingNumbersAsync(query, clientId, 10);
 1427                suggestions.AddRange(orderNumbers.Select(n => new
 1428                {
 1429                    type = "order",
 1430                    label = n,
 1431                    icon = "fas fa-box",
 1432                    hint = "Номер заказа"
 1433                } as object));
 1434
 1435                // 2. Артикулы и названия товаров из сообщений покупателей
 1436                var buyerMessages = await _chatStatisticService.GetSampleBuyerMessagesAsync(query, clientId, 100);
 1437
 1438                var seenArticles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 1439                var seenProducts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 1440
 1441                foreach (var (_, msgText, _, _) in buyerMessages)
 1442                {
 1443                    // Извлекаем артикулы
 1444                    var articleMatches = ArticleRegex.Matches(msgText);
 1445                    foreach (Match m in articleMatches)
 1446                    {
 1447                        var art = m.Groups[1].Value.Trim();
 1448                        if (art.Length >= 2 && seenArticles.Add(art) && seenArticles.Count <= 10)
 1449                        {
 1450                            suggestions.Add(new
 1451                            {
 1452                                type = "article",
 1453                                label = art,
 1454                                icon = "fas fa-tag",
 1455                                hint = "Артикул"
 1456                            } as object);
 1457                        }
 1458                    }
 1459
 1460                    // Извлекаем названия товаров (text in quotes)
 1461                    var productMatches = ProductNameRegex.Matches(msgText);
 1462                    foreach (Match m in productMatches)
 1463                    {
 1464                        var prod = m.Groups[1].Value.Trim();
 1465                        if (prod.Length >= 5 && seenProducts.Add(prod) && seenProducts.Count <= 10)
 1466                        {
 1467                            suggestions.Add(new
 1468                            {
 1469                                type = "product",
 1470                                label = prod,
 1471                                icon = "fas fa-cube",
 1472                                hint = "Товар"
 1473                            } as object);
 1474                        }
 1475                    }
 1476                }
 1477
 1478                return Json(new { success = true, suggestions });
 1479            }
 1480            catch (Exception ex)
 1481            {
 1482                return Json(new { success = false, message = ex.Message, suggestions = new object[0] });
 1483            }
 1484        }
 1485
 1486        /// <summary>
 1487        /// API: Поиск чатов по артикулу, названию товара, номеру заказа (из БД, без Ozon API)
 1488        /// </summary>
 1489        [HttpGet]
 1490        public async Task<IActionResult> SearchChatsDb(int clientId = 0, string query = "")
 1491        {
 1492            try
 1493            {
 1494                if (string.IsNullOrWhiteSpace(query) || query.Length < 2)
 1495                    return Json(new { success = false, message = "Запрос должен содержать минимум 2 символа" });
 1496
 1497                var chatOrdersDict = new Dictionary<string, OzonChatOrder>(StringComparer.OrdinalIgnoreCase);
 1498
 1499                // Если запрос выглядит как точный номер отправления — ищем по нему и возвращаем связанный чат
 1500                if (PostingNumberRegex.IsMatch(query))
 1501                {
 1502                    var exact = await _chatOrderService.GetByPostingNumberAsync(query);
 1503                    if (exact != null)
 1504                    {
 1505                        chatOrdersDict[exact.ChatId] = exact;
 1506                        var allChatOrdersSingle = chatOrdersDict.Values
 1507                            .OrderByDescending(co => co.LastMessageDate ?? co.CreatedAt)
 1508                            .ToList();
 1509
 1510                        return Json(new
 1511                        {
 1512                            success = true,
 1513                            query,
 1514                            chats = allChatOrdersSingle.Select(co => new
 1515                            {
 1516                                chatId = co.ChatId,
 1517                                chatStatus = co.ChatStatus ?? "UNSPECIFIED",
 1518                                chatType = co.ChatType ?? "UNSPECIFIED",
 1519                                createdAt = ToMoscowTime(co.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 1520                                createdAtFormatted = ToMoscowTime(co.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 1521                                unreadCount = co.UnreadMessagesCount,
 1522                                lastMessageId = 0,
 1523                                clientId = co.ClientId,
 1524                                clientName = co.Client?.Name ?? $"Клиент {co.ClientId}",
 1525                                postingNumber = co.PostingNumber,
 1526                                lastMessageText = co.LastMessageText ?? "",
 1527                                lastMessageDate = co.LastMessageDate.HasValue
 1528                                    ? ToMoscowTime(co.LastMessageDate.Value).ToString("dd.MM HH:mm")
 1529                                    : null,
 1530                                lastMessageFromCustomer = co.LastMessageFromCustomer
 1531                            }),
 1532                            totalChatsCount = allChatOrdersSingle.Count
 1533                        });
 1534                    }
 1535                }
 1536
 1537                // 1. Поиск по PostingNumber в OzonChatOrders
 1538                var byPosting = await _chatOrderService.SearchByPostingNumberAsync(query, clientId, 50);
 1539                foreach (var co in byPosting)
 1540                    chatOrdersDict[co.ChatId] = co;
 1541
 1542                // 2. Поиск по тексту сообщений покупателей в OzonChatResponseStatistics
 1543                var chatIdsFromMessages =
 1544                    await _chatStatisticService.GetChatIdsByMessageQueryAsync(query, clientId, 100);
 1545                if (chatIdsFromMessages.Any())
 1546                {
 1547                    var newChatIds = chatIdsFromMessages.Where(id => !chatOrdersDict.ContainsKey(id)).ToList();
 1548                    if (newChatIds.Any())
 1549                    {
 1550                        var byMessages = await _chatOrderService.GetByChatIdsAsync(newChatIds, clientId);
 1551                        foreach (var co in byMessages)
 1552                            chatOrdersDict.TryAdd(co.ChatId, co);
 1553                    }
 1554                }
 1555
 1556                var allChatOrders = chatOrdersDict.Values
 1557                    .OrderByDescending(co => co.LastMessageDate ?? co.CreatedAt)
 1558                    .ToList();
 1559
 1560                if (!allChatOrders.Any())
 1561                    return Json(new { success = true, query, chats = new object[0], totalChatsCount = 0 });
 1562
 1563                return Json(new
 1564                {
 1565                    success = true,
 1566                    query,
 1567                    chats = allChatOrders.Select(co => new
 1568                    {
 1569                        chatId = co.ChatId,
 1570                        chatStatus = co.ChatStatus ?? "UNSPECIFIED",
 1571                        chatType = co.ChatType ?? "UNSPECIFIED",
 1572                        createdAt = ToMoscowTime(co.CreatedAt).ToString("yyyy-MM-dd HH:mm:ss"),
 1573                        createdAtFormatted = ToMoscowTime(co.CreatedAt).ToString("dd.MM.yyyy HH:mm"),
 1574                        unreadCount = co.UnreadMessagesCount,
 1575                        lastMessageId = 0,
 1576                        clientId = co.ClientId,
 1577                        clientName = co.Client?.Name ?? $"Клиент {co.ClientId}",
 1578                        postingNumber = co.PostingNumber,
 1579                        lastMessageText = co.LastMessageText ?? "",
 1580                        lastMessageDate = co.LastMessageDate.HasValue
 1581                            ? ToMoscowTime(co.LastMessageDate.Value).ToString("dd.MM HH:mm")
 1582                            : null,
 1583                        lastMessageFromCustomer = co.LastMessageFromCustomer
 1584                    }),
 1585                    totalChatsCount = allChatOrders.Count
 1586                });
 1587            }
 1588            catch (Exception ex)
 1589            {
 1590                return Json(new { success = false, message = $"Ошибка поиска: {ex.Message}" });
 1591            }
 1592        }
 1593
 1594        /// <summary>
 1595        /// Прокси для загрузки файлов из Ozon Chat API
 1596        /// (файлы требуют авторизации через Client-Id и Api-Key)
 1597        /// </summary>
 1598        [HttpGet]
 1599        public async Task<IActionResult> GetChatFile(int clientId, string fileUrl)
 1600        {
 1601            try
 1602            {
 1603                if (string.IsNullOrWhiteSpace(fileUrl))
 1604                {
 1605                    return BadRequest("fileUrl is required");
 1606                }
 1607
 1608                var ozonClient = await _ozonClientServices.GetOzonClientAsync(clientId);
 1609                if (ozonClient == null || string.IsNullOrEmpty(ozonClient.DecryptClientId) ||
 1610                    string.IsNullOrEmpty(ozonClient.DecryptApiKey))
 1611                {
 1612                    return NotFound("Ozon client not found");
 1613                }
 1614
 1615                // Проверяем, что URL относится к api-seller.ozon.ru
 1616                if (!fileUrl.StartsWith("https://api-seller.ozon.ru/"))
 1617                {
 1618                    return BadRequest("Invalid file URL");
 1619                }
 1620
 1621                using var httpClient = _httpClientFactory.CreateClient();
 1622                httpClient.Timeout = TimeSpan.FromSeconds(30);
 1623                httpClient.DefaultRequestHeaders.Clear();
 1624                httpClient.DefaultRequestHeaders.Add("Client-Id", ozonClient.DecryptClientId);
 1625                httpClient.DefaultRequestHeaders.Add("Api-Key", ozonClient.DecryptApiKey);
 1626
 1627                var response = await httpClient.GetAsync(fileUrl);
 1628
 1629                if (!response.IsSuccessStatusCode)
 1630                {
 1631                    return StatusCode((int)response.StatusCode, "Failed to fetch file from Ozon");
 1632                }
 1633
 1634                var contentType = response.Content.Headers.ContentType?.MediaType ?? "application/octet-stream";
 1635                var fileBytes = await response.Content.ReadAsByteArrayAsync();
 1636
 1637                return File(fileBytes, contentType);
 1638            }
 1639            catch (Exception ex)
 1640            {
 1641                return StatusCode(500, $"Error fetching file: {ex.Message}");
 1642            }
 1643        }
 1644
 1645        /// <summary>
 1646        /// API: Получить статистику ответов менеджеров
 1647        /// </summary>
 1648        [HttpGet]
 1649        public async Task<IActionResult> GetResponseStatistics(int clientId = 0, DateTime? from = null,
 1650            DateTime? to = null, string? responderName = null, int page = 1, int pageSize = 50)
 1651        {
 1652            try
 1653            {
 1654                if (page < 1) page = 1;
 1655                pageSize = Math.Clamp(pageSize, 1, 100);
 1656
 1657                // Если даты не указаны - берем последние 30 дней
 1658                var dateFrom = from ?? DateTime.UtcNow.AddDays(-30);
 1659                // Для dateTo используем конец дня, чтобы включить все записи за этот день
 1660                var dateTo = to.HasValue
 1661                    ? to.Value.Date.AddDays(1).AddTicks(-1) // Конец дня (23:59:59.9999999)
 1662                    : DateTime.UtcNow.AddDays(1); // Завтра, чтобы включить сегодняшние записи
 1663
 1664                List<OzonChatResponseStatistic> statistics;
 1665
 1666                if (clientId > 0)
 1667                {
 1668                    statistics = await _chatStatisticService.GetByClientIdAsync(clientId, dateFrom, dateTo);
 1669                }
 1670                else
 1671                {
 1672                    // Получаем по всем клиентам
 1673                    var allStats = new List<OzonChatResponseStatistic>();
 1674                    var clients = await _ozonClientServices.GetOzonClients();
 1675                    foreach (var client in clients.Where(c => c.ClientType == ClientType.OZON))
 1676                    {
 1677                        var clientStats = await _chatStatisticService.GetByClientIdAsync(client.Id, dateFrom, dateTo);
 1678                        allStats.AddRange(clientStats);
 1679                    }
 1680
 1681                    statistics = allStats.OrderByDescending(s => s.SellerResponseDate ?? s.CreatedAt).ToList();
 1682                }
 1683
 1684                // Список всех уникальных менеджеров (для выпадающего списка)
 1685                var allResponders = statistics
 1686                    .Where(s => !string.IsNullOrEmpty(s.ResponderName) && s.SellerResponseDate.HasValue)
 1687                    .Select(s => s.ResponderName!)
 1688                    .Distinct()
 1689                    .OrderBy(n => n)
 1690                    .ToList();
 1691
 1692                // Исключаем записи без даты ответа
 1693                statistics = statistics.Where(s => s.SellerResponseDate.HasValue).ToList();
 1694
 1695                // Фильтрация по менеджеру
 1696                if (!string.IsNullOrEmpty(responderName))
 1697                {
 1698                    statistics = statistics
 1699                        .Where(s => s.ResponderName != null &&
 1700                                    s.ResponderName.Equals(responderName, StringComparison.OrdinalIgnoreCase))
 1701                        .ToList();
 1702                }
 1703
 1704                // Статистика для сводки
 1705                var totalResponses = statistics.Count;
 1706                var respondersStats = statistics
 1707                    .Where(s => !string.IsNullOrEmpty(s.ResponderName))
 1708                    .GroupBy(s => s.ResponderName)
 1709                    .Select(g => new
 1710                    {
 1711                        name = g.Key,
 1712                        count = g.Count(),
 1713                        avgResponseTime = g.Where(x => x.ResponseTimeSeconds.HasValue)
 1714                            .Average(x => (double?)x.ResponseTimeSeconds) ?? 0
 1715                    })
 1716                    .OrderByDescending(x => x.count)
 1717                    .ToList();
 1718
 1719                var avgResponseTime = statistics
 1720                    .Where(s => s.ResponseTimeSeconds.HasValue && s.ResponseTimeSeconds > 0)
 1721                    .Average(s => (double?)s.ResponseTimeSeconds) ?? 0;
 1722
 1723                // Пагинация
 1724                var totalItems = statistics.Count;
 1725                var totalPages = totalItems > 0 ? (int)Math.Ceiling((double)totalItems / pageSize) : 1;
 1726                var pagedStats = statistics
 1727                    .OrderByDescending(s => s.SellerResponseDate ?? s.CreatedAt)
 1728                    .Skip((page - 1) * pageSize)
 1729                    .Take(pageSize)
 1730                    .ToList();
 1731
 1732                return Json(new
 1733                {
 1734                    success = true,
 1735                    allResponders, // Список всех менеджеров для выпадающего списка
 1736                    summary = new
 1737                    {
 1738                        totalResponses,
 1739                        avgResponseTimeSeconds = Math.Round(avgResponseTime, 0),
 1740                        avgResponseTimeFormatted = FormatSeconds((int)avgResponseTime),
 1741                        responders = respondersStats,
 1742                        dateFrom = dateFrom.ToString("dd.MM.yyyy"),
 1743                        dateTo = dateTo.ToString("dd.MM.yyyy")
 1744                    },
 1745                    statistics = pagedStats.Select(s => new
 1746                    {
 1747                        id = s.Id,
 1748                        chatId = s.ChatId,
 1749                        clientId = s.ClientId,
 1750                        orderId = s.OrderId,
 1751                        postingNumber = s.PostingNumber,
 1752                        sellerResponseText = s.SellerResponseText != null && s.SellerResponseText.Length > 100
 1753                            ? s.SellerResponseText.Substring(0, 100) + "..."
 1754                            : s.SellerResponseText,
 1755                        sellerResponseDate = ToMoscowTime(s.SellerResponseDate)?.ToString("dd.MM.yyyy HH:mm"),
 1756                        responseTimeSeconds = s.ResponseTimeSeconds,
 1757                        responseTimeFormatted = FormatSeconds(s.ResponseTimeSeconds ?? 0),
 1758                        responderName = s.ResponderName ?? "Неизвестно",
 1759                        chatType = s.ChatType,
 1760                        chatStatus = s.ChatStatus
 1761                    }),
 1762                    pagination = new
 1763                    {
 1764                        currentPage = page,
 1765                        pageSize,
 1766                        totalPages,
 1767                        totalItems
 1768                    }
 1769                });
 1770            }
 1771            catch (Exception ex)
 1772            {
 1773                return Json(new { success = false, message = $"Ошибка получения статистики: {ex.Message}" });
 1774            }
 1775        }
 1776
 1777        /// <summary>
 1778        /// API: Получить записи трекинга времени по чату
 1779        /// </summary>
 1780        [HttpGet]
 1781        public async Task<IActionResult> GetResponseTimes(string chatId)
 1782        {
 1783            try
 1784            {
 1785                if (string.IsNullOrEmpty(chatId))
 1786                    return Json(new { success = false, message = "Не указан ID чата" });
 1787
 1788                var records = await _chatTimeService.GetByChatIdAsync(chatId);
 1789                var list = records.Select(r => new
 1790                {
 1791                    r.Id,
 1792                    r.ChatId,
 1793                    r.ClientId,
 1794                    userName = r.UserName,
 1795                    minutes = r.Minutes,
 1796                    recordedAt = ToMoscowTime(r.RecordedAt).ToString("yyyy-MM-dd HH:mm:ss")
 1797                }).ToList();
 1798
 1799                return Json(new { success = true, times = list });
 1800            }
 1801            catch (Exception ex)
 1802            {
 1803                return Json(new { success = false, message = ex.Message });
 1804            }
 1805        }
 1806
 1807        /// <summary>
 1808        /// API: Добавить запись времени ответа
 1809        /// </summary>
 1810        [HttpPost]
 1811        public async Task<IActionResult> AddResponseTime([FromBody] AddResponseTimeRequest request)
 1812        {
 1813            try
 1814            {
 1815                if (request == null || string.IsNullOrEmpty(request.ChatId) || request.ClientId <= 0 || request.Minutes 
 1816                    return Json(new { success = false, message = "Некорректные параметры запроса" });
 1817
 1818                var record = new OzonDomains.Models.OzonChatResponseTime
 1819                {
 1820                    ChatId = request.ChatId,
 1821                    ClientId = request.ClientId,
 1822                    UserName = !string.IsNullOrEmpty(User.Identity?.Name) ? User.Identity.Name : "Unknown",
 1823                    Minutes = request.Minutes,
 1824                    RecordedAt = DateTime.UtcNow,
 1825                    CreatedAt = DateTime.UtcNow
 1826                };
 1827
 1828                await _chatTimeService.Add(record);
 1829
 1830                return Json(new { success = true, recordId = record.Id });
 1831            }
 1832            catch (Exception ex)
 1833            {
 1834                return Json(new { success = false, message = ex.Message });
 1835            }
 1836        }
 1837
 1838        /// <summary>
 1839        /// DTO для добавления времени ответа
 1840        /// </summary>
 1841        public class AddResponseTimeRequest
 1842        {
 1843            public string ChatId { get; set; } = string.Empty;
 1844            public int ClientId { get; set; }
 1845            public int Minutes { get; set; }
 1846        }
 1847
 1848        /// <summary>
 1849        /// API: получить статистику времени (ручной трекинг)
 1850        /// </summary>
 1851        [HttpGet]
 1852        public async Task<IActionResult> GetResponseTimeStats(int clientId = 0, DateTime? from = null,
 1853            DateTime? to = null, int page = 1, int pageSize = 50)
 1854        {
 1855            try
 1856            {
 1857                if (page < 1) page = 1;
 1858                pageSize = Math.Clamp(pageSize, 1, 100);
 1859
 1860                var dateFrom = from ?? DateTime.UtcNow.AddDays(-30);
 1861                var dateTo = to.HasValue
 1862                    ? to.Value.Date.AddDays(1).AddTicks(-1)
 1863                    : DateTime.UtcNow.AddDays(1);
 1864
 1865                List<OzonChatResponseTime> records;
 1866                if (clientId > 0)
 1867                {
 1868                    records = await _chatTimeService.GetByClientIdAsync(clientId);
 1869                }
 1870                else
 1871                {
 1872                    records = new List<OzonChatResponseTime>();
 1873                    var clients = await _ozonClientServices.GetOzonClients();
 1874                    foreach (var c in clients.Where(c => c.ClientType == ClientType.OZON))
 1875                    {
 1876                        var r = await _chatTimeService.GetByClientIdAsync(c.Id);
 1877                        records.AddRange(r);
 1878                    }
 1879                }
 1880
 1881                // фильтруем по диапазону дат
 1882                records = records
 1883                    .Where(r => r.RecordedAt >= dateFrom && r.RecordedAt <= dateTo)
 1884                    .OrderByDescending(r => r.RecordedAt)
 1885                    .ToList();
 1886
 1887                var totalItems = records.Count;
 1888                var totalMinutes = records.Sum(r => r.Minutes);
 1889                var avgMinutes = totalItems > 0 ? (double)totalMinutes / totalItems : 0;
 1890
 1891                var paged = records.Skip((page - 1) * pageSize).Take(pageSize).ToList();
 1892
 1893                return Json(new
 1894                {
 1895                    success = true,
 1896                    summary = new
 1897                    {
 1898                        totalRecords = totalItems,
 1899                        totalMinutes,
 1900                        avgMinutes = Math.Round(avgMinutes, 1),
 1901                        dateFrom = dateFrom.ToString("dd.MM.yyyy"),
 1902                        dateTo = dateTo.ToString("dd.MM.yyyy")
 1903                    },
 1904                    items = paged.Select(r => new
 1905                    {
 1906                        r.Id,
 1907                        r.ChatId,
 1908                        r.ClientId,
 1909                        userName = r.UserName,
 1910                        minutes = r.Minutes,
 1911                        recordedAt = ToMoscowTime(r.RecordedAt).ToString("yyyy-MM-dd HH:mm"),
 1912                        clientName = r.Client?.Name
 1913                    }),
 1914                    pagination = new
 1915                    {
 1916                        currentPage = page,
 1917                        pageSize,
 1918                        totalPages = totalItems > 0 ? (int)Math.Ceiling((double)totalItems / pageSize) : 1,
 1919                        totalItems
 1920                    }
 1921                });
 1922            }
 1923            catch (Exception ex)
 1924            {
 1925                return Json(new { success = false, message = ex.Message });
 1926            }
 1927        }
 1928
 1929        /// <summary>
 1930        /// Форматирование секунд в читаемый вид
 1931        /// </summary>
 1932        private static string FormatSeconds(int totalSeconds)
 1933        {
 1934            if (totalSeconds < 60)
 1935                return $"{totalSeconds} сек";
 1936            if (totalSeconds < 3600)
 1937                return $"{totalSeconds / 60} мин {totalSeconds % 60} сек";
 1938
 1939            var hours = totalSeconds / 3600;
 1940            var minutes = (totalSeconds % 3600) / 60;
 1941            return $"{hours} ч {minutes} мин";
 1942        }
 1943
 1944        /// <summary>
 1945        /// Конвертация времени UTC в московское время (UTC+3)
 1946        /// </summary>
 1947        private static DateTime ToMoscowTime(DateTime utcTime)
 1948        {
 1949            if (utcTime.Kind == DateTimeKind.Local)
 1950                utcTime = utcTime.ToUniversalTime();
 1951            else if (utcTime.Kind == DateTimeKind.Unspecified)
 1952                utcTime = DateTime.SpecifyKind(utcTime, DateTimeKind.Utc);
 1953
 1954            return TimeZoneInfo.ConvertTimeFromUtc(utcTime, MoscowTimeZone);
 1955        }
 1956
 1957        /// <summary>
 1958        /// Конвертация nullable времени UTC в московское время
 1959        /// </summary>
 1960        private static DateTime? ToMoscowTime(DateTime? utcTime)
 1961        {
 1962            return utcTime.HasValue ? ToMoscowTime(utcTime.Value) : null;
 1963        }
 1964    }
 1965
 1966    /// <summary>
 1967    /// DTO для запроса отметки сообщений как прочитанных
 1968    /// </summary>
 1969    public class MarkAsReadRequest
 1970    {
 1971        public int ClientId { get; set; }
 1972        public string ChatId { get; set; } = string.Empty;
 1973        public long? FromMessageId { get; set; }
 1974    }
 1975
 1976    /// <summary>
 1977    /// DTO для запроса отправки сообщения
 1978    /// </summary>
 1979    public class SendMessageRequest
 1980    {
 01981        public int ClientId { get; set; }
 01982        public string ChatId { get; set; } = string.Empty;
 01983        public string Text { get; set; } = string.Empty;
 1984    }
 1985
 1986    /// <summary>
 1987    /// DTO для запроса отправки файла
 1988    /// </summary>
 1989    public class SendFileRequest
 1990    {
 1991        public int ClientId { get; set; }
 1992        public string ChatId { get; set; } = string.Empty;
 1993        public string Base64Content { get; set; } = string.Empty;
 1994        public string FileName { get; set; } = string.Empty;
 1995    }
 1996}