using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace WebClientTpos.Client.Services;

// EN: POS data service — all BFF requests are authenticated via httpOnly session cookie.
//     The browser automatically includes the cookie in same-origin requests (SEC-W-01 / FRONT-W-02).
//     No manual token attachment — eliminates the DefaultRequestHeaders race condition.
// VI: POS data service — tất cả request BFF được xác thực qua httpOnly session cookie.
//     Trình duyệt tự động đính kèm cookie cho các request cùng origin (SEC-W-01 / FRONT-W-02).
//     Không gắn token thủ công — loại bỏ race condition trên DefaultRequestHeaders.
public class PosDataService
{
    private readonly HttpClient _http;

    // EN: Read options — case-insensitive to handle camelCase responses from BFF/microservices.
    // VI: Options đọc — không phân biệt hoa thường để xử lý camelCase responses từ BFF/microservices.
    private static readonly JsonSerializerOptions _jsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    // EN: Write options — camelCase to match ASP.NET model binding defaults.
    // VI: Options ghi — camelCase để khớp với ASP.NET model binding mặc định.
    private static readonly JsonSerializerOptions _writeOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

    public PosDataService(HttpClient http)
    {
        _http = http;
    }

    // EN: Helper — create a per-request HttpRequestMessage to avoid DefaultRequestHeaders mutation.
    //     Auth is handled automatically by the browser via the httpOnly BFF session cookie.
    // VI: Helper — tạo HttpRequestMessage per-request để tránh mutation DefaultRequestHeaders.
    //     Auth được trình duyệt tự xử lý qua httpOnly BFF session cookie.
    private static HttpRequestMessage NewRequest(HttpMethod method, string url)
        => new(method, url);

    private static async Task<string> TryExtractError(HttpResponseMessage resp)
    {
        if ((int)resp.StatusCode == 401)
            return "Phiên đăng nhập hết hạn. Vui lòng đăng nhập lại.";
        try
        {
            var body = await resp.Content.ReadAsStringAsync();
            using var doc = JsonDocument.Parse(body);
            if (doc.RootElement.TryGetProperty("message", out var msg))
                return msg.GetString() ?? resp.StatusCode.ToString();
            if (doc.RootElement.TryGetProperty("detail", out var detail))
                return detail.GetString() ?? resp.StatusCode.ToString();
            if (doc.RootElement.TryGetProperty("title", out var title))
                return title.GetString() ?? resp.StatusCode.ToString();
            return body.Length > 200 ? $"Lỗi ({resp.StatusCode})" : body;
        }
        catch { return $"Lỗi ({resp.StatusCode})"; }
    }

    /// <summary>
    /// EN: Generic POST helper — sends JSON body to BFF endpoint via per-request HttpRequestMessage.
    ///     Uses per-request message (not DefaultRequestHeaders) to avoid race conditions (FRONT-W-02).
    /// VI: Helper POST chung — gửi JSON body đến BFF endpoint qua HttpRequestMessage per-request.
    ///     Dùng per-request message (không phải DefaultRequestHeaders) để tránh race condition (FRONT-W-02).
    /// </summary>
    public async Task<bool> PostAsync(string url, object body)
    {
        var request = NewRequest(HttpMethod.Post, url);
        request.Content = JsonContent.Create(body, options: _writeOptions);
        var resp = await _http.SendAsync(request);
        return resp.IsSuccessStatusCode;
    }

    /// <summary>
    /// EN: Generic GET helper — deserializes single object from BFF (handles ApiResponse envelope).
    /// VI: Helper GET chung — deserialize object đơn từ BFF (xử lý ApiResponse envelope).
    /// </summary>
    public Task<T?> GetFromApiAsync<T>(string url) where T : class
        => GetObjectFromApiAsync<T>(url);

    /// <summary>
    /// EN: Generic PUT helper — sends JSON body to BFF endpoint via per-request HttpRequestMessage.
    /// VI: Helper PUT chung — gửi JSON body đến BFF endpoint qua HttpRequestMessage per-request.
    /// </summary>
    public async Task<bool> PutAsync(string url, object body)
    {
        var request = NewRequest(HttpMethod.Put, url);
        request.Content = JsonContent.Create(body, options: _writeOptions);
        var resp = await _http.SendAsync(request);
        return resp.IsSuccessStatusCode;
    }

    /// <summary>
    /// EN: POST and deserialize response — sends JSON body and returns deserialized result.
    /// VI: POST và deserialize response — gửi JSON body và trả về kết quả đã deserialize.
    /// </summary>
    public async Task<T?> PostAndGetAsync<T>(string url, object body) where T : class
    {
        var request = NewRequest(HttpMethod.Post, url);
        request.Content = JsonContent.Create(body, options: _writeOptions);
        var resp = await _http.SendAsync(request);
        if (!resp.IsSuccessStatusCode) return null;
        var json = await resp.Content.ReadAsStringAsync();
        if (string.IsNullOrWhiteSpace(json)) return null;

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        if (root.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Object)
            return JsonSerializer.Deserialize<T>(data.GetRawText(), _jsonOptions);
        if (root.ValueKind == JsonValueKind.Object)
            return JsonSerializer.Deserialize<T>(json, _jsonOptions);
        return null;
    }

    /// <summary>
    /// EN: Robust list deserialization — standardized to { success, data, error } envelope first (FRONT-W-04),
    ///     with backward-compatible fallbacks for plain arrays, PagedResult, and legacy wrappers.
    ///     Uses per-request HttpRequestMessage to avoid DefaultRequestHeaders race condition (FRONT-W-02).
    /// VI: Deserialize list linh hoạt — ưu tiên envelope { success, data, error } (FRONT-W-04),
    ///     với fallback tương thích cho array thuần, PagedResult, và wrapper cũ.
    ///     Dùng HttpRequestMessage per-request để tránh race condition DefaultRequestHeaders (FRONT-W-02).
    /// </summary>
    public async Task<List<T>> GetListFromApiAsync<T>(string url)
    {
        var resp = await _http.SendAsync(NewRequest(HttpMethod.Get, url));
        if (!resp.IsSuccessStatusCode) return new();
        var json = await resp.Content.ReadAsStringAsync();
        if (string.IsNullOrWhiteSpace(json)) return new();

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;

        // Case 1: plain array [...] — check before any property lookups
        if (root.ValueKind == JsonValueKind.Array)
            return JsonSerializer.Deserialize<List<T>>(json, _jsonOptions) ?? new();

        // Case 0 (FRONT-W-04): { "success": false } — server explicitly signalled failure
        if (root.TryGetProperty("success", out var successProp) && successProp.ValueKind == JsonValueKind.False)
            return new();

        // Case 2: { "items": [...] } (PagedResult)
        if (root.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Array)
            return JsonSerializer.Deserialize<List<T>>(items.GetRawText(), _jsonOptions) ?? new();

        // Case 3: { "data": { "items": [...] } } (ApiResponse<PagedResult>)
        // Case 4: { "data": [...] } (ApiResponse<List> / standardized { success, data, error })
        if (root.TryGetProperty("data", out var data))
        {
            if (data.ValueKind == JsonValueKind.Object && data.TryGetProperty("items", out var dataItems) && dataItems.ValueKind == JsonValueKind.Array)
                return JsonSerializer.Deserialize<List<T>>(dataItems.GetRawText(), _jsonOptions) ?? new();
            if (data.ValueKind == JsonValueKind.Array)
                return JsonSerializer.Deserialize<List<T>>(data.GetRawText(), _jsonOptions) ?? new();
        }

        // Case 5: { "<anyKey>": [...] } — generic single-array-property wrapper (e.g. { "members": [...] })
        foreach (var prop in root.EnumerateObject())
        {
            if (prop.Value.ValueKind == JsonValueKind.Array)
                return JsonSerializer.Deserialize<List<T>>(prop.Value.GetRawText(), _jsonOptions) ?? new();
        }

        return new();
    }

    /// <summary>
    /// EN: Robust single-object deserialization — standardized to { success, data, error } envelope (FRONT-W-04),
    ///     with backward-compatible fallback for plain objects.
    ///     Uses per-request HttpRequestMessage (FRONT-W-02).
    /// VI: Deserialize đối tượng đơn linh hoạt — ưu tiên envelope { success, data, error } (FRONT-W-04),
    ///     với fallback tương thích cho object thuần.
    ///     Dùng HttpRequestMessage per-request (FRONT-W-02).
    /// </summary>
    private async Task<T?> GetObjectFromApiAsync<T>(string url) where T : class
    {
        var resp = await _http.SendAsync(NewRequest(HttpMethod.Get, url));
        if (!resp.IsSuccessStatusCode) return null;
        var json = await resp.Content.ReadAsStringAsync();
        if (string.IsNullOrWhiteSpace(json)) return null;

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;

        // Case 0 (FRONT-W-04): { "success": false } — server explicitly signalled failure
        if (root.TryGetProperty("success", out var successProp) && successProp.ValueKind == JsonValueKind.False)
            return null;

        // Case 1: { "data": {...} } (ApiResponse envelope / standardized { success, data, error })
        if (root.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Object)
            return JsonSerializer.Deserialize<T>(data.GetRawText(), _jsonOptions);

        // Case 2: plain object
        if (root.ValueKind == JsonValueKind.Object)
            return JsonSerializer.Deserialize<T>(json, _jsonOptions);

        return null;
    }

