using Meezi.API.Models.Tables;

using Meezi.Core.Entities;

using Meezi.Core.Enums;

using Meezi.Infrastructure.Data;

using Microsoft.EntityFrameworkCore;

using QRCoder;



namespace Meezi.API.Services;



public interface ITableService

{

    Task<IReadOnlyList<TableBoardDto>> GetTableBoardAsync(

        string cafeId,

        bool activeOnly = true,

        string? branchId = null,

        CancellationToken cancellationToken = default);



    Task<IReadOnlyList<TableDto>> GetTablesAsync(

        string cafeId,

        string? branchId = null,

        CancellationToken cancellationToken = default);



    Task<TableDto?> CreateTableAsync(string cafeId, CreateTableRequest request, CancellationToken cancellationToken = default);

    Task<TableDto?> PatchTableAsync(string cafeId, string tableId, PatchTableRequest request, CancellationToken cancellationToken = default);

    Task<TableBoardDto?> SetTableCleaningAsync(

        string cafeId,

        string tableId,

        bool isCleaning,

        CancellationToken cancellationToken = default);

    Task<BranchTableOperationResult<object>> DeleteTableAsync(
        string cafeId,
        string tableId,
        CancellationToken cancellationToken = default);

    Task<IReadOnlyList<TableBoardDto>> GetBranchTableBoardAsync(

        string cafeId,

        string branchId,

        bool activeOnly = true,

        CancellationToken cancellationToken = default);



    Task<IReadOnlyList<TableDto>?> GetBranchTablesAsync(
        string cafeId,
        string branchId,
        CancellationToken cancellationToken = default);



    Task<BranchTableOperationResult<TableDto>> CreateBranchTableAsync(

        string cafeId,

        string branchId,

        CreateBranchTableRequest request,

        CancellationToken cancellationToken = default);



    Task<BranchTableOperationResult<TableDto>> PatchBranchTableAsync(

        string cafeId,

        string branchId,

        string tableId,

        PatchBranchTableRequest request,

        CancellationToken cancellationToken = default);



    Task<BranchTableOperationResult<object>> DeleteBranchTableAsync(

        string cafeId,

        string branchId,

        string tableId,

        CancellationToken cancellationToken = default);



    Task<IReadOnlyList<TableSectionDto>?> GetBranchSectionsAsync(

        string cafeId,

        string branchId,

        CancellationToken cancellationToken = default);



    Task<BranchTableOperationResult<TableSectionDto>> CreateBranchSectionAsync(

        string cafeId,

        string branchId,

        CreateTableSectionRequest request,

        CancellationToken cancellationToken = default);



    Task<BranchTableOperationResult<TableSectionDto>> PatchBranchSectionAsync(

        string cafeId,

        string branchId,

        string sectionId,

        PatchTableSectionRequest request,

        CancellationToken cancellationToken = default);



    Task<BranchTableOperationResult<object>> DeleteBranchSectionAsync(

        string cafeId,

        string branchId,

        string sectionId,

        CancellationToken cancellationToken = default);



    Task<bool> CanAccessBranchAsync(

        string cafeId,

        string branchId,

        string? userId,

        EmployeeRole? role,

        CancellationToken cancellationToken = default);



    Task<QrResolveResponse?> ResolveQrAsync(string qrCode, CancellationToken cancellationToken = default);

    Task<byte[]?> GetQrPngAsync(string cafeId, string tableId, CancellationToken cancellationToken = default);

}



public class TableService : ITableService

{

    private static readonly OrderStatus[] OpenOrderStatuses =

    [

        OrderStatus.Pending,

        OrderStatus.Confirmed,

        OrderStatus.Preparing,

        OrderStatus.Ready

    ];



    private readonly AppDbContext _db;

    private readonly IConfiguration _configuration;

    private readonly IKdsNotifier _kdsNotifier;

    private readonly IBranchIdentityService _identity;



    public TableService(
        AppDbContext db,
        IConfiguration configuration,
        IKdsNotifier kdsNotifier,
        IBranchIdentityService identity)

