using foodmarket.Application.Common.Tenancy;
using foodmarket.Domain.Catalog;
using foodmarket.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace foodmarket.Infrastructure.Integrations.MoySklad;

public record MoySkladImportResult(
    int Total,
    int Created,
    int Skipped,
    int GroupsCreated,
    IReadOnlyList<string> Errors);

public class MoySkladImportService
{
    private readonly MoySkladClient _client;
    private readonly AppDbContext _db;
    private readonly ITenantContext _tenant;
    private readonly ILogger<MoySkladImportService> _log;

    public MoySkladImportService(
        MoySkladClient client,
        AppDbContext db,
        ITenantContext tenant,
        ILogger<MoySkladImportService> log)
    {
        _client = client;
        _db = db;
        _tenant = tenant;
        _log = log;
    }

    public Task<MoySkladApiResult<MsOrganization>> TestConnectionAsync(string token, CancellationToken ct)
        => _client.WhoAmIAsync(token, ct);

    public async Task<MoySkladImportResult> ImportCounterpartiesAsync(
        string token,
        bool overwriteExisting,
        CancellationToken ct,
        ImportJobProgress? progress = null,
        Guid? organizationIdOverride = null)
    {
        var orgId = organizationIdOverride ?? _tenant.OrganizationId
            ?? throw new InvalidOperationException("No tenant organization in context.");

        // MoySklad НЕ имеет поля "Поставщик/Покупатель" у контрагентов вообще —
        // counterparty entity содержит только group (группа доступа), tags
        // (произвольные), state (пользовательская цепочка статусов), companyType
        // (legal/individual/entrepreneur). Никакого role/kind. Поэтому у нас тоже
        // этого поля нет — пусть пользователь сам решит.

        static foodmarket.Domain.Catalog.CounterpartyType ResolveType(string? companyType)
            => companyType switch
            {
                "individual" or "entrepreneur" => foodmarket.Domain.Catalog.CounterpartyType.Individual,
                _ => foodmarket.Domain.Catalog.CounterpartyType.LegalEntity,
            };

        // Загружаем существующих в память — обновлять будем по имени (case-insensitive).
        // Раньше при overwriteExisting=true бага: пропускалась проверка "skip" и ВСЕ
        // поступающие записи добавлялись как новые, порождая дубли. Теперь если имя уже
        // есть — обновляем ту же запись, иначе создаём.
        var existingByName = await _db.Counterparties
            .ToDictionaryAsync(c => c.Name, c => c, StringComparer.OrdinalIgnoreCase, ct);

        var created = 0;
        var updated = 0;
        var skipped = 0;
        var total = 0;
        var errors = new List<string>();
        var batch = 0;

        await foreach (var c in _client.StreamCounterpartiesAsync(token, ct))
        {
            total++;
            if (progress is not null) progress.Total = total;
            // Архивных не пропускаем — импортируем как IsActive=false (см. ApplyCounterparty).

            try
            {
                if (existingByName.TryGetValue(c.Name, out var existing))
                {
                    if (!overwriteExisting) { skipped++; if (progress is not null) progress.Skipped = skipped; continue; }
                    ApplyCounterparty(existing, c, ResolveType);
                    updated++;
                    if (progress is not null) progress.Updated = updated;
                }
                else
                {
                    var entity = new foodmarket.Domain.Catalog.Counterparty { OrganizationId = orgId };
                    ApplyCounterparty(entity, c, ResolveType);
                    _db.Counterparties.Add(entity);
                    existingByName[c.Name] = entity;
                    created++;
                    if (progress is not null) progress.Created = created;
                }

                batch++;
                if (batch >= 100)
                {
                    await _db.SaveChangesAsync(ct);
                    batch = 0;
                }
            }
            catch (Exception ex)
            {
                _log.LogWarning(ex, "Failed to import counterparty {Name}", c.Name);
                errors.Add($"{c.Name}: {ex.Message}");
                if (progress is not null) progress.Errors = errors;
            }
        }

        if (batch > 0) await _db.SaveChangesAsync(ct);
        // `created` в отчёте = вставки + апдейты (чтобы не ломать UI, который знает только Created).
        return new MoySkladImportResult(total, created + updated, skipped, 0, errors);
    }

