using System.Text.Json;
using Meezi.API.Models.Menu;
using Meezi.API.Models.Orders;
using Meezi.API.Models.Public;
using Meezi.Core.Constants;
using Meezi.Core.Discover;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Infrastructure.Data;
using Meezi.API.Security;
using Meezi.Infrastructure.Discover;
using Microsoft.EntityFrameworkCore;

namespace Meezi.API.Services;

public interface IPublicService
{
    Task<IReadOnlyList<CafeDiscoverDto>> DiscoverAsync(
        DiscoverFilterParams filters,
        CancellationToken cancellationToken = default);
    Task<CafePublicDto?> GetCafeAsync(string slug, CancellationToken cancellationToken = default);
    Task<PublicMenuDto?> GetMenuAsync(string slug, CancellationToken cancellationToken = default);
    Task<(GuestOrderPlacedDto? Data, string? ErrorCode, string? ErrorMessage)> PlaceOrderAsync(
        string slug,
        GuestCreateOrderRequest request,
        CancellationToken cancellationToken = default);
    Task<OrderTrackDto?> TrackOrderAsync(
        string orderId,
        string trackingToken,
        CancellationToken cancellationToken = default);
    Task<(ReservationDto? Data, string? ErrorCode, string? Message)> CreateReservationAsync(
        string slug,
        CreateReservationRequest request,
        CancellationToken cancellationToken = default);
    Task<PublicMenuDto?> GetBranchMenuAsync(
        string cafeId,
        string branchId,
        CancellationToken cancellationToken = default);
    Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync(
        string cafeId,
        string branchId,
        PlaceGuestOrderRequest request,
        CancellationToken cancellationToken = default);
}

public class PublicService : IPublicService
{
    private readonly AppDbContext _db;
    private readonly IOrderService _orders;
    private readonly IReviewService _reviews;
    private readonly IKdsNotifier _kdsNotifier;
    private readonly IBranchMenuService _branchMenu;
    private readonly IBranchIdentityService _identity;
    private readonly IAbuseProtectionService _abuse;
    private readonly IHttpContextAccessor _http;

    public PublicService(
        AppDbContext db,
        IOrderService orders,
        IReviewService reviews,
        IKdsNotifier kdsNotifier,
        IBranchMenuService branchMenu,
        IBranchIdentityService identity,
        IAbuseProtectionService abuse,
        IHttpContextAccessor http)
    {
        _db = db;
        _orders = orders;
        _reviews = reviews;
        _kdsNotifier = kdsNotifier;
        _branchMenu = branchMenu;
        _identity = identity;
        _abuse = abuse;
        _http = http;
    }

    public Task<IReadOnlyList<CafeDiscoverDto>> DiscoverAsync(
        DiscoverFilterParams filters,
        CancellationToken cancellationToken = default) =>
        _reviews.DiscoverAsync(filters, cancellationToken);

    public async Task<CafePublicDto?> GetCafeAsync(string slug, CancellationToken cancellationToken = default)
    {
        var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Slug == slug, cancellationToken);
        if (cafe is null) return null;

        var (avg, count) = await _reviews.GetRatingSummaryAsync(cafe.Id, cancellationToken);

        var profile = CafeDiscoverProfileSerializer.Deserialize(cafe.DiscoverProfileJson);
        var badges = DiscoverBadgeMapping.ToDtos(cafe)
            .Select(b => new CafeBadgePublicDto(b.Key, b.Label, b.Icon))
            .ToList();

        var gallery = DeserializeStringList(cafe.GalleryJson);
        var hours   = DeserializeHours(cafe.WorkingHoursJson);