    {

        _db = db;

        _configuration = configuration;

        _kdsNotifier = kdsNotifier;

        _identity = identity;

    }



    public async Task<IReadOnlyList<TableBoardDto>> GetTableBoardAsync(

        string cafeId,

        bool activeOnly = true,

        string? branchId = null,

        CancellationToken cancellationToken = default)

    {

        var query = _db.Tables.Where(t => t.CafeId == cafeId);

        if (!string.IsNullOrEmpty(branchId))

            query = query.Where(t => t.BranchId == branchId);

        if (activeOnly)

            query = query.Where(t => t.IsActive);



        var tables = await LoadTablesOrderedAsync(query, cancellationToken);

        return await BuildBoardDtosAsync(cafeId, tables, cancellationToken);

    }



    public Task<IReadOnlyList<TableBoardDto>> GetBranchTableBoardAsync(

        string cafeId,

        string branchId,

        bool activeOnly = true,

        CancellationToken cancellationToken = default) =>

        GetTableBoardAsync(cafeId, activeOnly, branchId, cancellationToken);



    public async Task<IReadOnlyList<TableDto>> GetTablesAsync(

        string cafeId,

        string? branchId = null,

        CancellationToken cancellationToken = default)

    {

        var query = _db.Tables.Where(t => t.CafeId == cafeId && t.IsActive);

        if (!string.IsNullOrEmpty(branchId))

            query = query.Where(t => t.BranchId == branchId);

        var tables = await LoadTablesOrderedAsync(query, cancellationToken);

        return tables.Select(ToDto).ToList();

    }