    private static void ApplyCounterparty(
        foodmarket.Domain.Catalog.Counterparty entity,
        MsCounterparty c,
        Func<string?, foodmarket.Domain.Catalog.CounterpartyType> resolveType)
    {
        entity.Name = Trim(c.Name, 255) ?? c.Name;
        entity.LegalName = Trim(c.LegalTitle, 500);
        entity.Type = resolveType(c.CompanyType);
        entity.Bin = Trim(c.Inn, 20);
        entity.TaxNumber = Trim(c.Kpp, 20);
        entity.Phone = Trim(c.Phone, 50);
        entity.Email = Trim(c.Email, 255);
        entity.Address = Trim(c.ActualAddress ?? c.LegalAddress, 500);
        entity.Notes = Trim(c.Description, 1000);
    }

    public async Task<MoySkladImportResult> ImportProductsAsync(
        string token,
        bool overwriteExisting,
        CancellationToken ct,
        ImportJobProgress? progress = null,
        Guid? organizationIdOverride = null)
    {
        var orgId = organizationIdOverride ?? _tenant.OrganizationId
            ?? throw new InvalidOperationException("No tenant organization in context.");

        // Дефолт VAT — из страны организации (Country.VatRate), decimal(5,2).
        var defaultVat = await _db.Countries
            .Where(c => c.Code == (_db.Organizations
                .Where(o => o.Id == orgId).Select(o => o.CountryCode).First()))
            .Select(c => (decimal?)c.VatRate).FirstOrDefaultAsync(ct) ?? 16m;
        var baseUnit = await _db.UnitsOfMeasure.FirstOrDefaultAsync(u => u.Code == "796", ct)
                       ?? await _db.UnitsOfMeasure.FirstAsync(ct);
        var retailType = await _db.PriceTypes.FirstOrDefaultAsync(p => p.IsRetail, ct)
                         ?? await _db.PriceTypes.FirstAsync(ct);
        var kzt = await _db.Currencies.FirstAsync(c => c.Code == "KZT", ct);

        var countriesByName = await _db.Countries
            .IgnoreQueryFilters()
            .ToDictionaryAsync(c => c.Name, c => c.Id, ct);

        // Дефолтная группа на случай, когда у товара в MoySklad нет productFolder.
        var defaultGroup = await _db.ProductGroups.FirstOrDefaultAsync(g => g.Name == "Продукты питания", ct);
        if (defaultGroup is null)
        {
            defaultGroup = new ProductGroup
            {
                OrganizationId = orgId,
                Name = "Продукты питания",
                Path = "Продукты питания",
            };
            _db.ProductGroups.Add(defaultGroup);
            await _db.SaveChangesAsync(ct);
        }
        var defaultGroupId = defaultGroup.Id;

        // Import folders first — build flat then link parents. Архивные тоже берём,
        // помечаем IsActive=false — у MoySklad у productfolder есть archived.
        var folders = await _client.GetAllFoldersAsync(token, ct);
        var localGroupByMsId = new Dictionary<string, Guid>();
        var groupsCreated = 0;
        foreach (var f in folders.OrderBy(f => f.PathName?.Length ?? 0))
        {
            if (f.Id is null) continue;
            var existing = await _db.ProductGroups.FirstOrDefaultAsync(
                g => g.Name == f.Name && g.Path == (f.PathName ?? f.Name), ct);
            if (existing is not null)
            {
                localGroupByMsId[f.Id] = existing.Id;
                continue;
            }
            var g = new ProductGroup
            {
                OrganizationId = orgId,
                Name = f.Name,
                Path = string.IsNullOrEmpty(f.PathName) ? f.Name : $"{f.PathName}/{f.Name}",
            };
            _db.ProductGroups.Add(g);
            localGroupByMsId[f.Id] = g.Id;
            groupsCreated++;
        }
        if (groupsCreated > 0) await _db.SaveChangesAsync(ct);
        if (progress is not null) progress.GroupsCreated = groupsCreated;

        // Import products
        var errors = new List<string>();
        var created = 0;
        var updated = 0;
        var skipped = 0;
        var total = 0;
        // При overwriteExisting=true загружаем товары целиком, чтобы обновлять существующие
        // вместо создания дубликатов. Ключ = артикул (нормализованный).
        var existingByArticle = await _db.Products
            .Where(p => p.Article != null)
            .ToDictionaryAsync(p => p.Article!, p => p, StringComparer.OrdinalIgnoreCase, ct);
        var existingBarcodeSet = new HashSet<string>(
            await _db.ProductBarcodes.Select(b => b.Code).ToListAsync(ct));

        await foreach (var p in _client.StreamProductsAsync(token, ct))
        {
            total++;
            if (progress is not null) progress.Total = total;
            // Архивных не пропускаем — импортируем как IsActive=false.

            var article = string.IsNullOrWhiteSpace(p.Article) ? p.Code : p.Article;
            var alreadyByArticle = !string.IsNullOrWhiteSpace(article) && existingByArticle.ContainsKey(article);

            if (alreadyByArticle && !overwriteExisting)
            {
                skipped++;
                if (progress is not null) progress.Skipped = skipped;
                continue;
            }

            try
            {
                // Vat товара: из MoySklad.vat если задано, иначе дефолт страны.
                // VatEnabled: приоритет p.VatEnabled, fallback — «без НДС» если p.Vat=0.
                var vat = p.Vat.HasValue ? (decimal)p.Vat.Value : defaultVat;
                var vatEnabled = p.VatEnabled ?? (p.Vat is null || p.Vat.Value != 0);
                Guid? groupId = p.ProductFolder?.Meta?.Href is { } href && TryExtractId(href) is { } msGroupId
                                && localGroupByMsId.TryGetValue(msGroupId, out var gId) ? gId : null;
                Guid? countryId = p.Country?.Name is { } cn && countriesByName.TryGetValue(cn, out var cId) ? cId : null;

                var retailPrice = p.SalePrices?.FirstOrDefault(sp => sp.PriceType?.Name?.Contains("Розничная", StringComparison.OrdinalIgnoreCase) == true)
                                  ?? p.SalePrices?.FirstOrDefault();

                Product product;
                if (alreadyByArticle && overwriteExisting)
                {
                    product = existingByArticle[article!];
                    // Обновляем только скалярные поля — коллекции (prices, barcodes) оставляем:
                    // там могут быть данные, которые редактировал пользователь после импорта.
                    product.Name = Trim(p.Name, 500);
                    product.Article = Trim(article, 500);
                    product.Description = p.Description;
                    product.Vat = vat;
                    product.VatEnabled = vatEnabled;
                    product.ProductGroupId = groupId ?? product.ProductGroupId;
                    product.CountryOfOriginId = countryId ?? product.CountryOfOriginId;
                    product.Packaging = p.Weighed ? Packaging.Weight : Packaging.Piece;
                    product.IsMarked = !string.IsNullOrEmpty(p.TrackingType) && p.TrackingType != "NOT_TRACKED";
                    product.ReferencePrice = p.BuyPrice is null ? product.ReferencePrice : p.BuyPrice.Value / 100m;
                    updated++;
                    if (progress is not null) progress.Updated = updated;
                }
                else
                {
                    product = new Product
                    {
                        OrganizationId = orgId,
                        Name = Trim(p.Name, 500),
                        Article = Trim(article, 500),
                        Description = p.Description,
                        UnitOfMeasureId = baseUnit.Id,
                        Vat = vat,
                        VatEnabled = vatEnabled,
                        ProductGroupId = groupId ?? defaultGroupId,
                        CountryOfOriginId = countryId,
                        Packaging = p.Weighed ? Packaging.Weight : Packaging.Piece,
                        IsMarked = !string.IsNullOrEmpty(p.TrackingType) && p.TrackingType != "NOT_TRACKED",
                        ReferencePrice = p.BuyPrice is null ? null : p.BuyPrice.Value / 100m,
                        PurchaseCurrencyId = kzt.Id,
                    };

                    if (retailPrice is not null)
                    {
                        product.Prices.Add(new ProductPrice
                        {
                            OrganizationId = orgId,
                            PriceTypeId = retailType.Id,
                            Amount = retailPrice.Value / 100m,
                            CurrencyId = kzt.Id,
                        });
                    }

                    foreach (var b in ExtractBarcodes(p))
                    {
                        if (existingBarcodeSet.Contains(b.Code))
                        {
                            errors.Add($"{p.Name}: штрихкод {b.Code} уже занят, пропущен.");
                            if (progress is not null) progress.Errors = errors;
                            continue;
                        }
                        product.Barcodes.Add(b);
                        existingBarcodeSet.Add(b.Code);
                    }

                    _db.Products.Add(product);
                    if (!string.IsNullOrWhiteSpace(article)) existingByArticle[article] = product;
                    created++;
                    if (progress is not null) progress.Created = created;
                }

                // Flush чаще (каждые 100) чтобы при сетевом обрыве на следующей странице
                // мы сохранили как можно больше и смогли безопасно продолжить с overwrite.
                if ((created + updated) % 100 == 0) await _db.SaveChangesAsync(ct);
            }
            catch (Exception ex)
            {
                _log.LogWarning(ex, "Failed to import MoySklad product {Name}", p.Name);
                errors.Add($"{p.Name}: {ex.Message}");
                if (progress is not null) progress.Errors = errors;
            }
        }

        await _db.SaveChangesAsync(ct);

        // Финальная проверка дубликатов штрихкодов (исторические записи или
        // расхождения c уникальным индексом). Только warning в errors[].
        var duplicates = await _db.ProductBarcodes
            .GroupBy(b => b.Code)
            .Where(g => g.Count() > 1)
            .Select(g => g.Key)
            .ToListAsync(ct);
        foreach (var dup in duplicates)
            errors.Add($"Внимание: штрихкод {dup} привязан к нескольким товарам — почисти вручную.");
        if (progress is not null && duplicates.Count > 0) progress.Errors = errors;

        return new MoySkladImportResult(total, created + updated, skipped, groupsCreated, errors);
    }