        return new CafePublicDto(
            cafe.Id,
            cafe.Name,
            cafe.NameAr,
            cafe.NameEn,
            cafe.Slug,
            cafe.City,
            cafe.Address,
            cafe.Phone,
            cafe.LogoUrl,
            cafe.CoverImageUrl,
            cafe.Description,
            cafe.IsVerified,
            avg,
            count,
            CafeDiscoverProfileMapping.ToDto(profile),
            badges,
            gallery,
            hours?.IsOpenNow() ?? false,
            cafe.InstagramHandle,
            cafe.WebsiteUrl,
            ToHoursDto(hours));
    }

    private static readonly JsonSerializerOptions _jsonOpts = new()
    {
        PropertyNameCaseInsensitive = true,
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };

    private static IReadOnlyList<string> DeserializeStringList(string? json)
    {
        if (string.IsNullOrWhiteSpace(json)) return [];
        try { return JsonSerializer.Deserialize<List<string>>(json, _jsonOpts) ?? []; }
        catch { return []; }
    }

    private static WorkingHoursSchedule? DeserializeHours(string? json)
    {
        if (string.IsNullOrWhiteSpace(json)) return null;
        try { return JsonSerializer.Deserialize<WorkingHoursSchedule>(json, _jsonOpts); }
        catch { return null; }
    }

    private static WorkingHoursPublicDto? ToHoursDto(WorkingHoursSchedule? h)
    {
        if (h is null) return null;
        DaySchedulePublicDto? Map(DaySchedule? d) =>
            d is null ? null : new DaySchedulePublicDto(d.IsOpen, d.Open, d.Close);
        return new WorkingHoursPublicDto(Map(h.Sat), Map(h.Sun), Map(h.Mon), Map(h.Tue), Map(h.Wed), Map(h.Thu), Map(h.Fri));
    }

    public async Task<PublicMenuDto?> GetMenuAsync(string slug, CancellationToken cancellationToken = default)
    {
        var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Slug == slug, cancellationToken);
        if (cafe is null) return null;

        var categories = await _db.MenuCategories
            .Where(c => c.CafeId == cafe.Id && c.IsActive)
            .OrderBy(c => c.SortOrder)
            .ToListAsync(cancellationToken);

        var items = await _db.MenuItems
            .Include(i => i.Category)
            .Where(i => i.CafeId == cafe.Id && i.IsAvailable)
            .ToListAsync(cancellationToken);

        var grouped = categories
            .Select(cat => new PublicMenuCategoryDto(
                cat.Id,
                cat.Name,
                cat.NameAr,
                cat.NameEn,
                cat.Icon,
                cat.IconPresetId,
                cat.IconStyle,
                cat.ImageUrl,
                items
                    .Where(i => i.CategoryId == cat.Id)
                    .Select(i => new PublicMenuItemDto(
                        i.Id,
                        i.CategoryId,
                        i.Name,
                        i.NameAr,
                        i.NameEn,
                        i.Description,
                        i.Price,
                        i.DiscountPercent > 0 ? i.DiscountPercent : cat.DiscountPercent,
                        MenuItemImageDefaults.ResolveDisplayImageUrl(i),
                        i.VideoUrl,
                        i.Model3dUrl,
                        i.IsAvailable))
                    .ToList()))
            .Where(c => c.Items.Count > 0)
            .ToList();

        return new PublicMenuDto(cafe.Id, cafe.Name, cafe.Slug, CafeThemeMapping.FromJson(cafe.ThemeJson), grouped);
    }

    public async Task<(GuestOrderPlacedDto? Data, string? ErrorCode, string? ErrorMessage)> PlaceOrderAsync(
        string slug,
        GuestCreateOrderRequest request,
        CancellationToken cancellationToken = default)
    {
        var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Slug == slug, cancellationToken);
        if (cafe is null) return (null, "NOT_FOUND", "Cafe not found.");

        var traffic = await GuardPublicWriteAsync(cafe, request.CaptchaToken, guestOrder: true, cancellationToken);
        if (!traffic.Ok) return (null, traffic.ErrorCode, traffic.Message);

        var maxOrders = PlanLimits.MaxOrdersPerDay(cafe.PlanTier);
        if (maxOrders != int.MaxValue)
        {
            var todayStart = DateTime.UtcNow.Date;
            var count = await _db.Orders.CountAsync(
                o => o.CafeId == cafe.Id && o.CreatedAt >= todayStart,
                cancellationToken);
            if (count >= maxOrders)
                return (null, "PLAN_LIMIT_REACHED", "This cafe has reached its daily order limit.");
        }

        string? couponId = null;
        if (!string.IsNullOrWhiteSpace(request.CouponCode))
        {
            var coupon = await _db.Coupons.FirstOrDefaultAsync(
                c => c.CafeId == cafe.Id && c.Code == request.CouponCode && c.IsActive,
                cancellationToken);
            couponId = coupon?.Id;
        }

        var order = await _orders.CreateGuestOrderAsync(
            cafe.Id,
            new CreateOrderRequest(
                request.OrderType,
                null,
                request.TableId,
                null,
                request.GuestName,
                request.GuestPhone,
                null,
                couponId,
                request.Items),
            request.GuestPhone,
            request.GuestName,
            cancellationToken);

        if (order is null)
            return (null, "VALIDATION_ERROR", "Could not place order. Check menu items and table.");

        return (new GuestOrderPlacedDto(order.Id, order.Status, order.Total, order.TableNumber), null, null);
    }

    public async Task<OrderTrackDto?> TrackOrderAsync(
        string orderId,
        string trackingToken,
        CancellationToken cancellationToken = default)
    {
        var order = await _db.Orders
            .Include(o => o.Items)
            .ThenInclude(i => i.MenuItem)
            .Include(o => o.Table)
            .FirstOrDefaultAsync(
                o => o.Id == orderId && o.GuestTrackingToken == trackingToken,
                cancellationToken);

        if (order is null) return null;

        return OrderTrackingHelper.BuildTrackDto(order);
    }

    public async Task<(ReservationDto? Data, string? ErrorCode, string? Message)> CreateReservationAsync(
        string slug,
        CreateReservationRequest request,
        CancellationToken cancellationToken = default)
    {
        var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Slug == slug, cancellationToken);
        if (cafe is null) return (null, "NOT_FOUND", "Cafe not found.");

        var traffic = await GuardPublicWriteAsync(cafe, request.CaptchaToken, guestOrder: false, cancellationToken);
        if (!traffic.Ok) return (null, traffic.ErrorCode, traffic.Message);

        var entity = new TableReservation
        {
            CafeId = cafe.Id,
            TableId = request.TableId,
            GuestName = request.GuestName,
            GuestPhone = request.GuestPhone,
            Date = request.Date,
            Time = request.Time,
            PartySize = request.PartySize,
            Notes = request.Notes,
            Status = ReservationStatus.Pending
        };

        _db.TableReservations.Add(entity);
        await _db.SaveChangesAsync(cancellationToken);

        if (!string.IsNullOrEmpty(entity.TableId))
            await _kdsNotifier.NotifyTableStatusChangedAsync(cafe.Id, cancellationToken);

        var loaded = await _db.TableReservations
            .Include(r => r.Table)
            .FirstAsync(r => r.Id == entity.Id, cancellationToken);

        return (ToReservationDto(loaded), null, null);
    }

    public async Task<PublicMenuDto?> GetBranchMenuAsync(
        string cafeId,
        string branchId,
        CancellationToken cancellationToken = default)
    {
        var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken);
        if (cafe is null) return null;

        var branchMenu = await _branchMenu.GetBranchMenuAsync(
            cafeId,
            branchId,
            includeUnavailable: false,
            cancellationToken);
        if (branchMenu is null) return null;

        var categories = await _db.MenuCategories
            .Where(c => c.CafeId == cafeId && c.IsActive)
            .OrderBy(c => c.SortOrder)
            .ToListAsync(cancellationToken);

        var categoryById = categories.ToDictionary(c => c.Id);
        var grouped = branchMenu
            .Where(i => i.IsAvailable)
            .GroupBy(i => i.CategoryId)
            .Select(g =>
            {
                categoryById.TryGetValue(g.Key, out var cat);
                return new PublicMenuCategoryDto(
                    g.Key,
                    cat?.Name ?? "",
                    cat?.NameAr,
                    cat?.NameEn,
                    cat?.Icon,
                    cat?.IconPresetId,
                    cat?.IconStyle,
                    cat?.ImageUrl,
                    g.Select(i => new PublicMenuItemDto(
                        i.Id,
                        i.CategoryId,
                        i.Name,
                        i.NameAr,
                        i.NameEn,
                        i.Description,
                        i.EffectivePrice,
                        i.DiscountPercent,
                        MenuItemImageDefaults.IsUsableImageUrl(i.ImageUrl)
                            ? i.ImageUrl!
                            : MenuItemImageDefaults.ResolveImageUrl(i.Id, i.CategoryId, null),
                        i.VideoUrl,
                        i.Model3dUrl,
                        true)).ToList());
            })
            .Where(c => c.Items.Count > 0)
            .OrderBy(c => categoryById.GetValueOrDefault(c.Id)?.SortOrder ?? 0)
            .ToList();

        return new PublicMenuDto(cafe.Id, cafe.Name, cafe.Slug, CafeThemeMapping.FromJson(cafe.ThemeJson), grouped);
    }

    public async Task<(GuestQrOrderPlacedDto? Data, string? ErrorCode, string? Message)> PlaceBranchGuestOrderAsync(
        string cafeId,
        string branchId,
        PlaceGuestOrderRequest request,
        CancellationToken cancellationToken = default)
    {
        var cafe = await _db.Cafes.FirstOrDefaultAsync(c => c.Id == cafeId, cancellationToken);
        if (cafe is null) return (null, "NOT_FOUND", "Cafe not found.");

        var traffic = await GuardPublicWriteAsync(cafe, request.CaptchaToken, guestOrder: true, cancellationToken);
        if (!traffic.Ok) return (null, traffic.ErrorCode, traffic.Message);

        var maxOrders = PlanLimits.MaxOrdersPerDay(cafe.PlanTier);
        if (maxOrders != int.MaxValue)
        {
            var todayStart = DateTime.UtcNow.Date;
            var count = await _db.Orders.CountAsync(
                o => o.CafeId == cafe.Id && o.CreatedAt >= todayStart,
                cancellationToken);
            if (count >= maxOrders)
                return (null, "PLAN_LIMIT_REACHED", "This cafe has reached its daily order limit.");
        }

        return await _orders.PlaceBranchGuestOrderAsync(cafeId, branchId, request, cancellationToken);
    }

    private async Task<(bool Ok, string? ErrorCode, string? Message)> GuardPublicWriteAsync(
        Cafe cafe,
        string? captchaToken,
        bool guestOrder,
        CancellationToken cancellationToken)
    {
        var availability = PublicCafeGuard.EnsureAcceptingPublicTraffic(cafe);
        if (!availability.Ok)
            return (false, availability.ErrorCode, availability.Message);

        var ctx = _http.HttpContext;
        if (ctx is null)
            return (true, null, null);

        var ip = ClientIpResolver.GetClientIp(ctx);
        var writeCheck = await _abuse.CheckPublicWriteByIpAsync(ip, cancellationToken);
        if (!writeCheck.Allowed)
            return (false, writeCheck.ErrorCode, writeCheck.Message);

        if (guestOrder)
        {
            var orderCheck = await _abuse.CheckGuestOrderAsync(cafe.Id, ip, cancellationToken);
            if (!orderCheck.Allowed)
                return (false, orderCheck.ErrorCode, orderCheck.Message);
        }

        var captcha = await _abuse.VerifyCaptchaAsync(captchaToken, cancellationToken);
        if (!captcha.Ok)
            return (false, captcha.ErrorCode, captcha.Message);

        return (true, null, null);
    }

    private static ReservationDto ToReservationDto(TableReservation r) =>
        ReservationService.Map(r);
}
