using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces;
using PowderCoating.Infrastructure.Data;

namespace PowderCoating.Infrastructure.Repositories;

/// <summary>
/// Generic repository that provides standard CRUD, search, and pagination operations for any
/// entity that inherits from <see cref="BaseEntity"/>.
/// <para>
/// The repository operates on top of <see cref="ApplicationDbContext"/>, which already applies
/// two global query filters to every query: soft-delete (<c>IsDeleted == false</c>) and
/// multi-tenancy (<c>CompanyId == CurrentCompanyId</c>).  Pass <c>ignoreQueryFilters: true</c>
/// to any read method to bypass <em>both</em> filters simultaneously — use this sparingly and
/// only in SuperAdmin contexts or when generating unique sequential numbers (to avoid gaps).
/// </para>
/// <para>
/// All write methods (Add, Update, Delete, SoftDelete) stage changes in the EF Core change
/// tracker only.  Call <see cref="IUnitOfWork.SaveChangesAsync"/> (or <c>CompleteAsync</c>)
/// after one or more write operations to flush the batch to the database in a single round-trip.
/// </para>
/// </summary>
/// <typeparam name="T">An entity type that inherits <see cref="BaseEntity"/>.</typeparam>
public class Repository<T> : IRepository<T> where T : BaseEntity
{
    /// <summary>The shared DbContext instance; all repositories within the same <see cref="UnitOfWork"/> share this context so they participate in the same change-tracker and transaction.</summary>
    protected readonly ApplicationDbContext _context;
    /// <summary>The EF Core DbSet for <typeparamref name="T"/>; obtained once in the constructor to avoid repeated <c>Set&lt;T&gt;()</c> dictionary lookups.</summary>
    protected readonly DbSet<T> _dbSet;

    /// <summary>
    /// Initialises the repository by resolving the <see cref="DbSet{T}"/> from the shared context.
    /// </summary>
    /// <param name="context">The scoped DbContext provided by <see cref="UnitOfWork"/>.</param>
    public Repository(ApplicationDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    /// <summary>
    /// Retrieves a single entity by its primary key, with optional eager loading.
    /// Returns <c>null</c> if no matching row is found (after applying query filters).
    /// </summary>
    /// <param name="id">The primary key value to look up.</param>
    /// <param name="ignoreQueryFilters">
    /// When <c>true</c>, bypasses both the soft-delete and multi-tenancy global query filters.
    /// Use for SuperAdmin lookups of deleted or cross-tenant records.
    /// </param>
    /// <param name="includes">Optional navigation-property selectors for eager loading (avoids N+1 queries).</param>
    public virtual async Task<T?> GetByIdAsync(int id, bool ignoreQueryFilters = false, params Expression<Func<T, object>>[] includes)
    {
        IQueryable<T> query = _dbSet;

        if (ignoreQueryFilters)
        {
            query = query.IgnoreQueryFilters();
        }

        foreach (var include in includes)
        {
            query = query.Include(include);
        }

        return await query.FirstOrDefaultAsync(e => e.Id == id);
    }

    /// <summary>
    /// Retrieves all entities of type <typeparamref name="T"/>, with optional eager loading.
    /// Global query filters (soft-delete + tenant isolation) are applied unless
    /// <paramref name="ignoreQueryFilters"/> is <c>true</c>.
    /// <para>
    /// Avoid calling this without a filter on large tables — prefer <see cref="FindAsync"/> or
    /// <see cref="GetPagedAsync"/> to limit the result set.
    /// </para>
    /// </summary>
    /// <param name="ignoreQueryFilters">When <c>true</c>, bypasses soft-delete and multi-tenancy filters.</param>
    /// <param name="includes">Optional navigation-property selectors for eager loading.</param>
    public virtual async Task<IEnumerable<T>> GetAllAsync(bool ignoreQueryFilters = false, params Expression<Func<T, object>>[] includes)
    {
        IQueryable<T> query = _dbSet;

        if (ignoreQueryFilters)
        {
            query = query.IgnoreQueryFilters();
        }

        foreach (var include in includes)
        {
            query = query.Include(include);
        }

        return await query.ToListAsync();
    }

    /// <summary>
    /// Returns all entities that satisfy <paramref name="predicate"/>, with optional eager loading.
    /// The predicate is composed with global query filters (unless bypassed), so the final SQL
    /// WHERE clause is the conjunction of both conditions — no tenant data can leak through a
    /// permissive predicate.
    /// </summary>
    /// <param name="predicate">A LINQ expression used to filter results.</param>
    /// <param name="ignoreQueryFilters">When <c>true</c>, bypasses soft-delete and multi-tenancy filters.</param>
    /// <param name="includes">Optional navigation-property selectors for eager loading.</param>
    public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate, bool ignoreQueryFilters = false, params Expression<Func<T, object>>[] includes)
    {
        IQueryable<T> query = _dbSet;

        if (ignoreQueryFilters)
            query = query.IgnoreQueryFilters();

        foreach (var include in includes)
            query = query.Include(include);

        return await query.Where(predicate).ToListAsync();
    }

