namespace Adapters.Driven.Persistence.Repositories;

using Adapters.Driven.Persistence.Context;
using Domain.Entities;
using Domain.Ports.Repositories;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;

public class StudentRepository(AppDbContext context) : IStudentRepository
{
    public async Task<Student?> GetByIdAsync(int id, CancellationToken ct = default) =>
        await context.Students.FindAsync([id], ct);

    public async Task<Student?> GetByIdWithEnrollmentsAsync(int id, CancellationToken ct = default) =>
        await context.Students
            .Include(s => s.Enrollments)
                .ThenInclude(e => e.Subject)
                    .ThenInclude(sub => sub.Professor)
            .AsSplitQuery() // Avoid cartesian explosion
            .FirstOrDefaultAsync(s => s.Id == id, ct);

    public async Task<IReadOnlyList<Student>> GetAllAsync(CancellationToken ct = default) =>
        await context.Students.AsNoTracking().ToListAsync(ct);

    public async Task<IReadOnlyList<Student>> GetAllWithEnrollmentsAsync(CancellationToken ct = default) =>
        await context.Students
            .Include(s => s.Enrollments)
                .ThenInclude(e => e.Subject)
                    .ThenInclude(sub => sub.Professor)
            .AsSplitQuery() // Avoid cartesian explosion
            .AsNoTrackingWithIdentityResolution() // Better than AsNoTracking with navigations
            .ToListAsync(ct);

    public async Task<bool> ExistsAsync(int id, CancellationToken ct = default) =>
        await CompiledQueries.StudentExists(context, id);

    public async Task<bool> EmailExistsAsync(string email, int? excludeId = null, CancellationToken ct = default)
    {
        var normalizedEmail = email.Trim().ToLowerInvariant();

        // Use raw SQL to avoid value object translation issues
        // SQL Server requires column alias for SqlQueryRaw
        var sql = excludeId.HasValue
            ? "SELECT 1 AS Value FROM Students WHERE LOWER(Email) = {0} AND Id != {1}"
            : "SELECT 1 AS Value FROM Students WHERE LOWER(Email) = {0}";

        var parameters = excludeId.HasValue
            ? new object[] { normalizedEmail, excludeId.Value }
            : new object[] { normalizedEmail };

        return await context.Database
            .SqlQueryRaw<int>(sql, parameters)
            .AnyAsync(ct);
    }

    // Direct projection - query only needed columns
    public async Task<IReadOnlyList<TResult>> GetAllProjectedAsync<TResult>(
        Expression<Func<Student, TResult>> selector,
        CancellationToken ct = default) =>
        await context.Students
            .AsNoTracking()
            .Select(selector)
            .ToListAsync(ct);

    public async Task<TResult?> GetByIdProjectedAsync<TResult>(
        int id,
        Expression<Func<Student, TResult>> selector,
        CancellationToken ct = default) =>
        await context.Students
            .Where(s => s.Id == id)
            .AsNoTracking()
            .Select(selector)
            .FirstOrDefaultAsync(ct);

    // Keyset pagination - more efficient than OFFSET for large datasets
    public async Task<(IReadOnlyList<TResult> Items, int? NextCursor, int TotalCount)> GetPagedProjectedAsync<TResult>(
        Expression<Func<Student, TResult>> selector,
        int? afterId = null,
        int pageSize = 10,
        CancellationToken ct = default)
    {
        var query = context.Students.AsNoTracking();

        // Keyset: WHERE Id > afterId (uses index efficiently)
        if (afterId.HasValue)
            query = query.Where(s => s.Id > afterId.Value);

        // Get one extra to determine if there's a next page
        var items = await query
            .OrderBy(s => s.Id)
            .Take(pageSize + 1)
            .Select(selector)
            .ToListAsync(ct);

        var hasMore = items.Count > pageSize;
        var resultItems = hasMore ? items.Take(pageSize).ToList() : items;

        // Get next cursor from last item (requires Id in projection or separate query)
        int? nextCursor = null;
        if (hasMore && resultItems.Count > 0)
        {
            var lastId = await context.Students
                .AsNoTracking()
                .Where(s => afterId.HasValue ? s.Id > afterId.Value : true)
                .OrderBy(s => s.Id)
                .Skip(pageSize - 1)
                .Select(s => s.Id)
                .FirstOrDefaultAsync(ct);
            nextCursor = lastId > 0 ? lastId : null;
        }

        // Use compiled query for count
        var totalCount = await CompiledQueries.CountStudents(context);

        return (resultItems, nextCursor, totalCount);
    }

    public void Add(Student student) => context.Students.Add(student);
    public void Update(Student student) => context.Students.Update(student);
    public void Delete(Student student) => context.Students.Remove(student);
}
