using System.Globalization;
using CartWise.Application.DTOs;
using CartWise.Application.Interfaces;
using CartWise.Application.Results;
using CartWise.Domain.Entities;
using CartWise.Domain.Enums;
using Microsoft.EntityFrameworkCore;

namespace CartWise.Application.Services;

public class ProductService : IProductService
{
    private readonly IApplicationDbContext _db;
    private readonly IEnumerable<IProductDataProvider> _providers;
    private readonly TimeProvider _timeProvider;

    public ProductService(IApplicationDbContext db, IEnumerable<IProductDataProvider> providers, TimeProvider timeProvider)
    {
        _db = db;
        _providers = providers;
        _timeProvider = timeProvider;
    }

    public async Task<IReadOnlyList<ProductSummaryDto>> SearchAsync(string query, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(query))
        {
            return [];
        }

        var normalizedQuery = query.Trim().ToUpperInvariant();

        var rows = await (
            from product in _db.Products
            join brand in _db.Brands on product.BrandId equals brand.BrandId into brandJoin
            from brand in brandJoin.DefaultIfEmpty()
            where product.NormalizedName.Contains(normalizedQuery)
            orderby product.Name
            select new { product, BrandName = brand != null ? brand.Name : null }
        )
        .AsNoTracking()
        .Take(25)
        .ToListAsync(cancellationToken);

        return rows.Select(row => new ProductSummaryDto
        {
            ProductId = row.product.ProductId,
            Name = row.product.Name,
            BrandName = row.BrandName,
            PackageDisplay = FormatPackage(row.product.SizeValue, row.product.SizeUnit),
            ImageUrl = row.product.ImageUrl
        }).ToList();
    }

    public async Task<ProductDetailsDto?> GetDetailsAsync(Guid productId, CancellationToken cancellationToken = default)
    {
        var product = await _db.Products
            .Include(p => p.Identifiers)
            .AsNoTracking()
            .FirstOrDefaultAsync(p => p.ProductId == productId, cancellationToken);

        if (product is null)
        {
            return null;
        }

        return await ToDetailsDtoAsync(product, cancellationToken);
    }

    public async Task<Result<ProductDetailsDto>> ResolveBarcodeAsync(string barcode, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(barcode))
        {
            return Result<ProductDetailsDto>.Failure("Barcode is required.");
        }

        var normalizedBarcode = ProductIdentifier.Normalize(barcode);

        var localProduct = await _db.ProductIdentifiers
            .Where(i => i.NormalizedValue == normalizedBarcode)
            .Join(_db.Products.Include(p => p.Identifiers), i => i.ProductId, p => p.ProductId, (i, p) => p)
            .AsNoTracking()
            .FirstOrDefaultAsync(cancellationToken);

        if (localProduct is not null)
        {
            var localDetails = await ToDetailsDtoAsync(localProduct, cancellationToken);
            return Result<ProductDetailsDto>.Success(localDetails);
        }

        foreach (var provider in _providers)
        {
            var lookup = await provider.FindByBarcodeAsync(normalizedBarcode, cancellationToken);
            if (lookup is null)
            {
                continue;
            }

            var product = await PersistImportedProductAsync(lookup, normalizedBarcode, provider.Name, cancellationToken);
            var details = await ToDetailsDtoAsync(product, cancellationToken);
            return Result<ProductDetailsDto>.Success(details);
        }

        return Result<ProductDetailsDto>.Failure("No product was found for that barcode.");
    }

    private async Task<Product> PersistImportedProductAsync(
        ProductLookupResult lookup,
        string normalizedBarcode,
        string providerName,
        CancellationToken cancellationToken)
    {
        Guid? brandId = null;
        if (!string.IsNullOrWhiteSpace(lookup.BrandName))
        {
            var normalizedBrandName = lookup.BrandName.Trim().ToUpperInvariant();
            brandId = await _db.Brands
                .Where(b => b.NormalizedName == normalizedBrandName)
                .Select(b => (Guid?)b.BrandId)
                .FirstOrDefaultAsync(cancellationToken);

            if (brandId is null)
            {
                var brand = new Brand(lookup.BrandName);
                _db.Brands.Add(brand);
                brandId = brand.BrandId;
            }
        }

        var product = new Product(
            lookup.Name,
            MapSourceType(providerName),
            _timeProvider.GetUtcNow().UtcDateTime,
            brandId: brandId,
            sizeValue: lookup.SizeValue,
            sizeUnit: lookup.SizeUnit,
            imageUrl: lookup.ImageUrl,
            sourceExternalId: lookup.SourceExternalId);

        product.AddIdentifier(ProductIdentifierType.Gtin, normalizedBarcode);

        _db.Products.Add(product);
        foreach (var identifier in product.Identifiers)
        {
            _db.ProductIdentifiers.Add(identifier);
        }

        await _db.SaveChangesAsync(cancellationToken);

        return product;
    }

    private async Task<ProductDetailsDto> ToDetailsDtoAsync(Product product, CancellationToken cancellationToken)
    {
        var brandName = product.BrandId is null
            ? null
            : await _db.Brands.AsNoTracking().Where(b => b.BrandId == product.BrandId).Select(b => b.Name).FirstOrDefaultAsync(cancellationToken);

        var conceptName = product.GroceryConceptId is null
            ? null
            : await _db.GroceryConcepts.AsNoTracking().Where(c => c.GroceryConceptId == product.GroceryConceptId).Select(c => c.Name).FirstOrDefaultAsync(cancellationToken);

        return new ProductDetailsDto
        {
            ProductId = product.ProductId,
            Name = product.Name,
            BrandName = brandName,
            ConceptName = conceptName,
            PackageDisplay = FormatPackage(product.SizeValue, product.SizeUnit),
            ImageUrl = product.ImageUrl,
            BarcodeIdentifiers = product.Identifiers.Select(i => i.Value).ToList()
        };
    }

    private static ProductSourceType MapSourceType(string providerName) => providerName switch
    {
        "OpenFoodFacts" => ProductSourceType.OpenFoodFacts,
        "USDA" => ProductSourceType.Usda,
        _ => ProductSourceType.Manual
    };

    private static string? FormatPackage(decimal? sizeValue, string? sizeUnit)
    {
        if (sizeValue is null)
        {
            return sizeUnit;
        }

        var text = sizeValue.Value.ToString("0.###", CultureInfo.InvariantCulture);
        return sizeUnit is null ? text : $"{text} {sizeUnit}";
    }
}