    public async Task<IReadOnlyList<TableDto>?> GetBranchTablesAsync(
        string cafeId,
        string branchId,
        CancellationToken cancellationToken = default)
    {
        if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))
            return null;



        return await GetTablesAsync(cafeId, branchId, cancellationToken);

    }



    public async Task<TableDto?> CreateTableAsync(

        string cafeId,

        CreateTableRequest request,

        CancellationToken cancellationToken = default)

    {

        var branchId = request.BranchId;

        if (string.IsNullOrEmpty(branchId))

            branchId = await GetDefaultBranchIdAsync(cafeId, cancellationToken);

        if (branchId is null) return null;



        if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))

            return null;



        var entity = new Table

        {

            CafeId = cafeId,

            BranchId = branchId,

            Number = request.Number.Trim(),

            Capacity = request.Capacity,

            Floor = request.Floor?.Trim(),

            QrCode = Guid.NewGuid().ToString("N"),

            IsActive = request.IsActive

        };



        _db.Tables.Add(entity);

        await _db.SaveChangesAsync(cancellationToken);

        return ToDto(entity);

    }



    public async Task<BranchTableOperationResult<TableDto>> CreateBranchTableAsync(

        string cafeId,

        string branchId,

        CreateBranchTableRequest request,

        CancellationToken cancellationToken = default)

    {

        if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))

            return Fail<TableDto>("BRANCH_NOT_FOUND", "Branch not found.");



        if (!string.IsNullOrEmpty(request.SectionId))

        {

            var sectionOk = await _db.TableSections.AnyAsync(

                s => s.Id == request.SectionId && s.CafeId == cafeId && s.BranchId == branchId,

                cancellationToken);

            if (!sectionOk)

                return Fail<TableDto>("SECTION_NOT_FOUND", "Section not found.");

        }



        var entity = new Table

        {

            CafeId = cafeId,

            BranchId = branchId,

            SectionId = request.SectionId,

            Number = request.Number.Trim(),

            Capacity = request.Capacity,

            Floor = request.Floor?.Trim(),

            SortOrder = request.SortOrder,

            QrCode = Guid.NewGuid().ToString("N"),

            IsActive = request.IsActive

        };



        _db.Tables.Add(entity);

        await _db.SaveChangesAsync(cancellationToken);

        await _db.Entry(entity).Reference(t => t.Section).LoadAsync(cancellationToken);

        return Ok(ToDto(entity));

    }



    public async Task<TableDto?> PatchTableAsync(

        string cafeId,

        string tableId,

        PatchTableRequest request,

        CancellationToken cancellationToken = default)

    {

        var entity = await _db.Tables

            .Include(t => t.Section)

            .FirstOrDefaultAsync(t => t.Id == tableId && t.CafeId == cafeId, cancellationToken);

        if (entity is null) return null;



        if (request.Number is not null) entity.Number = request.Number.Trim();

        if (request.Capacity.HasValue) entity.Capacity = request.Capacity.Value;

        if (request.Floor is not null) entity.Floor = request.Floor.Trim();

        if (request.BranchId is not null && !string.IsNullOrEmpty(request.BranchId))

        {

            if (!await BranchExistsAsync(cafeId, request.BranchId, cancellationToken))

                return null;

            entity.BranchId = request.BranchId;

        }

        if (request.ImageUrl is not null)

            entity.ImageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) ? null : request.ImageUrl;

        if (request.VideoUrl is not null)

            entity.VideoUrl = string.IsNullOrWhiteSpace(request.VideoUrl) ? null : request.VideoUrl;

        if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value;

        if (request.IsCleaning.HasValue) entity.IsCleaning = request.IsCleaning.Value;



        await _db.SaveChangesAsync(cancellationToken);

        return ToDto(entity);

    }



    public async Task<BranchTableOperationResult<TableDto>> PatchBranchTableAsync(

        string cafeId,

        string branchId,

        string tableId,

        PatchBranchTableRequest request,

        CancellationToken cancellationToken = default)

    {

        var entity = await _db.Tables

            .Include(t => t.Section)

            .FirstOrDefaultAsync(

                t => t.Id == tableId && t.CafeId == cafeId && t.BranchId == branchId,

                cancellationToken);

        if (entity is null)

            return Fail<TableDto>("TABLE_NOT_FOUND", "Table not found.");



        if (request.SectionId is not null)

        {

            if (string.IsNullOrEmpty(request.SectionId))

                entity.SectionId = null;

            else

            {

                var sectionOk = await _db.TableSections.AnyAsync(

                    s => s.Id == request.SectionId && s.CafeId == cafeId && s.BranchId == branchId,

                    cancellationToken);

                if (!sectionOk)

                    return Fail<TableDto>("SECTION_NOT_FOUND", "Section not found.");

                entity.SectionId = request.SectionId;

            }

        }



        if (request.Number is not null) entity.Number = request.Number.Trim();

        if (request.Capacity.HasValue) entity.Capacity = request.Capacity.Value;

        if (request.Floor is not null) entity.Floor = request.Floor.Trim();

        if (request.SortOrder.HasValue) entity.SortOrder = request.SortOrder.Value;

        if (request.ImageUrl is not null)

            entity.ImageUrl = string.IsNullOrWhiteSpace(request.ImageUrl) ? null : request.ImageUrl;

        if (request.VideoUrl is not null)

            entity.VideoUrl = string.IsNullOrWhiteSpace(request.VideoUrl) ? null : request.VideoUrl;

        if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value;

        if (request.IsCleaning.HasValue) entity.IsCleaning = request.IsCleaning.Value;



        await _db.SaveChangesAsync(cancellationToken);

        await _db.Entry(entity).Reference(t => t.Section).LoadAsync(cancellationToken);

        return Ok(ToDto(entity));

    }



    public async Task<BranchTableOperationResult<object>> DeleteTableAsync(
        string cafeId,
        string tableId,
        CancellationToken cancellationToken = default)
    {
        var branchId = await _db.Tables
            .Where(t => t.Id == tableId && t.CafeId == cafeId)
            .Select(t => t.BranchId)
            .FirstOrDefaultAsync(cancellationToken);
        if (string.IsNullOrEmpty(branchId))
            return Fail<object>("TABLE_NOT_FOUND", "Table not found.");
        return await DeleteBranchTableAsync(cafeId, branchId, tableId, cancellationToken);
    }

    public async Task<BranchTableOperationResult<object>> DeleteBranchTableAsync(

        string cafeId,

        string branchId,

        string tableId,

        CancellationToken cancellationToken = default)

    {

        var entity = await _db.Tables.FirstOrDefaultAsync(

            t => t.Id == tableId && t.CafeId == cafeId && t.BranchId == branchId,

            cancellationToken);

        if (entity is null)

            return Fail<object>("TABLE_NOT_FOUND", "Table not found.");



        var hasOpenOrder = await _db.Orders.AnyAsync(

            o => o.CafeId == cafeId

                && o.TableId == tableId

                && OpenOrderStatuses.Contains(o.Status),

            cancellationToken);

        if (hasOpenOrder)

            return Fail<object>("TABLE_HAS_OPEN_ORDER", "This table has an open order.");



        entity.DeletedAt = DateTime.UtcNow;

        entity.IsActive = false;

        entity.IsCleaning = false;

        await _db.SaveChangesAsync(cancellationToken);

        await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);

        return Ok<object>(new { tableId });

    }



    public async Task<IReadOnlyList<TableSectionDto>?> GetBranchSectionsAsync(

        string cafeId,

        string branchId,

        CancellationToken cancellationToken = default)

    {

        if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))

            return null;



        var sections = await _db.TableSections
            .Where(s => s.CafeId == cafeId && s.BranchId == branchId)
            .OrderBy(s => s.SortOrder)
            .ThenBy(s => s.Name)
            .ToListAsync(cancellationToken);

        var tableCounts = await _db.Tables
            .Where(t => t.CafeId == cafeId && t.BranchId == branchId && t.SectionId != null)
            .GroupBy(t => t.SectionId!)
            .Select(g => new { SectionId = g.Key, Count = g.Count() })
            .ToDictionaryAsync(x => x.SectionId, x => x.Count, cancellationToken);

        return sections
            .Select(s => new TableSectionDto(
                s.Id,
                s.BranchId,
                s.Name,
                s.SortOrder,
                s.IsActive,
                tableCounts.GetValueOrDefault(s.Id)))
            .ToList();

    }



    public async Task<BranchTableOperationResult<TableSectionDto>> CreateBranchSectionAsync(

        string cafeId,

        string branchId,

        CreateTableSectionRequest request,

        CancellationToken cancellationToken = default)

    {

        if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))

            return Fail<TableSectionDto>("BRANCH_NOT_FOUND", "Branch not found.");



        var entity = new TableSection

        {

            CafeId = cafeId,

            BranchId = branchId,

            Name = request.Name.Trim(),

            SortOrder = request.SortOrder

        };

        _db.TableSections.Add(entity);

        await _db.SaveChangesAsync(cancellationToken);

        return Ok(new TableSectionDto(entity.Id, entity.BranchId, entity.Name, entity.SortOrder, entity.IsActive, 0));

    }



    public async Task<BranchTableOperationResult<TableSectionDto>> PatchBranchSectionAsync(

        string cafeId,

        string branchId,

        string sectionId,

        PatchTableSectionRequest request,

        CancellationToken cancellationToken = default)

    {

        var entity = await _db.TableSections.FirstOrDefaultAsync(

            s => s.Id == sectionId && s.CafeId == cafeId && s.BranchId == branchId,

            cancellationToken);

        if (entity is null)

            return Fail<TableSectionDto>("SECTION_NOT_FOUND", "Section not found.");



        if (request.Name is not null) entity.Name = request.Name.Trim();

        if (request.SortOrder.HasValue) entity.SortOrder = request.SortOrder.Value;

        if (request.IsActive.HasValue) entity.IsActive = request.IsActive.Value;



        await _db.SaveChangesAsync(cancellationToken);

        var tableCount = await _db.Tables.CountAsync(

            t => t.SectionId == sectionId && t.CafeId == cafeId,

            cancellationToken);

        return Ok(new TableSectionDto(entity.Id, entity.BranchId, entity.Name, entity.SortOrder, entity.IsActive, tableCount));

    }



    public async Task<BranchTableOperationResult<object>> DeleteBranchSectionAsync(

        string cafeId,

        string branchId,

        string sectionId,

        CancellationToken cancellationToken = default)

    {

        var entity = await _db.TableSections.FirstOrDefaultAsync(

            s => s.Id == sectionId && s.CafeId == cafeId && s.BranchId == branchId,

            cancellationToken);

        if (entity is null)

            return Fail<object>("SECTION_NOT_FOUND", "Section not found.");



        var hasTables = await _db.Tables.AnyAsync(

            t => t.SectionId == sectionId && t.CafeId == cafeId && t.BranchId == branchId,

            cancellationToken);

        if (hasTables)

            return Fail<object>("TABLE_SECTION_HAS_TABLES", "Section has tables assigned.");



        entity.DeletedAt = DateTime.UtcNow;

        await _db.SaveChangesAsync(cancellationToken);

        return Ok<object>(new { sectionId });

    }



    public async Task<bool> CanAccessBranchAsync(

        string cafeId,

        string branchId,

        string? userId,

        EmployeeRole? role,

        CancellationToken cancellationToken = default)

    {

        if (!await BranchExistsAsync(cafeId, branchId, cancellationToken))

            return false;

        if (role is not EmployeeRole.Manager)

            return true;

        if (string.IsNullOrEmpty(userId))

            return false;

        var employee = await _db.Employees.FirstOrDefaultAsync(

            e => e.Id == userId && e.CafeId == cafeId,

            cancellationToken);

        return employee?.BranchId == branchId;

    }



    public async Task<TableBoardDto?> SetTableCleaningAsync(

        string cafeId,

        string tableId,

        bool isCleaning,

        CancellationToken cancellationToken = default)

    {

        var entity = await _db.Tables.FirstOrDefaultAsync(t => t.Id == tableId && t.CafeId == cafeId, cancellationToken);

        if (entity is null) return null;



        entity.IsCleaning = isCleaning;

        await _db.SaveChangesAsync(cancellationToken);

        await _kdsNotifier.NotifyTableStatusChangedAsync(cafeId, cancellationToken);

        var board = await GetTableBoardAsync(cafeId, activeOnly: false, branchId: entity.BranchId, cancellationToken);

        return board.FirstOrDefault(t => t.Id == tableId);

    }



    public async Task<QrResolveResponse?> ResolveQrAsync(string qrCode, CancellationToken cancellationToken = default)

    {

        var table = await _db.Tables
            .Include(t => t.Cafe)
            .Include(t => t.Branch)
            .FirstOrDefaultAsync(
                t => t.QrCode == qrCode && t.IsActive && t.DeletedAt == null,
                cancellationToken);

        if (table?.Cafe is null || table.Branch is null || !table.Branch.IsActive)
            return null;

        var identity = await _identity.GetEffectiveIdentityAsync(
            table.CafeId,
            table.BranchId,
            cancellationToken);

        return new QrResolveResponse(
            table.CafeId,
            table.Cafe.Slug,
            table.Id,
            table.Number,
            table.Number,
            table.BranchId,
            table.Branch.Name,
            table.Cafe.Name,
            identity?.PrimaryColor ?? "#0F6E56",
            identity?.LogoUrl ?? table.Cafe.LogoUrl,
            identity?.WelcomeText ?? "خوش آمدید",
            identity?.WifiPassword,
            identity?.Address,
            table.IsCleaning);

    }



    public async Task<byte[]?> GetQrPngAsync(string cafeId, string tableId, CancellationToken cancellationToken = default)

    {

        var table = await _db.Tables

            .FirstOrDefaultAsync(t => t.Id == tableId && t.CafeId == cafeId, cancellationToken);

        if (table is null) return null;



        var url = BuildPublicQrUrl(table.QrCode);

        using var generator = new QRCodeGenerator();

        using var data = generator.CreateQrCode(url, QRCodeGenerator.ECCLevel.Q);

        var png = new PngByteQRCode(data);

        return png.GetGraphic(20);

    }



    private static BranchTableOperationResult<T> Ok<T>(T data) =>

        new(true, data, null, null);



    private static BranchTableOperationResult<T> Fail<T>(string code, string message) =>

        new(false, default, code, message);



    private async Task<bool> BranchExistsAsync(string cafeId, string branchId, CancellationToken ct) =>

        await _db.Branches.AnyAsync(b => b.Id == branchId && b.CafeId == cafeId, ct);



    private async Task<string?> GetDefaultBranchIdAsync(string cafeId, CancellationToken ct) =>

        await _db.Branches

            .Where(b => b.CafeId == cafeId && b.IsActive)

            .OrderBy(b => b.CreatedAt)

            .Select(b => b.Id)

            .FirstOrDefaultAsync(ct);



    private static async Task<List<Table>> LoadTablesOrderedAsync(

        IQueryable<Table> query,

        CancellationToken ct)

    {

        var tables = await query.Include(t => t.Section).ToListAsync(ct);

        return tables

            .OrderBy(t => t.Section?.SortOrder ?? int.MaxValue)

            .ThenBy(t => t.SortOrder)

            .ThenBy(t => t.Number, StringComparer.Ordinal)

            .ToList();

    }



    private async Task<IReadOnlyList<TableBoardDto>> BuildBoardDtosAsync(

        string cafeId,

        List<Table> tables,

        CancellationToken cancellationToken)

    {

        if (tables.Count == 0)

            return [];



        var tableIds = tables.Select(t => t.Id).ToList();

        var today = DateOnly.FromDateTime(DateTime.UtcNow);



        var openOrders = await _db.Orders

            .Include(o => o.Customer)

            .Where(o => o.CafeId == cafeId

                && o.TableId != null

                && tableIds.Contains(o.TableId)

                && OpenOrderStatuses.Contains(o.Status))

            .ToListAsync(cancellationToken);



        var reservations = await _db.TableReservations

            .Where(r => r.CafeId == cafeId

                && r.TableId != null

                && tableIds.Contains(r.TableId!)

                && r.Date == today

                && r.Status != ReservationStatus.Cancelled)

            .ToListAsync(cancellationToken);



        return tables.Select(t =>

        {

            var order = openOrders.FirstOrDefault(o => o.TableId == t.Id);

            var reservation = reservations.FirstOrDefault(r => r.TableId == t.Id && order is null);



            var status = t.IsCleaning

                ? TableBoardStatus.Cleaning

                : order is not null

                    ? TableBoardStatus.Busy

                    : reservation is not null

                        ? TableBoardStatus.Reserved

                        : TableBoardStatus.Free;



            TableCurrentOrderSummary? current = null;

            if (order is not null)

            {

                var guestLabel = !string.IsNullOrWhiteSpace(order.GuestName)

                    ? order.GuestName

                    : order.Customer?.Name;

                current = new TableCurrentOrderSummary(
                    order.Id,
                    order.Status,
                    order.Total,
                    guestLabel,
                    order.Source);

            }

            else if (reservation is not null)

            {

                current = new TableCurrentOrderSummary(

                    reservation.Id,

                    OrderStatus.Pending,

                    0,

                    reservation.GuestName);

            }



            return new TableBoardDto(

                t.Id,

                t.BranchId,

                t.SectionId,

                t.Section?.Name,

                t.SortOrder,

                t.Number,

                t.Capacity,

                t.Floor,

                t.QrCode,

                BuildPublicQrUrl(t.QrCode),

                t.ImageUrl,

                t.VideoUrl,

                t.IsActive,

                status,

                current,

                t.IsCleaning);

        }).ToList();

    }



    private TableDto ToDto(Table t) => new(

        t.Id,

        t.BranchId,

        t.SectionId,

        t.Section?.Name,

        t.SortOrder,

        t.Number,

        t.Capacity,

        t.Floor,

        t.QrCode,

        BuildPublicQrUrl(t.QrCode),

        t.ImageUrl,

        t.VideoUrl,

        t.IsActive);



    private string BuildPublicQrUrl(string qrCode)

    {

        var baseUrl = _configuration["App:QrPublicBaseUrl"]?.TrimEnd('/')

            ?? "https://meezi.ir";

        return $"{baseUrl}/q/{qrCode}";

    }

}