    private static List<ProductBarcode> ExtractBarcodes(MsProduct p)
    {
        if (p.Barcodes is null) return [];
        var list = new List<ProductBarcode>();
        var primarySet = false;
        foreach (var entry in p.Barcodes)
        {
            foreach (var (kind, code) in entry)
            {
                if (string.IsNullOrWhiteSpace(code)) continue;
                var type = kind switch
                {
                    "ean13" => BarcodeType.Ean13,
                    "ean8" => BarcodeType.Ean8,
                    "code128" => BarcodeType.Code128,
                    "gtin" => BarcodeType.Ean13,
                    "upca" => BarcodeType.Upca,
                    "upce" => BarcodeType.Upce,
                    _ => BarcodeType.Other,
                };
                list.Add(new ProductBarcode { Code = code.Length > 500 ? code[..500] : code, Type = type, IsPrimary = !primarySet });
                primarySet = true;
            }
        }
        return list;
    }

    private static string? Trim(string? s, int max)
        => string.IsNullOrEmpty(s) ? s : (s.Length <= max ? s : s[..max]);

    private static string? TryExtractId(string href)
    {
        // href like "https://api.moysklad.ru/api/remap/1.2/entity/productfolder/<guid>"
        var lastSlash = href.LastIndexOf('/');
        return lastSlash >= 0 ? href[(lastSlash + 1)..] : null;
    }
}