    public record ShopInfo(Guid Id, string Name, string Slug, string? Description, string? Phone, string? Email, string? Category, string? Status, Guid? MerchantId = null);
    public record ProductInfo(Guid Id, string Name, decimal Price, string? Sku, string? Description, string? CategoryName, int? DurationMinutes, Guid? CategoryId = null)
    {
        public string? Category => CategoryName;
    }
    public record CategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder, string? ImageUrl = null);
    public record TableInfo(Guid Id, string TableNumber, int Capacity, string? Zone, string Status, Guid? SessionId, int? GuestCount, DateTime? StartedAt, decimal HourlyRate = 0, int? PositionX = null, int? PositionY = null, string? QrToken = null);
    public record AppointmentInfo(Guid Id, Guid? CustomerId, Guid? StaffId, Guid? ResourceId, Guid ServiceId, DateTime StartTime, DateTime EndTime, string Status, string? ResourceName,
        string? TherapistName = null, string? ServiceName = null, string? CustomerName = null, string? Notes = null);
    public record ShopAssignmentInfo(Guid ShopId, string? ShopRole, Guid? BranchId);
    public record StaffInfo(Guid Id, Guid? UserId, string? EmployeeCode, string? Phone, string? Email, DateTime? JoinedAt, DateTime? TerminatedAt, string? Role, string? Status, string? ShopName,
        string? FirstName = null, string? LastName = null, string? Address = null, string? ProfilePhotoUrl = null, string? DocumentFrontUrl = null, string? DocumentBackUrl = null,
        List<ShopAssignmentInfo>? ShopAssignments = null);

    public async Task<List<StaffInfo>> GetStaffForShopAsync(Guid shopId)
    {
        var all = await GetStaffAsync();
        return all.Where(s =>
            s.ShopAssignments == null || s.ShopAssignments.Count == 0 ||
            s.ShopAssignments.Any(a => a.ShopId == shopId)).ToList();
    }

    public async Task<List<ShopInfo>> GetShopsAsync()
        => await GetListFromApiAsync<ShopInfo>("api/bff/shops");

    public async Task<ShopInfo?> GetShopByIdAsync(Guid shopId)
        => await GetObjectFromApiAsync<ShopInfo>($"api/bff/shops/{shopId}");

    /// <summary>
    /// EN: Get products for a shop. Deduplicates by product ID to prevent UI duplication
    ///     when the API response contains duplicate entries (e.g. from multi-format deserialization).
    /// VI: Lấy sản phẩm của shop. Loại bỏ trùng lặp theo ID để tránh hiển thị đôi trên UI
    ///     khi API response chứa bản ghi trùng (ví dụ từ deserialization đa định dạng).
    /// </summary>
    public async Task<List<ProductInfo>> GetProductsAsync(Guid shopId)
    {
        var products = await GetListFromApiAsync<ProductInfo>($"api/bff/shops/{shopId}/products");
        // EN: Deduplicate by Id — prevents double-display regardless of API response shape.
        // VI: Loại trùng theo Id — ngăn hiển thị đôi bất kể định dạng API response.
        return products.GroupBy(p => p.Id).Select(g => g.First()).ToList();
    }

    public async Task<List<CategoryInfo>> GetCategoriesAsync(Guid shopId)
        => await GetListFromApiAsync<CategoryInfo>($"api/bff/shops/{shopId}/categories");

    public async Task<List<TableInfo>> GetTablesAsync(Guid shopId)
        => await GetListFromApiAsync<TableInfo>($"api/bff/shops/{shopId}/tables");

    public async Task<List<AppointmentInfo>> GetAppointmentsAsync(Guid shopId)
        => await GetListFromApiAsync<AppointmentInfo>($"api/bff/shops/{shopId}/appointments");

    public async Task<List<StaffInfo>> GetStaffAsync()
        => await GetListFromApiAsync<StaffInfo>("api/bff/staff");

    // ═══ STAFF PORTAL — PROFILE & HR ═══

    // EN: Staff profile for current logged-in user (role, shop assignment)
    // VI: Thong tin nhan vien cho user dang dang nhap (vai tro, phan cong shop)
    public record StaffProfileInfo(Guid StaffId, Guid? UserId, string? Email, string? FirstName, string? LastName,
        string? Role, string? ShopName, Guid? ShopId, string? ShopRole, string? Status, string? EmployeeCode, string? Phone);

    public async Task<StaffProfileInfo?> GetMyStaffProfileAsync()
        => await GetObjectFromApiAsync<StaffProfileInfo>("api/bff/staff/me");

    // EN: Attendance records for staff portal
    // VI: Ban ghi cham cong cho cong nhan vien
    public record AttendanceRecord(Guid Id, Guid StaffId, DateTime Date, DateTime? CheckIn, DateTime? CheckOut,
        decimal? HoursWorked, string? Status, string? Notes);

    public async Task<List<AttendanceRecord>> GetMyAttendanceAsync(int month = 0, int year = 0)
    {
        var qs = month > 0 && year > 0 ? $"?month={month}&year={year}" : "";
        return await GetListFromApiAsync<AttendanceRecord>($"api/bff/staff/me/attendance{qs}");
    }

    // EN: Leave request records
    // VI: Ban ghi yeu cau nghi phep
    public record LeaveRequest(Guid Id, Guid StaffId, string LeaveType, DateTime StartDate, DateTime EndDate,
        string? Reason, string Status, string? ApprovedBy, DateTime CreatedAt);

    public async Task<List<LeaveRequest>> GetMyLeaveRequestsAsync()
        => await GetListFromApiAsync<LeaveRequest>("api/bff/staff/me/leave-requests");

    // EN: Staff notification
    // VI: Thong bao nhan vien
    public record StaffNotification(Guid Id, string Title, string? Message, string Type, bool IsRead, DateTime CreatedAt);

    public async Task<List<StaffNotification>> GetMyNotificationsAsync()
        => await GetListFromApiAsync<StaffNotification>("api/bff/staff/me/notifications");

    // ═══ ADMIN-LEVEL PRODUCT/CATEGORY METHODS ═══

    // EN: Admin-level records with shop_id and category info
    // VI: Record cấp admin với shop_id và thông tin danh mục
    public record AdminProductInfo(Guid Id, string Name, decimal Price, string? Sku, string? Description,
        string? ImageUrl, bool IsActive, string? Type, Guid ShopId, DateTime CreatedAt, string? CategoryName, Guid? CategoryId = null);
    public record AdminCategoryInfo(Guid Id, string Name, string? Description, int DisplayOrder,
        Guid ShopId, Guid? ParentId, bool IsActive, string? ImageUrl = null);
    public record CreateProductRequest(Guid ShopId, string Name, string? Description, decimal Price,
        string? Type, string? Sku, string? ImageUrl, Guid? CategoryId = null);

    public async Task<List<AdminProductInfo>> GetAllProductsAsync(Guid? shopId = null)
    {
        var url = shopId.HasValue ? $"api/bff/products?shopId={shopId}" : "api/bff/products";
        return await GetListFromApiAsync<AdminProductInfo>(url);
    }

    public async Task<List<AdminCategoryInfo>> GetAllCategoriesAsync(Guid? shopId = null)
    {
        var url = shopId.HasValue ? $"api/bff/categories?shopId={shopId}" : "api/bff/categories";
        return await GetListFromApiAsync<AdminCategoryInfo>(url);
    }

    public async Task<bool> CreateProductAsync(CreateProductRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/products", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<bool> UpdateProductAsync(Guid productId, CreateProductRequest req)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/products/{productId}", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<bool> DeleteProductAsync(Guid productId)
    {
        var resp = await _http.DeleteAsync($"api/bff/products/{productId}");
        return resp.IsSuccessStatusCode;
    }

    // ═══ INVENTORY METHODS ═══

    // EN: Inventory item info — includes item type, unit, cost and supplier metadata.
    // VI: Thông tin mặt hàng tồn kho — bao gồm loại, đơn vị, giá nhập và nhà cung cấp.
    public record InventoryItemInfo(Guid Id, Guid ProductId, Guid ShopId, int Quantity,
        int ReorderLevel, int ReservedQuantity, DateTime? UpdatedAt, string? ProductName,
        string? ItemType = null, string? Unit = null, decimal? CostPerUnit = null,
        string? SupplierName = null, DateTime? ExpiryDate = null);

    // EN: Request to create a new inventory item (raw material / supply).
    // VI: Yêu cầu tạo mặt hàng tồn kho mới (nguyên liệu / vật tư).
    public record CreateInventoryItemRequest(Guid ShopId, string Name, int ItemTypeId, string Unit,
        decimal CostPerUnit, int InitialQuantity, int ReorderLevel, string? SupplierName, DateTime? ExpiryDate);

    public async Task<List<InventoryItemInfo>> GetInventoryAsync(Guid? shopId = null)
    {
        var url = shopId.HasValue ? $"api/bff/inventory?shopId={shopId}" : "api/bff/inventory";
        return await GetListFromApiAsync<InventoryItemInfo>(url);
    }

    // EN: Create a new inventory item (raw material, finished product, or supply).
    // VI: Tạo mặt hàng tồn kho mới (nguyên liệu, thành phẩm, hoặc vật tư).
    public async Task<bool> CreateInventoryItemAsync(CreateInventoryItemRequest req)
    {
        var r= await _http.PostAsJsonAsync("api/bff/inventory/items", req, _writeOptions);
        return r.IsSuccessStatusCode;
    }

    public async Task<bool> DeleteInventoryItemAsync(Guid inventoryItemId)
    {
        var resp = await _http.DeleteAsync($"api/bff/inventory/items/{inventoryItemId}");
        return resp.IsSuccessStatusCode;
    }

    // ═══ MEMBERSHIP/CUSTOMER METHODS ═══

    public record MemberInfo(Guid Id, string? CountryCode, string? Gender, int CurrentExp,
        int CurrentLevel, int TotalExpEarned, DateTime CreatedAt, string? LevelName,
        string? DisplayName = null, string? Phone = null);

    public async Task<List<MemberInfo>> GetMembersAsync(string? search = null)
    {
        var url = string.IsNullOrWhiteSpace(search) ? "api/bff/members" : $"api/bff/members?search={Uri.EscapeDataString(search)}";
        return await GetListFromApiAsync<MemberInfo>(url);
    }

    // ═══ STAFF CREATE ═══

    public record CreateStaffRequest(Guid MerchantId, string? EmployeeCode, string? Phone, string? Email, string? Role,
        string? FirstName = null, string? LastName = null, string? Address = null,
        string? ProfilePhotoUrl = null, string? DocumentFrontUrl = null, string? DocumentBackUrl = null);

    public async Task<bool> CreateStaffAsync(CreateStaffRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/staff", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<bool> UpdateStaffAsync(Guid staffId, CreateStaffRequest req)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/staff/{staffId}", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<bool> DeleteStaffAsync(Guid staffId)
    {
        var resp = await _http.DeleteAsync($"api/bff/staff/{staffId}");
        return resp.IsSuccessStatusCode;
    }

    public record UpdateInventoryRequest(int Quantity, int ReorderLevel);

    public async Task<bool> UpdateInventoryAsync(Guid inventoryId, UpdateInventoryRequest req)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/inventory/{inventoryId}", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    // ═══ STAFF ROLES & SCHEDULES ═══

    public record StaffRoleInfo(int Id, string Name);
    public record ScheduleInfo(Guid Id, Guid StaffId, Guid ShopId, int DayOfWeek, string StartTime, string EndTime,
        string? EmployeeCode, string? Role, string? Phone);

    public async Task<List<StaffRoleInfo>> GetStaffRolesAsync()
        => await GetListFromApiAsync<StaffRoleInfo>("api/bff/staff/roles");

    public async Task<List<ScheduleInfo>> GetStaffSchedulesAsync(Guid? shopId = null)
    {
        var url = shopId.HasValue ? $"api/bff/staff/schedules?shopId={shopId}" : "api/bff/staff/schedules";
        return await GetListFromApiAsync<ScheduleInfo>(url);
    }

    // ═══ ORDERS ═══

    public record OrderInfo(Guid Id, Guid ShopId, decimal TotalAmount, int StatusId, DateTime CreatedAt,
        string? Status, string? PaymentMethod, string? Notes, int ItemCount = 0);

    public async Task<List<OrderInfo>> GetOrdersAsync(Guid? shopId = null, string filter = "today")
    {
        var url = shopId.HasValue
            ? $"api/bff/orders?shopId={shopId}&filter={filter}"
            : $"api/bff/orders?filter={filter}";
        return await GetListFromApiAsync<OrderInfo>(url);
    }

    // ═══ WALLETS / FINANCE ═══

    public record WalletInfo(Guid Id, decimal Balance, string? Currency, Guid OwnerId, DateTime CreatedAt, decimal TotalIncome, decimal TotalExpense);
    public record WalletTxnInfo(Guid Id, Guid WalletId, decimal Amount, string? Description, DateTime CreatedAt, string? ItemName);

    public async Task<List<WalletInfo>> GetWalletsAsync()
        => await GetListFromApiAsync<WalletInfo>("api/bff/wallets");

    public async Task<List<WalletTxnInfo>> GetWalletTransactionsAsync(int limit = 50)
        => await GetListFromApiAsync<WalletTxnInfo>($"api/bff/wallet/transactions?limit={limit}");

    // ═══ DEVICES ═══

    public record DeviceInfo(Guid Id, string? DeviceToken, string? Platform, bool IsActive, DateTime CreatedAt, string? StaffCode);

    public async Task<List<DeviceInfo>> GetDevicesAsync()
        => await GetListFromApiAsync<DeviceInfo>("api/bff/devices");

    // ═══ PROMOTIONS ═══

    public record PromotionInfo(Guid Id, string Name, string? Description, DateTime? StartDate, DateTime? EndDate,
        bool IsActive, string? DiscountType, decimal? DiscountValue, int VoucherCount, int RedemptionCount);

    public async Task<List<PromotionInfo>> GetPromotionsAsync()
        => await GetListFromApiAsync<PromotionInfo>("api/bff/promotions");

    // ═══ CAMPAIGNS CRUD ═══

    // EN: Campaign record and request DTOs for CRUD operations
    // VI: Record chiến dịch và DTO yêu cầu cho CRUD
    public record CampaignInfo(Guid Id, string Name, string? Description, decimal FaceValue,
        int TotalVouchers, int IssuedVouchers, DateTime? StartDate, DateTime? EndDate, string? Status, DateTime CreatedAt);
    public record PaginatedCampaignResponse(List<CampaignInfo>? Items, int TotalCount, int PageNumber, int PageSize, int TotalPages);
    public record CreateCampaignRequest(string Name, string? Description, decimal FaceValue, int TotalVouchers, DateTime StartDate, DateTime EndDate);

    public async Task<List<CampaignInfo>> GetCampaignsAsync()
    {
        return await GetListFromApiAsync<CampaignInfo>("api/bff/campaigns");
    }

    public async Task<bool> CreateCampaignAsync(CreateCampaignRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/campaigns", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<bool> UpdateCampaignAsync(Guid campaignId, CreateCampaignRequest req)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/campaigns/{campaignId}", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<bool> DeleteCampaignAsync(Guid campaignId)
    {
        var resp = await _http.DeleteAsync($"api/bff/campaigns/{campaignId}");
        return resp.IsSuccessStatusCode;
    }

    // ═══ MEMBER CRUD ═══

    // EN: Member create/update request DTOs
    // VI: DTO tạo/cập nhật thành viên
    public record CreateMemberRequest(string? Gender, string? CountryCode, string? Name = null, string? Phone = null);
    public record UpdateMemberRequest(string? Gender, string? Preferences);

    public async Task<(bool Ok, string? Error)> CreateMemberAsync(CreateMemberRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/members", req, _writeOptions);
        if (resp.IsSuccessStatusCode) return (true, null);
        var err = await TryExtractError(resp);
        return (false, err);
    }

    public async Task<bool> UpdateMemberAsync(Guid memberId, UpdateMemberRequest req)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/members/{memberId}", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<bool> DeleteMemberAsync(Guid memberId)
    {
        var resp = await _http.DeleteAsync($"api/bff/members/{memberId}");
        return resp.IsSuccessStatusCode;
    }

    // ═══ INVENTORY TRANSACTIONS ═══

    public record InventoryTxnInfo(Guid Id, Guid InventoryItemId, int Quantity, string? Notes, DateTime CreatedAt, string? TransactionType);

    public async Task<List<InventoryTxnInfo>> GetInventoryTransactionsAsync(Guid? shopId = null)
    {
        var url = shopId.HasValue ? $"api/bff/inventory/transactions?shopId={shopId}" : "api/bff/inventory/transactions";
        return await GetListFromApiAsync<InventoryTxnInfo>(url);
    }

    // ═══ INVENTORY OPERATIONS ═══

    // EN: Stock-in request — includes optional invoice image URL and unit cost.
    // VI: Yêu cầu nhập kho — bao gồm URL hóa đơn và giá nhập tùy chọn.
    public record StockInRequest(Guid ProductId, Guid ShopId, int Amount, string? Notes,
        string? InvoiceImageUrl = null, decimal? UnitCost = null, string? SupplierName = null);

    public async Task<bool> StockInAsync(StockInRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/inventory/stock-in", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public record StockOutRequest(Guid ProductId, Guid ShopId, int Amount, string? Notes);

    public async Task<bool> StockOutAsync(StockOutRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/inventory/stock-out", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public record AdjustStockRequest(Guid ProductId, Guid ShopId, int NewQuantity, string Notes);

    public async Task<bool> AdjustStockAsync(AdjustStockRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/inventory/adjust", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public record LowStockItemInfo(Guid Id, Guid ProductId, string? ProductName, int Quantity, int LowStockThreshold, Guid ShopId);

    public async Task<List<LowStockItemInfo>> GetLowStockAsync(Guid? shopId = null)
    {
        var url = shopId.HasValue ? $"api/bff/inventory/low-stock?shopId={shopId}" : "api/bff/inventory/low-stock";
        return await GetListFromApiAsync<LowStockItemInfo>(url);
    }

    // ═══ WASTAGE & STOCKTAKE ═══

    public record RecordWastageRequest(Guid InventoryItemId, int Amount, string Reason, string? Notes);

    public async Task<bool> RecordWastageAsync(RecordWastageRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/inventory/wastage", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public record StocktakeItemRequest(Guid InventoryItemId, int CountedQuantity);
    public record StocktakeRequest(Guid ShopId, List<StocktakeItemRequest> Items);
    public record StocktakeDiscrepancy(Guid InventoryItemId, string? ItemName, int ExpectedQuantity, int CountedQuantity, int Difference);
    public record StocktakeResult(List<StocktakeDiscrepancy> Discrepancies, int TotalItemsCounted);

    public async Task<StocktakeResult?> StocktakeAsync(StocktakeRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/inventory/stocktake", req, _writeOptions);
        if (!resp.IsSuccessStatusCode) return null;
        try
        {
            var json = await resp.Content.ReadAsStringAsync();
            using var doc = JsonDocument.Parse(json);
            if (doc.RootElement.TryGetProperty("data", out var data))
                return JsonSerializer.Deserialize<StocktakeResult>(data.GetRawText(), _jsonOptions);
            return JsonSerializer.Deserialize<StocktakeResult>(json, _jsonOptions);
        }
        catch { return null; }
    }

    // ═══ MEMBERSHIP LEVELS ═══

    public record LevelDefinitionInfo(Guid Id, int LevelNumber, string Name, int RequiredExp,
        string? Description = null, string? BadgeColor = null, bool IsActive = true, int MemberCount = 0,
        int MinExp = 0, int MaxExp = 0)
    {
        public int Level => LevelNumber;
    };

    public async Task<List<LevelDefinitionInfo>> GetMembershipLevelsAsync()
        => await GetListFromApiAsync<LevelDefinitionInfo>("api/bff/membership/levels");

    public static List<LevelDefinitionInfo> EnrichLevelDefinitions(List<LevelDefinitionInfo> levels, List<MemberInfo> members)
    {
        if (!levels.Any()) return levels;
        var sorted = levels.OrderBy(l => l.LevelNumber).ToList();
        var enriched = new List<LevelDefinitionInfo>();
        for (int i = 0; i < sorted.Count; i++)
        {
            var lvl = sorted[i];
            int minExp = lvl.RequiredExp;
            int maxExp = (i + 1 < sorted.Count) ? sorted[i + 1].RequiredExp - 1 : int.MaxValue;
            int memberCount = members.Count(m => m.CurrentLevel == lvl.LevelNumber);
            enriched.Add(lvl with { MinExp = minExp, MaxExp = maxExp == int.MaxValue ? 999999 : maxExp, MemberCount = memberCount });
        }
        return enriched;
    }

    public static List<MemberInfo> ResolveMemberLevelNames(List<MemberInfo> members, List<LevelDefinitionInfo> levels)
    {
        var levelMap = levels.ToDictionary(l => l.LevelNumber, l => l.Name);
        return members.Select(m => m with { LevelName = levelMap.GetValueOrDefault(m.CurrentLevel) ?? m.LevelName }).ToList();
    }

    // ═══ LEVEL DEFINITION CRUD ═══

    public record CreateLevelRequest(int LevelNumber, string Name, int RequiredExp, string? Description, string? BadgeColor);

    public async Task<(bool Ok, string? Error)> CreateLevelAsync(CreateLevelRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/membership/levels", req, _writeOptions);
        if (resp.IsSuccessStatusCode) return (true, null);
        return (false, await TryExtractError(resp));
    }

    public async Task<(bool Ok, string? Error)> UpdateLevelAsync(Guid levelId, CreateLevelRequest req)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/membership/levels/{levelId}", req, _writeOptions);
        if (resp.IsSuccessStatusCode) return (true, null);
        return (false, await TryExtractError(resp));
    }

    public async Task<bool> DeleteLevelAsync(Guid levelId)
    {
        var resp = await _http.DeleteAsync($"api/bff/membership/levels/{levelId}");
        return resp.IsSuccessStatusCode;
    }

    // ═══ EXP MANAGEMENT ═══

    public record AddExpRequest(int Points, int SourceId, string? ReferenceId);
    public record AddExpResult(Guid MemberId, int PointsAdded, int CurrentExp, int TotalExpEarned,
        int PreviousLevel, int CurrentLevel, bool LeveledUp);
    public record MemberProgressInfo(Guid MemberId, int CurrentLevel, string? CurrentLevelName,
        int CurrentExp, int TotalExpEarned, int ExpToNextLevel, int ProgressPercent,
        int? NextLevel, string? NextLevelName, string? BadgeColor);
    public record ExpTransactionInfo(Guid Id, int Points, string? Source, int SourceId,
        string? ReferenceId, int LevelAtTime, DateTime CreatedAt);

    public async Task<AddExpResult?> AddExperienceAsync(Guid memberId, AddExpRequest req)
    {
        var resp = await _http.PostAsJsonAsync($"api/bff/members/{memberId}/experience", req, _writeOptions);
        if (resp.IsSuccessStatusCode)
            return await resp.Content.ReadFromJsonAsync<AddExpResult>(_jsonOptions);
        return null;
    }

    public async Task<MemberProgressInfo?> GetMemberProgressAsync(Guid memberId)
        => await GetObjectFromApiAsync<MemberProgressInfo>($"api/bff/members/{memberId}/progress");

    public async Task<List<ExpTransactionInfo>> GetExperienceHistoryAsync(Guid memberId)
        => await GetListFromApiAsync<ExpTransactionInfo>($"api/bff/members/{memberId}/experience");

    // ═══ FILE UPLOAD (Storage Service) ═══

    public async Task<string?> UploadImageAsync(Stream fileStream, string fileName, string contentType)
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        using var content = new MultipartFormDataContent();
        var streamContent = new StreamContent(fileStream);
        streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
        content.Add(streamContent, "file", fileName);
        var resp = await _http.PostAsync("api/bff/files/upload?accessLevel=public", content);
        if (!resp.IsSuccessStatusCode) return null;
        var json = await resp.Content.ReadAsStringAsync();
        using var doc = JsonDocument.Parse(json);
        // EN: Try to extract download URL or file ID from response
        if (doc.RootElement.TryGetProperty("downloadUrl", out var url))
            return url.GetString();
        if (doc.RootElement.TryGetProperty("data", out var data))
        {
            if (data.TryGetProperty("downloadUrl", out var dUrl)) return dUrl.GetString();
            if (data.TryGetProperty("fileId", out var fId)) return $"api/bff/files/{fId.GetString()}/download";
        }
        if (doc.RootElement.TryGetProperty("fileId", out var fileId))
            return $"api/bff/files/{fileId.GetString()}/download";
        return null;
    }

    // ═══ SHOP STATS (aggregated per-shop) ═══

    public record ShopStatsInfo(Guid ShopId, int ProductCount, int OrderCount, int StaffCount, decimal Revenue);

    public async Task<List<ShopStatsInfo>> GetShopStatsAsync()
        => await GetListFromApiAsync<ShopStatsInfo>("api/bff/shops/stats");

    // ═══ BOOKING RESOURCES ═══

    public record ResourceInfo(Guid Id, string Name, string? ResourceType, int Capacity, bool IsActive);

    public async Task<List<ResourceInfo>> GetResourcesAsync(Guid shopId)
        => await GetListFromApiAsync<ResourceInfo>($"api/bff/shops/{shopId}/resources");

    // ═══ POS DASHBOARD (real-time daily stats) ═══

    // EN: POS dashboard response DTOs — matching camelCase API response from OrderService
    // VI: DTOs cho response dashboard POS — khớp camelCase API response từ OrderService
    public record PosDashboardInfo(
        decimal Revenue, int OrderCount, int ItemsSold, decimal AvgOrderValue,
        List<PopularItemInfo> PopularItems,
        List<PaymentBreakdownInfo> PaymentBreakdown,
        List<HourlyRevenueInfo> HourlyRevenue,
        List<RecentOrderInfo> RecentOrders);
    public record PopularItemInfo(Guid ProductId, string ProductName, int QuantitySold, decimal Revenue)
    {
        // EN: Aliases for Razor display / VI: Alias cho hiển thị Razor
        public string Name => ProductName;
        public int Qty => QuantitySold;
    }
    public record PaymentBreakdownInfo(string Method, int Count, decimal Amount)
    {
        public int Pct { get; set; }
    }
    public record HourlyRevenueInfo(int Hour, decimal Revenue, int OrderCount)
    {
        public string HourLabel => $"{Hour}h";
        public int Pct { get; set; }
    }
    public record RecentOrderInfo(Guid Id, decimal TotalAmount, string Status, int ItemCount, DateTime CreatedAt);

    public async Task<PosDashboardInfo> GetPosDashboardAsync(Guid shopId, string? period = "today")
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        return await _http.GetFromJsonAsync<PosDashboardInfo>(
            $"api/bff/pos/dashboard?shopId={shopId}&period={period ?? "today"}", _jsonOptions)
            ?? new(0, 0, 0, 0, new(), new(), new(), new());
    }

    // ═══ POS ORDER CREATE ═══

    // EN: POS order creation DTOs
    // VI: DTOs cho tạo đơn POS
    public record CreatePosOrderRequest(Guid ShopId, string? PaymentMethod, List<PosOrderItemRequest> Items,
        decimal? DiscountAmount = null, string? DiscountType = null, string? DiscountReference = null, Guid? TableId = null);
    public record PosOrderItemRequest(Guid ProductId, string ProductName, int Quantity, decimal UnitPrice, string? ProductType = "Physical");
    public record CreatePosOrderResponse(Guid OrderId, string TransactionId, decimal TotalAmount, string Status);

    public async Task<CreatePosOrderResponse?> CreatePosOrderAsync(CreatePosOrderRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/pos/orders", req, _writeOptions);
        if (resp.IsSuccessStatusCode)
            return await resp.Content.ReadFromJsonAsync<CreatePosOrderResponse>(_jsonOptions);
        return null;
    }

    // ═══ PAY ORDER ═══

    /// <summary>
    /// EN: Payment result from order-service via BFF.
    /// VI: Kết quả thanh toán từ order-service qua BFF.
    /// </summary>
    public record PayOrderResponse(bool Success, string? Status, string? PaymentUrl, decimal? ChangeAmount, string? TransactionId, string? ErrorMessage);

    /// <summary>
    /// EN: Pay an order with payment method details. Returns rich result for all payment flows.
    /// VI: Thanh toán order với chi tiết phương thức. Trả về kết quả đầy đủ cho tất cả luồng thanh toán.
    /// </summary>
    public async Task<PayOrderResponse?> PayOrderWithDetailsAsync(
        Guid orderId, Guid shopId, string paymentMethod,
        decimal? amountTendered = null, string? returnUrl = null)
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        var body = new { PaymentMethod = paymentMethod, AmountTendered = amountTendered, ReturnUrl = returnUrl };
        var resp = await _http.PostAsJsonAsync($"api/bff/orders/{orderId}/pay?shopId={shopId}", body, _writeOptions);
        if (resp.IsSuccessStatusCode)
        {
            // EN: BFF proxies order-service response: { success: true, data: { ... } }
            // VI: BFF proxy response từ order-service: { success: true, data: { ... } }
            try
            {
                var json = await resp.Content.ReadAsStringAsync();
                using var doc = System.Text.Json.JsonDocument.Parse(json);
                if (doc.RootElement.TryGetProperty("data", out var data))
                {
                    return System.Text.Json.JsonSerializer.Deserialize<PayOrderResponse>(data.GetRawText(), _jsonOptions);
                }
                return System.Text.Json.JsonSerializer.Deserialize<PayOrderResponse>(json, _jsonOptions);
            }
            catch { return new PayOrderResponse(false, null, null, null, null, "Không thể xử lý phản hồi từ máy chủ"); }
        }
        return new PayOrderResponse(false, null, null, null, null, "Payment failed");
    }

    /// <summary>
    /// EN: Simple pay order (backward compatible). Returns true if successful.
    /// VI: Thanh toán order đơn giản (tương thích ngược). Trả về true nếu thành công.
    /// </summary>
    public async Task<bool> PayOrderAsync(Guid orderId, Guid shopId, string? paymentMethod = null)
    {
        var result = await PayOrderWithDetailsAsync(orderId, shopId, paymentMethod ?? "cash");
        return result?.Success ?? false;
    }

    // ═══ ACTIVE TABLE ORDERS ═══

    // EN: DTOs for active table orders (orders with table_id, status=Validated)
    // VI: DTOs cho active table orders (orders có table_id, status=Validated)
    public record ActiveTableOrderDto
    {
        public Guid OrderId { get; init; }
        public Guid? TableId { get; init; }
        public decimal TotalAmount { get; init; }
        public DateTime CreatedAt { get; init; }
        public List<ActiveTableOrderItemDto> Items { get; init; } = new();
    }

    public record ActiveTableOrderItemDto
    {
        public Guid ProductId { get; init; }
        public string ProductName { get; init; } = string.Empty;
        public int Quantity { get; init; }
        public decimal UnitPrice { get; init; }
    }

    public async Task<List<ActiveTableOrderDto>> GetActiveTableOrdersAsync(Guid shopId)
    {
        var resp = await _http.GetAsync($"api/bff/orders/active-by-table?shopId={shopId}");
        if (resp.IsSuccessStatusCode)
            return await resp.Content.ReadFromJsonAsync<List<ActiveTableOrderDto>>(_jsonOptions) ?? new();
        return new();
    }

    // ═══ CATEGORIES CRUD ═══

    // EN: Category create/update request DTO
    // VI: DTO tạo/cập nhật danh mục
    public record AdminCreateCategoryRequest(Guid ShopId, string Name, string? Description, int DisplayOrder, string? ImageUrl = null);

    public async Task<bool> CreateCategoryAsync(AdminCreateCategoryRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/categories", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<bool> UpdateCategoryAsync(Guid categoryId, AdminCreateCategoryRequest req)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/categories/{categoryId}", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<bool> DeleteCategoryAsync(Guid categoryId)
    {
        var resp = await _http.DeleteAsync($"api/bff/categories/{categoryId}");
        return resp.IsSuccessStatusCode;
    }

    // ═══ ORDER DETAIL & CANCEL ═══

    // EN: Order detail DTOs
    // VI: DTOs cho chi tiết đơn hàng
    public record OrderDetailInfo(Guid Id, Guid ShopId, decimal TotalAmount, string? Status, int StatusId, string? PaymentMethod, string? Notes, DateTime CreatedAt);
    public record OrderItemInfo(Guid Id, string? ProductName, int Quantity, decimal UnitPrice, decimal Subtotal);
    public record OrderDetailResponse(OrderDetailInfo? Order, List<OrderItemInfo>? Items);

    public async Task<OrderDetailResponse?> GetOrderDetailAsync(Guid orderId, Guid? shopId = null)
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        var qs = shopId.HasValue ? $"?shopId={shopId}" : "";
        return await _http.GetFromJsonAsync<OrderDetailResponse>($"api/bff/orders/{orderId}{qs}", _jsonOptions);
    }

    public async Task<bool> CancelOrderAsync(Guid orderId)
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        using var req = new HttpRequestMessage(HttpMethod.Put, $"api/bff/orders/{orderId}/cancel");
        req.Headers.Authorization = _http.DefaultRequestHeaders.Authorization;
        var resp = await _http.SendAsync(req);
        return resp.IsSuccessStatusCode;
    }

    // ═══ SHOP UPDATE ═══

    // EN: Shop update DTO
    // VI: DTO cập nhật thông tin cửa hàng
    public record UpdateShopRequest(string? Name, string? Phone, string? Email, string? Description, string? OpenTime, string? CloseTime, string? OpenDays);

    public async Task<bool> UpdateShopAsync(Guid shopId, UpdateShopRequest req)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/shops/{shopId}", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    // ═══ REVENUE REPORT ═══

    // EN: Revenue report item DTO
    // VI: DTO cho từng dòng báo cáo doanh thu
    // EN: Revenue report item DTO — matches OrderService response { periodStart, orderCount, revenue }
    // VI: DTO báo cáo doanh thu — khớp response OrderService { periodStart, orderCount, revenue }
    public record RevenueReportItem(DateTime PeriodStart, long OrderCount, decimal Revenue, decimal AvgOrderValue = 0);

    public async Task<List<RevenueReportItem>> GetRevenueReportAsync(string period = "daily", Guid? shopId = null)
    {
        var url = shopId.HasValue
            ? $"api/bff/reports/revenue?period={period}&shopId={shopId}"
            : $"api/bff/reports/revenue?period={period}";
        return await GetListFromApiAsync<RevenueReportItem>(url);
    }

    // ═══ SHOP SETTINGS ═══

    public record ShopFeaturesInfo
    {
        public bool HasInventory { get; init; }
        public bool HasBooking { get; init; }
        public bool HasTables { get; init; }
        public bool HasKitchen { get; init; }
        public bool HasShipping { get; init; }
        public bool HasDelivery { get; init; }
    }
    public record ShopSettingsInfo
    {
        public Guid ShopId { get; init; }
        public ShopFeaturesInfo? Features { get; init; }
        public string? OpenTime { get; init; }
        public string? CloseTime { get; init; }
        public List<string>? OpenDays { get; init; }
    }
    public record UpdateShopSettingsRequest(ShopFeaturesInfo? Features, string? OpenTime, string? CloseTime, List<string>? OpenDays);

    public async Task<ShopSettingsInfo?> GetShopSettingsAsync(Guid shopId)
        => await GetObjectFromApiAsync<ShopSettingsInfo>($"api/bff/shops/{shopId}/settings");

    public async Task<bool> UpdateShopSettingsAsync(Guid shopId, UpdateShopSettingsRequest req)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/shops/{shopId}/settings", req, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    // ═══ TOP PRODUCTS ═══

    // EN: Top product DTO — matches OrderService { productName, totalQuantity, totalRevenue }
    // VI: DTO sản phẩm bán chạy — khớp OrderService { productName, totalQuantity, totalRevenue }
    public record TopProductInfo(string? ProductName, long TotalQuantity, decimal TotalRevenue, int OrderCount = 0);

    public async Task<List<TopProductInfo>> GetTopProductsAsync(Guid? shopId = null, int limit = 10)
    {
        var url = shopId.HasValue
            ? $"api/bff/reports/top-products?shopId={shopId}&limit={limit}"
            : $"api/bff/reports/top-products?limit={limit}";
        return await GetListFromApiAsync<TopProductInfo>(url);
    }

    // ═══ TABLES CRUD ═══

    public record CreateTableRequest(Guid ShopId, string TableNumber, int Capacity, string? Zone, decimal? HourlyRate = null);

    public async Task<bool> CreateTableAsync(CreateTableRequest req)
    { var r = await _http.PostAsJsonAsync("api/bff/tables", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> UpdateTableAsync(Guid tableId, CreateTableRequest req)
    { var r = await _http.PutAsJsonAsync($"api/bff/tables/{tableId}", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> UpdateTablePositionAsync(Guid tableId, int x, int y)
    { var r = await _http.PutAsJsonAsync($"api/bff/tables/{tableId}", new { positionX = x, positionY = y }, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> DeleteTableAsync(Guid tableId)
    { var r = await _http.DeleteAsync($"api/bff/tables/{tableId}"); return r.IsSuccessStatusCode; }

    // ═══ APPOINTMENTS CRUD ═══

    public record CreateAppointmentRequest(Guid ShopId, Guid? CustomerId, Guid? StaffId, Guid? ResourceId, Guid? ServiceId, DateTime StartTime, DateTime EndTime, string? Status = null);

    public async Task<bool> CreateAppointmentAsync(CreateAppointmentRequest req)
    { var r = await _http.PostAsJsonAsync("api/bff/appointments", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> UpdateAppointmentAsync(Guid apptId, CreateAppointmentRequest req)
    { var r = await _http.PutAsJsonAsync($"api/bff/appointments/{apptId}", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> CancelAppointmentAsync(Guid apptId)
    { var r = await _http.DeleteAsync($"api/bff/appointments/{apptId}/cancel"); return r.IsSuccessStatusCode; }

    // EN: Get single appointment by ID / VI: Lấy appointment theo ID
    public async Task<AppointmentInfo?> GetAppointmentByIdAsync(Guid appointmentId)
        => await GetObjectFromApiAsync<AppointmentInfo>($"api/bff/appointments/{appointmentId}");

    // EN: Update appointment status (confirm, start, complete) / VI: Cập nhật trạng thái appointment
    public async Task<bool> UpdateAppointmentStatusAsync(Guid appointmentId, string action)
    { var r = await _http.PatchAsync($"api/bff/appointments/{appointmentId}/status", JsonContent.Create(new { action }, options: _writeOptions)); return r.IsSuccessStatusCode; }

    // EN: Search customers/members / VI: Tìm kiếm khách hàng/thành viên
    public async Task<List<MemberInfo>> SearchCustomersAsync(Guid shopId, string query)
        => await GetMembersAsync(query);

    // ═══ THERAPIST / SPA STAFF CRUD ═══

    // EN: Therapist record for Spa/Beauty management
    // VI: Record nhân viên trị liệu cho quản lý Spa/Beauty
    public record TherapistInfo(Guid Id, string Name, string[]? Specialties, bool IsActive, string? Phone = null, string? Email = null, string? WorkingHours = null);
    public record CreateTherapistRequest(Guid ShopId, string Name, string[]? Specialties, string? Phone = null, string? Email = null, string? WorkingHours = null);

    public async Task<List<TherapistInfo>> GetTherapistsAsync(Guid shopId)
        => await GetListFromApiAsync<TherapistInfo>($"api/bff/shops/{shopId}/therapists");

    public async Task<bool> CreateTherapistAsync(CreateTherapistRequest req)
    { var r = await _http.PostAsJsonAsync("api/bff/therapists", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> UpdateTherapistAsync(Guid therapistId, CreateTherapistRequest req)
    { var r = await _http.PutAsJsonAsync($"api/bff/therapists/{therapistId}", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> DeactivateTherapistAsync(Guid therapistId)
    { var r = await _http.DeleteAsync($"api/bff/therapists/{therapistId}"); return r.IsSuccessStatusCode; }

    // ═══ ENHANCED APPOINTMENT METHODS (SPA) ═══

    // EN: Get appointments for a specific date / VI: Lấy lịch hẹn theo ngày cụ thể
    public async Task<List<AppointmentInfo>> GetAppointmentsByDateAsync(Guid shopId, DateTime date)
        => await GetListFromApiAsync<AppointmentInfo>($"api/bff/shops/{shopId}/appointments?date={date:yyyy-MM-dd}");

    // EN: Cancel appointment with reason / VI: Hủy lịch hẹn kèm lý do
    public async Task<bool> CancelAppointmentWithReasonAsync(Guid apptId, string reason)
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        var request = new HttpRequestMessage(HttpMethod.Delete, $"api/bff/appointments/{apptId}/cancel")
        {
            Content = JsonContent.Create(new { reason }, options: _writeOptions)
        };
        var r = await _http.SendAsync(request);
        return r.IsSuccessStatusCode;
    }

    // ═══ RESOURCES CRUD ═══

    public record CreateResourceRequest(Guid ShopId, string Name, string ResourceType, int Capacity);

    public async Task<bool> CreateResourceAsync(CreateResourceRequest req)
    { var r = await _http.PostAsJsonAsync("api/bff/resources", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> UpdateResourceAsync(Guid resourceId, CreateResourceRequest req)
    { var r = await _http.PutAsJsonAsync($"api/bff/resources/{resourceId}", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> DeleteResourceAsync(Guid resourceId)
    { var r = await _http.DeleteAsync($"api/bff/resources/{resourceId}"); return r.IsSuccessStatusCode; }

    // ═══ STAFF SCHEDULES CRUD ═══

    public record CreateScheduleRequest(Guid ShopId, Guid StaffId, int DayOfWeek, string StartTime, string EndTime);

    public async Task<bool> CreateScheduleAsync(CreateScheduleRequest req)
    { var r = await _http.PostAsJsonAsync("api/bff/staff/schedules", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> UpdateScheduleAsync(Guid scheduleId, CreateScheduleRequest req)
    { var r = await _http.PutAsJsonAsync($"api/bff/staff/schedules/{scheduleId}", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> DeleteScheduleAsync(Guid scheduleId)
    { var r = await _http.DeleteAsync($"api/bff/staff/schedules/{scheduleId}"); return r.IsSuccessStatusCode; }

    // ═══ KITCHEN TICKETS ═══

    public record KitchenTicketInfo(Guid Id, Guid SessionId, Guid OrderItemId, string ItemName, string? Station, int Priority, string Status, DateTime CreatedAt, DateTime? CompletedAt);
    public record UpdateTicketStatusRequest(string Status);

    public async Task<List<KitchenTicketInfo>> GetKitchenTicketsAsync(Guid? shopId = null, string? status = null)
    {
        // EN: Use staff-context BFF endpoint when no shopId — auto-resolves from staff profile
        // VI: Dùng BFF endpoint theo context nhân viên khi không có shopId — tự lấy từ profile
        var qs = !string.IsNullOrEmpty(status) ? $"?status={status}" : "";
        if (shopId.HasValue)
            return await GetListFromApiAsync<KitchenTicketInfo>($"api/bff/shops/{shopId}/kitchen-tickets{qs}");
        return await GetListFromApiAsync<KitchenTicketInfo>($"api/bff/kitchen/tickets{qs}");
    }

    public async Task<bool> UpdateTicketStatusAsync(Guid ticketId, UpdateTicketStatusRequest req)
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        var request = new HttpRequestMessage(HttpMethod.Patch, $"api/bff/kitchen/tickets/{ticketId}/status")
        {
            Content = JsonContent.Create(req, options: _writeOptions)
        };
        var r = await _http.SendAsync(request);
        return r.IsSuccessStatusCode;
    }

    // ═══ RECIPES CRUD ═══

    // EN: Recipe ingredient info — includes optional inventory item link for COGS.
    // VI: Thông tin nguyên liệu công thức — bao gồm liên kết tùy chọn đến mặt hàng tồn kho để tính giá vốn.
    public record RecipeIngredientInfo(Guid Id, Guid RecipeId, string IngredientName, decimal Quantity, string Unit, decimal CostPerUnit,
        Guid? InventoryItemId = null, decimal QuantityPerServing = 0);
    public record RecipeInfo(Guid Id, Guid ProductId, Guid ShopId, string Name, string? Instructions, int PrepTimeMinutes, bool IsActive, DateTime CreatedAt,
        List<RecipeIngredientInfo>? Ingredients = null);
    public record CreateRecipeRequest(Guid ShopId, Guid ProductId, string Name, string? Instructions, int PrepTimeMinutes, List<RecipeIngredientRequest>? Ingredients);
    public record RecipeIngredientRequest(string IngredientName, decimal Quantity, string Unit, decimal CostPerUnit,
        Guid? InventoryItemId = null, decimal QuantityPerServing = 0);

    public async Task<List<RecipeInfo>> GetRecipesAsync(Guid? shopId = null)
    {
        if (!shopId.HasValue) return new();
        var url = $"api/bff/shops/{shopId}/recipes";
        return await GetListFromApiAsync<RecipeInfo>(url);
    }

    public async Task<bool> CreateRecipeAsync(CreateRecipeRequest req)
    { var r = await _http.PostAsJsonAsync("api/bff/recipes", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> UpdateRecipeAsync(Guid recipeId, CreateRecipeRequest req)
    { var r = await _http.PutAsJsonAsync($"api/bff/recipes/{recipeId}", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> DeleteRecipeAsync(Guid recipeId)
    { var r = await _http.DeleteAsync($"api/bff/recipes/{recipeId}"); return r.IsSuccessStatusCode; }

    // ═══ VOUCHER VALIDATION ═══

    public record VoucherValidationInfo(bool IsValid, string? ErrorMessage, Guid? VoucherId,
        string? VoucherCode, decimal? RemainingValue, DateTime? ExpiresAt, string? CampaignName);

    public async Task<VoucherValidationInfo?> ValidateVoucherAsync(string code)
        => await GetObjectFromApiAsync<VoucherValidationInfo>($"api/bff/vouchers/validate/{Uri.EscapeDataString(code)}");

    public async Task<bool> RedeemVoucherAsync(Guid voucherId, decimal amount)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/vouchers/redeem", new { voucherId, amount }, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    // ═══ ADMIN VOUCHER MANAGEMENT ═══

    public record AdminVoucherInfo(Guid Id, Guid CampaignId, string? CampaignName, string? Code,
        Guid? OwnerId, string? OwnerEmail, decimal FaceValue, decimal RemainingValue, string? Status,
        DateTime? ClaimedAt, DateTime? ExpiresAt, DateTime? RedeemedAt, DateTime? CreatedAt);

    public record PaginatedVoucherResponse(List<AdminVoucherInfo>? Items, int TotalCount, int PageNumber, int PageSize, int TotalPages);

    public async Task<List<AdminVoucherInfo>> GetAdminVouchersAsync(Guid? campaignId = null, string? status = null, int pageSize = 50)
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        var qs = $"pageSize={pageSize}";
        if (campaignId.HasValue) qs += $"&campaignId={campaignId}";
        if (!string.IsNullOrEmpty(status)) qs += $"&status={status}";
        var resp = await _http.GetFromJsonAsync<PaginatedVoucherResponse>($"api/bff/vouchers/list?{qs}", _jsonOptions);
        return resp?.Items ?? new();
    }

    public async Task<bool> RevokeVoucherAsync(Guid voucherId)
    { var r = await _http.PostAsync($"api/bff/vouchers/{voucherId}/revoke", null); return r.IsSuccessStatusCode; }

    // ═══ CAMPAIGN ACTIONS ═══

    public async Task<bool> ActivateCampaignAsync(Guid campaignId)
    { var r = await _http.PostAsync($"api/bff/campaigns/{campaignId}/activate", null); return r.IsSuccessStatusCode; }

    public async Task<bool> PauseCampaignAsync(Guid campaignId)
    { var r = await _http.PostAsync($"api/bff/campaigns/{campaignId}/pause", null); return r.IsSuccessStatusCode; }

    // ═══ STAFF IAM ═══

    public record InviteStaffWithAccountRequest(string Email, string Password, string FirstName, string LastName, string Role, Guid? ShopId);

    public async Task<(bool Ok, string? Error)> InviteStaffWithAccountAsync(InviteStaffWithAccountRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/staff/invite-with-account", req, _writeOptions);
        if (resp.IsSuccessStatusCode) return (true, null);
        var err = await TryExtractError(resp);
        return (false, err);
    }

    // ═══ STORAGE / DRIVE ═══

    public record StorageFileInfo(Guid Id, string FileName, string? ContentType, long FileSizeBytes, string? AccessLevel, DateTime UploadedAt);
    public record StorageFolderInfo(Guid Id, Guid? ParentId, string Name, string? Path, DateTime CreatedAt);
    public record CreateFolderRequest(string Name, Guid? ParentId);

    // Storage API wraps responses in { success, data, error }
    private record StorageApiResponse<T>(bool Success, T? Data, string? Error);
    private record UserFilesResult(List<StorageFileInfo> Files, int TotalCount);

    public async Task<List<StorageFileInfo>> GetStorageFilesAsync(int skip = 0, int take = 50, string? search = null)
    {
        try
        {
            var qs = $"?skip={skip}&take={take}";
            if (!string.IsNullOrEmpty(search)) qs += $"&search={Uri.EscapeDataString(search)}";
            var wrapper = await _http.GetFromJsonAsync<StorageApiResponse<UserFilesResult>>($"api/bff/files{qs}", _jsonOptions);
            return wrapper?.Data?.Files ?? new();
        }
        catch { return new(); }
    }

    public async Task<bool> DeleteStorageFileAsync(Guid fileId)
    { var r = await _http.DeleteAsync($"api/bff/files/{fileId}"); return r.IsSuccessStatusCode; }

    public async Task<string?> GetDownloadUrlAsync(Guid fileId)
    {
        var resp = await _http.GetAsync($"api/bff/files/{fileId}/download-url");
        if (!resp.IsSuccessStatusCode) return null;
        var json = await resp.Content.ReadFromJsonAsync<JsonElement>(_jsonOptions);
        if (json.TryGetProperty("data", out var data))
        {
            if (data.TryGetProperty("url", out var url)) return url.GetString();
            if (data.TryGetProperty("downloadUrl", out var dl)) return dl.GetString();
        }
        if (json.TryGetProperty("url", out var directUrl)) return directUrl.GetString();
        return null;
    }

    public async Task<List<StorageFolderInfo>> GetFoldersAsync(Guid? parentId = null)
    {
        try
        {
            var qs = parentId.HasValue ? $"?parentId={parentId}" : "";
            var wrapper = await _http.GetFromJsonAsync<StorageApiResponse<IEnumerable<StorageFolderInfo>>>($"api/bff/folders{qs}", _jsonOptions);
            return wrapper?.Data?.ToList() ?? new();
        }
        catch { return new(); }
    }

    public async Task<(bool Ok, string? Error)> CreateFolderAsync(CreateFolderRequest req)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/folders", req, _writeOptions);
        if (resp.IsSuccessStatusCode) return (true, null);
        var err = await TryExtractError(resp);
        return (false, err);
    }

    public async Task<bool> DeleteFolderAsync(Guid folderId)
    { var r = await _http.DeleteAsync($"api/bff/folders/{folderId}"); return r.IsSuccessStatusCode; }

    public async Task<bool> UploadFileRawAsync(MultipartFormDataContent content)
    {
        var resp = await _http.PostAsync("api/bff/files/upload?accessLevel=public", content);
        return resp.IsSuccessStatusCode;
    }

    // ═══ TABLE QR ═══

    public async Task<string?> GenerateTableQrTokenAsync(Guid tableId)
    {
        var resp = await _http.PostAsJsonAsync($"api/bff/tables/{tableId}/generate-qr", new { }, _writeOptions);
        if (resp.IsSuccessStatusCode)
        {
            var json = await resp.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>(_jsonOptions);
            if (json.TryGetProperty("data", out var data) && data.TryGetProperty("qrToken", out var token))
                return token.GetString();
        }
        return null;
    }

    public record TableByTokenInfo(Guid Id, Guid ShopId, string TableNumber, int Capacity, string? Zone);

    public async Task<TableByTokenInfo?> GetTableByTokenAsync(string token)
    {
        var resp = await _http.GetAsync($"api/bff/tables/by-token/{token}");
        if (resp.IsSuccessStatusCode)
        {
            var json = await resp.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>(_jsonOptions);
            if (json.TryGetProperty("data", out var data))
                return System.Text.Json.JsonSerializer.Deserialize<TableByTokenInfo>(data.GetRawText(), _jsonOptions);
        }
        return null;
    }

    // ═══ RESERVATIONS ═══

    public record ReservationInfo(Guid Id, Guid ShopId, Guid? TableId, string GuestName, string? Phone,
        int PartySize, DateTime ReservationTime, string Status, string? Note, DateTime CreatedAt);
    public record CreateReservationRequest2(Guid ShopId, string GuestName, int PartySize, DateTime ReservationTime,
        string? Phone = null, Guid? TableId = null, string? Note = null);

    public async Task<List<ReservationInfo>> GetReservationsAsync(Guid shopId, string? date = null)
    {
        var url = $"api/bff/shops/{shopId}/reservations";
        if (!string.IsNullOrEmpty(date)) url += $"?date={Uri.EscapeDataString(date)}";
        return await GetListFromApiAsync<ReservationInfo>(url);
    }

    public async Task<bool> CreateReservationAsync(CreateReservationRequest2 req)
    { var r = await _http.PostAsJsonAsync("api/bff/reservations", req, _writeOptions); return r.IsSuccessStatusCode; }

    public async Task<bool> UpdateReservationStatusAsync(Guid reservationId, string status)
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        var request = new HttpRequestMessage(HttpMethod.Patch, $"api/bff/reservations/{reservationId}/status")
        {
            Content = JsonContent.Create(new { status }, options: _writeOptions)
        };
        var r = await _http.SendAsync(request);
        return r.IsSuccessStatusCode;
    }

    // ═══ TABLE STATUS ═══

    public async Task<bool> UpdateTableStatusAsync(Guid tableId, string status)
    {
        // EN: Auth via httpOnly cookie — no manual token attachment needed.
        // VI: Auth qua httpOnly cookie — không cần gắn token thủ công.
        var request = new HttpRequestMessage(HttpMethod.Patch, $"api/bff/tables/{tableId}/status")
        {
            Content = JsonContent.Create(new { status }, options: _writeOptions)
        };
        var r = await _http.SendAsync(request);
        return r.IsSuccessStatusCode;
    }

    // ═══ SESSIONS ═══

    public record SessionInfo(Guid Id, Guid TableId, Guid ShopId, int GuestCount, DateTime StartedAt, DateTime? ClosedAt, string Status);

    public async Task<SessionInfo?> OpenSessionAsync(Guid tableId, Guid shopId, int guestCount = 1)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/sessions",
            new { tableId, shopId, guestCount }, _writeOptions);
        if (resp.IsSuccessStatusCode)
        {
            var json = await resp.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>(_jsonOptions);
            if (json.TryGetProperty("data", out var data))
            {
                var sessionId = data.GetProperty("sessionId").GetGuid();
                return new SessionInfo(sessionId, tableId, shopId, guestCount, DateTime.UtcNow, null, "Open");
            }
        }
        return null;
    }

    public async Task<SessionInfo?> GetSessionAsync(Guid sessionId)
    {
        var resp = await _http.GetAsync($"api/bff/sessions/{sessionId}");
        if (resp.IsSuccessStatusCode)
        {
            var json = await resp.Content.ReadFromJsonAsync<System.Text.Json.JsonElement>(_jsonOptions);
            if (json.TryGetProperty("data", out var data))
                return System.Text.Json.JsonSerializer.Deserialize<SessionInfo>(data.GetRawText(), _jsonOptions);
        }
        return null;
    }

    public async Task<bool> CloseSessionAsync(Guid sessionId)
    {
        var resp = await _http.PostAsJsonAsync($"api/bff/sessions/{sessionId}/close", new { }, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    /// <summary>
    /// EN: Extend an active session by additional minutes. Uses table update to persist extended time.
    /// VI: Gia hạn phiên đang hoạt động thêm số phút. Dùng table update để lưu thời gian gia hạn.
    /// </summary>
    public async Task<bool> ExtendSessionAsync(Guid tableId, int additionalMinutes)
    {
        var resp = await _http.PostAsJsonAsync($"api/bff/tables/{tableId}/extend",
            new { additionalMinutes }, _writeOptions);
        // EN: Fallback — if dedicated extend endpoint doesn't exist, use PATCH status to signal extension
        // VI: Dự phòng — nếu endpoint extend chưa có, dùng PATCH status để báo hiệu gia hạn
        if ((int)resp.StatusCode == 404)
        {
            var patch = new HttpRequestMessage(HttpMethod.Patch, $"api/bff/tables/{tableId}/status")
            {
                Content = JsonContent.Create(new { status = "occupied", extendMinutes = additionalMinutes }, options: _writeOptions)
            };
            var fallback = await _http.SendAsync(patch);
            return fallback.IsSuccessStatusCode;
        }
        return resp.IsSuccessStatusCode;
    }

    // ═══ SHOP PUBLISH (draft → active) ═══

    public async Task<bool> PublishShopAsync(Guid shopId)
    {
        var resp = await _http.PostAsync($"api/bff/shops/{shopId}/publish", null);
        return resp.IsSuccessStatusCode;
    }

    // ═══ EOD REPORTS / DAILY CLOSE ═══

    // EN: EOD report DTO — matches backend EodReportDto
    // VI: DTO bao cao cuoi ngay — khop voi EodReportDto backend
    public record EodReportInfo(
        DateTime ReportDate, Guid ShopId,
        int TotalOrders, int CompletedOrders, int CancelledOrders,
        decimal TotalRevenue, decimal CashRevenue, decimal CardRevenue, decimal OnlineRevenue,
        decimal DiscountTotal,
        List<EodPaymentBreakdownInfo> PaymentBreakdown,
        List<EodTopItemInfo> TopItems,
        List<EodHourlyRevenueInfo> HourlyRevenue);

    public record EodPaymentBreakdownInfo(string Method, int Count, decimal Amount);
    public record EodTopItemInfo(string ItemName, int Quantity, decimal Revenue);
    public record EodHourlyRevenueInfo(int Hour, int OrderCount, decimal Revenue);

    // EN: Close day result DTO
    // VI: DTO ket qua dong ngay
    public record CloseDayResultInfo(bool Success, EodReportInfo? Report, string? Message, int PendingOrderCount);

    /// <summary>
    /// EN: Get End-of-Day report for a shop on a specific date.
    /// VI: Lay bao cao cuoi ngay cho shop vao ngay cu the.
    /// </summary>
    public async Task<EodReportInfo?> GetEodReportAsync(Guid shopId, DateTime date)
    {
        var url = $"api/bff/reports/eod?shopId={shopId}&date={date:yyyy-MM-dd}";
        return await GetObjectFromApiAsync<EodReportInfo>(url);
    }

    /// <summary>
    /// EN: Close the business day and generate final EOD report.
    /// VI: Dong ngay kinh doanh va tao bao cao cuoi ngay cuoi cung.
    /// </summary>
    public async Task<CloseDayResultInfo?> CloseDayAsync(Guid shopId, DateTime date)
    {
        var url = "api/bff/reports/close-day";
        return await PostAndGetAsync<CloseDayResultInfo>(url, new { shopId, closeDate = date.ToString("yyyy-MM-dd") });
    }

    // ═══ ADVANCED REPORTS — REVENUE ANALYTICS & STAFF PERFORMANCE ═══

    // EN: Revenue analytics DTO — matches backend RevenueAnalyticsDto
    // VI: DTO phan tich doanh thu — khop voi RevenueAnalyticsDto backend
    public record RevenueAnalyticsInfo(
        decimal TotalRevenue, decimal PreviousPeriodRevenue, decimal GrowthPercentage,
        int TotalOrders, decimal AverageOrderValue,
        List<RevenueTrendInfo> Trends,
        List<PaymentMethodStatsInfo> PaymentMethods,
        List<TopProductAnalyticsInfo> TopProducts,
        List<VerticalRevenueInfo> VerticalBreakdown);

    public class RevenueTrendInfo
    {
        public DateTime Date { get; set; }
        public decimal Revenue { get; set; }
        public int OrderCount { get; set; }
    }

    public class PaymentMethodStatsInfo
    {
        public string Method { get; set; } = string.Empty;
        public decimal Amount { get; set; }
        public int Count { get; set; }
        public decimal Percentage { get; set; }
    }

    public class TopProductAnalyticsInfo
    {
        public string Name { get; set; } = string.Empty;
        public int QuantitySold { get; set; }
        public decimal Revenue { get; set; }
    }

    public class VerticalRevenueInfo
    {
        public string Vertical { get; set; } = string.Empty;
        public decimal Revenue { get; set; }
        public int OrderCount { get; set; }
    }

    // EN: Staff performance DTO — matches backend StaffPerformanceDto
    // VI: DTO hieu suat nhan vien — khop voi StaffPerformanceDto backend
    public record StaffPerformanceInfo(List<StaffMetricsInfo> Staff, StaffMetricsInfo ShopAverage);

    public class StaffMetricsInfo
    {
        public Guid? StaffId { get; set; }
        public string StaffName { get; set; } = string.Empty;
        public int OrdersHandled { get; set; }
        public decimal TotalRevenue { get; set; }
        public decimal AverageOrderValue { get; set; }
        public int CompletedOrders { get; set; }
        public int CancelledOrders { get; set; }
        public double CompletionRate { get; set; }
        public double AverageHandlingTimeMinutes { get; set; }
    }

    /// <summary>
    /// EN: Get advanced revenue analytics for a shop over a date range.
    /// VI: Lay phan tich doanh thu nang cao cho shop trong khoang thoi gian.
    /// </summary>
    public async Task<RevenueAnalyticsInfo?> GetRevenueAnalyticsAsync(Guid shopId, DateTime startDate, DateTime endDate, string period = "daily")
    {
        var url = $"api/bff/reports/revenue-analytics?shopId={shopId}&startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}&period={Uri.EscapeDataString(period)}";
        return await GetObjectFromApiAsync<RevenueAnalyticsInfo>(url);
    }

    /// <summary>
    /// EN: Get staff performance metrics for a shop over a date range.
    /// VI: Lay chi so hieu suat nhan vien cho shop trong khoang thoi gian.
    /// </summary>
    public async Task<StaffPerformanceInfo?> GetStaffPerformanceAsync(Guid shopId, DateTime startDate, DateTime endDate)
    {
        var url = $"api/bff/reports/staff-performance?shopId={shopId}&startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}";
        return await GetObjectFromApiAsync<StaffPerformanceInfo>(url);
    }

    // ═══ RETAIL POS — BARCODE LOOKUP, STOCK LEVELS, RETURNS/EXCHANGES ═══

    // EN: Product lookup info from catalog-service (barcode/SKU search).
    // VI: Thong tin san pham tra cuu tu catalog-service (tim theo ma vach/SKU).
    public record ProductLookupInfo(Guid Id, string Name, string? Sku, string? Barcode, decimal Price, string? ImageUrl);

    // EN: Stock level info from inventory-service (bulk stock check).
    // VI: Thong tin ton kho tu inventory-service (kiem tra hang loat).
    public record StockLevelInfo(Guid ProductId, int Available, int Reserved, int Minimum, bool IsLowStock);

    // EN: Return item info — items being returned from an order.
    // VI: Thong tin san pham tra — san pham dang tra tu don hang.
    public record ReturnItemInfo(Guid OrderItemId, int Quantity, string? Reason);

    // EN: Exchange item info — new items replacing returned items.
    // VI: Thong tin san pham doi — san pham moi thay the san pham tra.
    public record ExchangeItemInfo(Guid ProductId, int Quantity, decimal UnitPrice);

    // EN: Return order info — history of returns for an order.
    // VI: Thong tin don tra — lich su tra hang cua don hang.
    public record ReturnOrderInfo(Guid Id, DateTime ReturnedAt, string Reason, decimal TotalAmount, List<ReturnItemInfo> Items);

    /// <summary>
    /// EN: Lookup product by barcode or SKU from catalog-service.
    /// VI: Tra cuu san pham theo ma vach hoac SKU tu catalog-service.
    /// </summary>
    public async Task<ProductLookupInfo?> LookupProductByBarcodeAsync(Guid shopId, string barcode)
        => await GetObjectFromApiAsync<ProductLookupInfo>($"api/bff/shops/{shopId}/products/lookup?barcode={Uri.EscapeDataString(barcode)}");

    /// <summary>
    /// EN: Bulk stock check for multiple products from inventory-service.
    /// VI: Kiem tra ton kho hang loat cho nhieu san pham tu inventory-service.
    /// </summary>
    public async Task<List<StockLevelInfo>> GetStockLevelsAsync(Guid shopId, List<Guid> productIds)
    {
        var resp = await _http.PostAsJsonAsync($"api/bff/shops/{shopId}/inventory/stock-levels",
            new { productIds }, _writeOptions);
        if (!resp.IsSuccessStatusCode) return new();
        var json = await resp.Content.ReadAsStringAsync();
        if (string.IsNullOrWhiteSpace(json)) return new();
        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        if (root.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Array)
            return JsonSerializer.Deserialize<List<StockLevelInfo>>(data.GetRawText(), _jsonOptions) ?? new();
        if (root.ValueKind == JsonValueKind.Array)
            return JsonSerializer.Deserialize<List<StockLevelInfo>>(json, _jsonOptions) ?? new();
        return new();
    }

    /// <summary>
    /// EN: Create a return for an order (process refund).
    /// VI: Tao don tra hang cho don hang (xu ly hoan tien).
    /// </summary>
    public async Task<(bool Ok, string? Error)> CreateReturnAsync(Guid shopId, Guid originalOrderId, List<ReturnItemInfo> items, string reason)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/orders/returns",
            new { shopId, originalOrderId, items, reason }, _writeOptions);
        if (resp.IsSuccessStatusCode) return (true, null);
        return (false, await TryExtractError(resp));
    }

    /// <summary>
    /// EN: Create an exchange for an order (return + new items).
    /// VI: Tao don doi hang cho don hang (tra + san pham moi).
    /// </summary>
    public async Task<(bool Ok, string? Error)> CreateExchangeAsync(Guid shopId, Guid originalOrderId,
        List<ReturnItemInfo> returnItems, List<ExchangeItemInfo> newItems, string reason)
    {
        var resp = await _http.PostAsJsonAsync("api/bff/orders/exchanges",
            new { shopId, originalOrderId, returnItems, newItems, reason }, _writeOptions);
        if (resp.IsSuccessStatusCode) return (true, null);
        return (false, await TryExtractError(resp));
    }

    /// <summary>
    /// EN: Get return history for a specific order.
    /// VI: Lay lich su tra hang cho don hang cu the.
    /// </summary>
    public async Task<List<ReturnOrderInfo>> GetOrderReturnsAsync(Guid orderId)
        => await GetListFromApiAsync<ReturnOrderInfo>($"api/bff/orders/{orderId}/returns");

    // ═══ CAFE — STAMP CARDS (membership-service) ═══

    /// <summary>
    /// EN: Stamp card info — customer's current stamp card state.
    /// VI: Thong tin the tem — trang thai the tem hien tai cua khach hang.
    /// </summary>
    public record StampCardInfo(Guid Id, Guid MemberId, string CardName, int TotalStampsRequired,
        int CurrentStamps, bool IsCompleted, bool RewardClaimed);

    /// <summary>
    /// EN: Barista queue item — a single drink in the preparation queue.
    /// VI: Mon trong hang doi barista — mot ly trong hang doi pha che.
    /// </summary>
    public record BaristaQueueItemInfo(Guid Id, string DrinkName, string? Customizations,
        string Status, string? AssignedTo, int EstimatedMinutes, int Priority,
        DateTime CreatedAt, DateTime? StartedAt);

    /// <summary>
    /// EN: Barista queue stats — aggregate counts for the queue dashboard.
    /// VI: Thong ke hang doi barista — so lieu tong hop cho bang dieu khien hang doi.
    /// </summary>
    public record BaristaQueueStatsInfo(int TotalQueued, int Preparing, int Ready, double AvgPrepTimeMinutes);

    /// <summary>
    /// EN: Get a customer's stamp card for a specific shop.
    /// VI: Lay the tem cua khach hang cho mot cua hang cu the.
    /// </summary>
    public async Task<StampCardInfo?> GetStampCardAsync(Guid shopId, Guid memberId)
        => await GetObjectFromApiAsync<StampCardInfo>($"api/bff/cafe/{shopId}/stamp-cards/{memberId}");

    /// <summary>
    /// EN: Add a stamp to the customer's card (optionally linked to an order).
    /// VI: Them tem vao the khach hang (tuy chon lien ket voi don hang).
    /// </summary>
    public async Task<StampCardInfo?> AddStampAsync(Guid shopId, Guid memberId, Guid? orderId = null)
        => await PostAndGetAsync<StampCardInfo>($"api/bff/cafe/{shopId}/stamp-cards/{memberId}/stamps",
            new { orderId });

    /// <summary>
    /// EN: Claim the free-drink reward when the stamp card is completed.
    /// VI: Nhan thuong ly mien phi khi the tem da day.
    /// </summary>
    public async Task<bool> ClaimRewardAsync(Guid stampCardId)
        => await PostAsync($"api/bff/cafe/stamp-cards/{stampCardId}/claim", new { });

    /// <summary>
    /// EN: Get all items currently in the barista queue for a shop.
    /// VI: Lay tat ca mon dang trong hang doi barista cua cua hang.
    /// </summary>
    public async Task<List<BaristaQueueItemInfo>> GetBaristaQueueAsync(Guid shopId)
        => await GetListFromApiAsync<BaristaQueueItemInfo>($"api/bff/cafe/{shopId}/barista-queue");

    /// <summary>
    /// EN: Get aggregate stats for the barista queue.
    /// VI: Lay thong ke tong hop cho hang doi barista.
    /// </summary>
    public async Task<BaristaQueueStatsInfo?> GetBaristaQueueStatsAsync(Guid shopId)
        => await GetObjectFromApiAsync<BaristaQueueStatsInfo>($"api/bff/cafe/{shopId}/barista-queue/stats");

    /// <summary>
    /// EN: Add a drink to the barista queue.
    /// VI: Them ly vao hang doi barista.
    /// </summary>
    public record QueueDrinkRequest(string DrinkName, string? Customizations, int EstimatedMinutes, int Priority = 0, Guid? OrderId = null);

    public async Task<BaristaQueueItemInfo?> QueueDrinkAsync(Guid shopId, QueueDrinkRequest req)
        => await PostAndGetAsync<BaristaQueueItemInfo>($"api/bff/cafe/{shopId}/barista-queue", req);

    /// <summary>
    /// EN: Start preparing a drink — assigns barista name.
    /// VI: Bat dau pha che — gan ten barista.
    /// </summary>
    public async Task<bool> StartPreparingDrinkAsync(Guid queueItemId, string baristaName)
        => await PostAsync($"api/bff/cafe/barista-queue/{queueItemId}/start",
            new { baristaName });

    /// <summary>
    /// EN: Mark a drink as ready for pickup.
    /// VI: Danh dau ly da san sang de lay.
    /// </summary>
    public async Task<bool> MarkDrinkReadyAsync(Guid queueItemId)
        => await PostAsync($"api/bff/cafe/barista-queue/{queueItemId}/ready", new { });

    /// <summary>
    /// EN: Mark a drink as delivered to the customer.
    /// VI: Danh dau ly da giao cho khach hang.
    /// </summary>
    public async Task<bool> MarkDrinkDeliveredAsync(Guid queueItemId)
        => await PostAsync($"api/bff/cafe/barista-queue/{queueItemId}/delivered", new { });

    // ═══ SERVICE HEALTH CHECK ═══

    public record ServiceHealthInfo(string Name, string Icon, bool IsOnline, int? LatencyMs);

    private static readonly (string Name, string Icon, string HealthPath)[] _healthEndpoints = new[]
    {
        ("IAM Service", "shield", "api/iam/health"),
        ("Merchant Service", "store", "api/merchants/health"),
        ("Catalog Service", "package", "api/catalog/health"),
        ("Order Service", "shopping-bag", "api/orders/health"),
    };

    public async Task<List<ServiceHealthInfo>> CheckServicesHealthAsync()
    {
        var results = new List<ServiceHealthInfo>();
        var tasks = _healthEndpoints.Select(async svc =>
        {
            var sw = System.Diagnostics.Stopwatch.StartNew();
            try
            {
                using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                var resp = await _http.GetAsync(svc.HealthPath, cts.Token);
                sw.Stop();
                return new ServiceHealthInfo(svc.Name, svc.Icon, resp.IsSuccessStatusCode, (int)sw.ElapsedMilliseconds);
            }
            catch
            {
                sw.Stop();
                return new ServiceHealthInfo(svc.Name, svc.Icon, false, null);
            }
        }).ToArray();

        return (await Task.WhenAll(tasks)).ToList();
    }

    // ═══ PUBLIC MENU (NO AUTH) ═══

    // EN: Public DTOs for customer-facing QR menu (no authentication required).
    // VI: DTO công khai cho menu QR dành cho khách hàng (không cần xác thực).
    public record PublicShopInfo(Guid Id, string Name, string? Address, string? LogoUrl, string? Phone, string? Description);
    public record PublicMenuCategory(Guid Id, string Name, string? Description, int DisplayOrder, List<PublicMenuItem> Items);
    public record PublicMenuItem(Guid Id, string Name, string? Description, decimal Price, string? ImageUrl, bool IsAvailable);

    /// <summary>
    /// EN: Get public shop info (no auth) — for customer QR menu header.
    /// VI: Lấy thông tin shop công khai (không cần auth) — cho header menu QR khách hàng.
    /// </summary>
    public async Task<PublicShopInfo?> GetPublicShopInfoAsync(Guid shopId)
    {
        // EN: No AttachToken() — this is a public endpoint.
        // VI: Không AttachToken() — đây là endpoint công khai.
        var resp = await _http.GetAsync($"api/public/shops/{shopId}");
        if (!resp.IsSuccessStatusCode) return null;
        var json = await resp.Content.ReadAsStringAsync();
        if (string.IsNullOrWhiteSpace(json)) return null;

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;
        if (root.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Object)
            return JsonSerializer.Deserialize<PublicShopInfo>(data.GetRawText(), _jsonOptions);
        if (root.ValueKind == JsonValueKind.Object)
            return JsonSerializer.Deserialize<PublicShopInfo>(json, _jsonOptions);
        return null;
    }

    /// <summary>
    /// EN: Get public menu with categories and items (no auth) — for customer QR menu.
    /// VI: Lấy menu công khai với danh mục và sản phẩm (không cần auth) — cho menu QR khách hàng.
    /// </summary>
    public async Task<List<PublicMenuCategory>> GetPublicMenuAsync(Guid shopId)
    {
        // EN: No AttachToken() — this is a public endpoint.
        // VI: Không AttachToken() — đây là endpoint công khai.
        var resp = await _http.GetAsync($"api/public/shops/{shopId}/menu");
        if (!resp.IsSuccessStatusCode) return new();
        var json = await resp.Content.ReadAsStringAsync();
        if (string.IsNullOrWhiteSpace(json)) return new();

        using var doc = JsonDocument.Parse(json);
        var root = doc.RootElement;

        // Handle { "data": [...] } or plain array
        if (root.TryGetProperty("data", out var data) && data.ValueKind == JsonValueKind.Array)
            return JsonSerializer.Deserialize<List<PublicMenuCategory>>(data.GetRawText(), _jsonOptions) ?? new();
        if (root.ValueKind == JsonValueKind.Array)
            return JsonSerializer.Deserialize<List<PublicMenuCategory>>(json, _jsonOptions) ?? new();
        // Handle { "data": { "items": [...] } }
        if (root.TryGetProperty("data", out var dataObj) && dataObj.ValueKind == JsonValueKind.Object
            && dataObj.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Array)
            return JsonSerializer.Deserialize<List<PublicMenuCategory>>(items.GetRawText(), _jsonOptions) ?? new();

        return new();
    }

    // ═══ AI CHAT ═══

    public record AiChatConfigInfo(string? Provider, string? ApiKeyMasked, string? Model, string? SystemPrompt, bool Enabled);
    public record AiChatMessageDto(string Role, string Content);
    public record AiChatRequestDto(Guid ShopId, List<AiChatMessageDto> Messages);
    public record AiChatResponseDto(string? Content, List<string>? ToolsUsed);

    public async Task<AiChatConfigInfo?> GetAiChatConfigAsync(Guid shopId)
    {
        try { return await _http.GetFromJsonAsync<AiChatConfigInfo>($"api/bff/ai/config?shopId={shopId}", _jsonOptions); }
        catch { return null; }
    }

    public async Task<bool> UpdateAiChatConfigAsync(Guid shopId, object config)
    {
        var resp = await _http.PutAsJsonAsync($"api/bff/ai/config?shopId={shopId}", config, _writeOptions);
        return resp.IsSuccessStatusCode;
    }

    public async Task<AiChatResponseDto?> SendAiChatAsync(AiChatRequestDto request)
    {
        try
        {
            var resp = await _http.PostAsJsonAsync("api/bff/ai/chat", request, _writeOptions);
            if (!resp.IsSuccessStatusCode) return new AiChatResponseDto("Lỗi: " + resp.ReasonPhrase, null);
            return await resp.Content.ReadFromJsonAsync<AiChatResponseDto>(_jsonOptions);
        }
        catch (Exception ex) { return new AiChatResponseDto("Lỗi: " + ex.Message, null); }
    }
}