    /// <summary>
    /// Returns the first entity that satisfies <paramref name="predicate"/>, or <c>null</c> if none match.
    /// Useful when exactly one result is expected but membership in the result set is uncertain
    /// (e.g., looking up a record by a unique business key such as email or job number).
    /// </summary>
    /// <param name="predicate">A LINQ expression used to filter results.</param>
    /// <param name="ignoreQueryFilters">When <c>true</c>, bypasses soft-delete and multi-tenancy filters.</param>
    /// <param name="includes">Optional navigation-property selectors for eager loading.</param>
    public virtual async Task<T?> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate, bool ignoreQueryFilters = false, params Expression<Func<T, object>>[] includes)
    {
        IQueryable<T> query = _dbSet;

        if (ignoreQueryFilters)
            query = query.IgnoreQueryFilters();

        foreach (var include in includes)
            query = query.Include(include);

        return await query.FirstOrDefaultAsync(predicate);
    }

    /// <summary>
    /// Returns <c>true</c> if at least one entity satisfies <paramref name="predicate"/>.
    /// More efficient than <see cref="CountAsync"/> when only existence is needed, because
    /// EF Core translates this to <c>SELECT TOP 1</c> rather than a full aggregation.
    /// </summary>
    /// <param name="predicate">A LINQ expression used to test membership.</param>
    /// <param name="ignoreQueryFilters">When <c>true</c>, bypasses soft-delete and multi-tenancy filters.</param>
    public virtual async Task<bool> AnyAsync(Expression<Func<T, bool>> predicate, bool ignoreQueryFilters = false)
    {
        IQueryable<T> query = _dbSet;

        if (ignoreQueryFilters)
        {
            query = query.IgnoreQueryFilters();
        }

        return await query.AnyAsync(predicate);
    }

    /// <summary>
    /// Counts entities, optionally filtered by <paramref name="predicate"/>.
    /// Used by dashboard stat cards and pagination metadata.
    /// When <paramref name="predicate"/> is <c>null</c>, counts all visible rows for the current tenant.
    /// </summary>
    /// <param name="predicate">An optional LINQ expression to narrow the count. Pass <c>null</c> for a total count.</param>
    /// <param name="ignoreQueryFilters">When <c>true</c>, bypasses soft-delete and multi-tenancy filters to count across all tenants or include deleted rows.</param>
    public virtual async Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null, bool ignoreQueryFilters = false)
    {
        IQueryable<T> query = _dbSet;

        if (ignoreQueryFilters)
        {
            query = query.IgnoreQueryFilters();
        }

        return predicate == null
            ? await query.CountAsync()
            : await query.CountAsync(predicate);
    }

    /// <summary>
    /// Stages a new entity for insertion.
    /// The entity is not yet written to the database — call <see cref="IUnitOfWork.SaveChangesAsync"/>
    /// to commit.  <see cref="ApplicationDbContext.SaveChangesAsync"/> will auto-stamp
    /// <c>CreatedAt</c>, <c>CreatedBy</c>, and <c>CompanyId</c> (if 0) before the INSERT.
    /// </summary>
    /// <param name="entity">The new entity to insert.</param>
    /// <returns>The same entity reference (useful for chaining or to read the auto-generated ID after save).</returns>
    public virtual async Task<T> AddAsync(T entity)
    {
        await _dbSet.AddAsync(entity);
        return entity;
    }

    /// <summary>
    /// Stages a collection of new entities for bulk insertion.
    /// More efficient than calling <see cref="AddAsync"/> in a loop because EF Core batches the
    /// INSERTs into a single round-trip.
    /// </summary>
    /// <param name="entities">The entities to insert.</param>
    /// <returns>The same collection reference passed in.</returns>
    public virtual async Task<IEnumerable<T>> AddRangeAsync(IEnumerable<T> entities)
    {
        await _dbSet.AddRangeAsync(entities);
        return entities;
    }

    /// <summary>
    /// Marks an entity as modified so that all its properties will be included in the next UPDATE.
    /// <para>
    /// Returns a completed <see cref="Task"/> synchronously because EF Core's
    /// <c>DbSet.Update</c> is synchronous — the async signature is preserved for interface
    /// consistency and to allow overrides that perform async validation.
    /// </para>
    /// </summary>
    /// <param name="entity">The entity to update. Must be tracked or will be attached in the Modified state.</param>
    public virtual Task UpdateAsync(T entity)
    {
        _dbSet.Update(entity);
        return Task.CompletedTask;
    }

    /// <summary>
    /// Marks a collection of entities as modified for bulk update.
    /// EF Core batches the UPDATEs into a single round-trip on save.
    /// </summary>
    /// <param name="entities">The entities to update.</param>
    public virtual Task UpdateRangeAsync(IEnumerable<T> entities)
    {
        _dbSet.UpdateRange(entities);
        return Task.CompletedTask;
    }

    /// <summary>
    /// Physically removes an entity from the database.
    /// <para>
    /// Use sparingly — most deletions should use <see cref="SoftDeleteAsync(T)"/> instead so that
    /// audit history is preserved and the entity remains visible to SuperAdmin queries.
    /// Physical delete is appropriate for draft records, temporary data, or entities whose
    /// referential integrity constraints prohibit soft delete.
    /// </para>
    /// </summary>
    /// <param name="entity">The entity to physically remove.</param>
    public virtual async Task DeleteAsync(T entity)
    {
        _dbSet.Remove(entity);
        await Task.CompletedTask;
    }

    /// <summary>
    /// Retrieves the entity by <paramref name="id"/> and then physically removes it.
    /// If no entity is found (already deleted or not in this tenant), the operation is a no-op.
    /// </summary>
    /// <param name="id">The primary key of the entity to delete.</param>
    public virtual async Task DeleteAsync(int id)
    {
        var entity = await GetByIdAsync(id);
        if (entity != null)
        {
            await DeleteAsync(entity);
        }
    }

    /// <summary>
    /// Physically removes a collection of entities in a single SQL DELETE statement.
    /// EF Core translates <c>RemoveRange</c> into a batched DELETE for efficiency.
    /// </summary>
    /// <param name="entities">The entities to physically remove.</param>
    public virtual async Task DeleteRangeAsync(IEnumerable<T> entities)
    {
        _dbSet.RemoveRange(entities);
        await Task.CompletedTask;
    }

    /// <summary>
    /// Soft-deletes an entity by setting <c>IsDeleted = true</c> and <c>DeletedAt = UtcNow</c>.
    /// The row remains in the database and is visible to SuperAdmin queries with
    /// <c>ignoreQueryFilters: true</c>, preserving full audit history.
    /// The entity is then staged as Modified — call <see cref="IUnitOfWork.SaveChangesAsync"/> to commit.
    /// </summary>
    /// <param name="entity">The entity to soft-delete.</param>
    public virtual async Task SoftDeleteAsync(T entity)
    {
        entity.IsDeleted = true;
        entity.DeletedAt = DateTime.UtcNow;
        await UpdateAsync(entity);
    }

    /// <summary>
    /// Retrieves the entity by <paramref name="id"/> and soft-deletes it.
    /// If no entity is found (already deleted or not in this tenant), the operation is a no-op.
    /// </summary>
    /// <param name="id">The primary key of the entity to soft-delete.</param>
    public virtual async Task SoftDeleteAsync(int id)
    {
        var entity = await GetByIdAsync(id);
        if (entity != null)
        {
            await SoftDeleteAsync(entity);
        }
    }

    /// <summary>
    /// Returns a page of results with the total count for pagination metadata.
    /// <para>
    /// <b>Design note</b>: the total count is computed <em>before</em> ordering and slicing so
    /// that callers always receive the accurate count for the current filter, regardless of page
    /// size.  The ordering is applied after counting to minimise SQL complexity.
    /// </para>
    /// </summary>
    /// <param name="pageNumber">1-based page index.</param>
    /// <param name="pageSize">Number of items per page.</param>
    /// <param name="filter">Optional predicate to narrow the result set; applied before count and paging.</param>
    /// <param name="orderBy">Optional ordering function; applied after counting but before slicing.</param>
    /// <param name="includes">Optional navigation-property selectors for eager loading.</param>
    /// <returns>A tuple of (Items, TotalCount) where TotalCount is the total matching rows before paging.</returns>
    public virtual async Task<(IEnumerable<T> Items, int TotalCount)> GetPagedAsync(
        int pageNumber,
        int pageSize,
        Expression<Func<T, bool>>? filter = null,
        Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null,
        params Expression<Func<T, object>>[] includes)
    {
        IQueryable<T> query = _dbSet;

        if (filter != null)
        {
            query = query.Where(filter);
        }

        foreach (var include in includes)
        {
            query = query.Include(include);
        }

        var totalCount = await query.CountAsync();

        if (orderBy != null)
        {
            query = orderBy(query);
        }

        var items = await query
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();

        return (items, totalCount);
    }
}
