using JobsMedical.Web.Data;
using JobsMedical.Web.Models;
using Microsoft.EntityFrameworkCore;

namespace JobsMedical.Web.Services;

/// <summary>
/// In-app notifications. When a new shift/job is published, notifies users whose saved
/// preferences match it (role / city / — for shifts — shift type). Users with no preference set
/// are NOT notified (avoids spamming everyone). One notification per matching user.
/// </summary>
public class NotificationService
{
    private readonly AppDbContext _db;
    private readonly PushNotifier _push;
    private readonly NotificationHub _hub;
    private readonly ILogger<NotificationService> _log;

    public NotificationService(AppDbContext db, PushNotifier push, NotificationHub hub, ILogger<NotificationService> log)
    {
        _db = db;
        _push = push;
        _hub = hub;
        _log = log;
    }

    public Task<int> UnreadCountAsync(int userId) =>
        _db.Notifications.CountAsync(n => n.UserId == userId && !n.IsRead);

    public Task<List<Notification>> ListAsync(int userId, int take = 50) =>
        _db.Notifications.Where(n => n.UserId == userId)
            .OrderByDescending(n => n.CreatedAt).Take(take).ToListAsync();

    public async Task MarkAllReadAsync(int userId) =>
        await _db.Notifications.Where(n => n.UserId == userId && !n.IsRead)
            .ExecuteUpdateAsync(s => s.SetProperty(n => n.IsRead, true));

    /// <summary>Admin broadcast: notify a set of users (in-app + push) with a custom message.</summary>
    public Task BroadcastAsync(IReadOnlyCollection<int> userIds, string title, string? body, string? url)
        => AddAsync(userIds.ToList(), title, body, url ?? "/");

    public async Task NotifyNewShiftAsync(int shiftId)
    {
        var s = await _db.Shifts.Include(x => x.Facility).Include(x => x.Role)
            .FirstOrDefaultAsync(x => x.Id == shiftId);
        if (s is null) return;

        var prefUsers = await MatchingUserIdsAsync(s.RoleId, s.Facility.CityId, s.ShiftType);
        var alertUsers = await ShiftAlertUserIdsAsync(s);
        var users = prefUsers.Union(alertUsers).Distinct().ToList();
        var title = $"شیفت جدید: {s.Role.Name}";
        var body = $"{s.Facility.Name} — {JalaliDate.WeekDayName(s.Date)} {JalaliDate.Time(s.StartTime)}";
        await AddAsync(users, title, body, $"/Shifts/Details/{s.Id}");
    }

    public async Task NotifyNewJobAsync(int jobId)
    {
        var j = await _db.JobOpenings.Include(x => x.Facility).Include(x => x.Role)
            .FirstOrDefaultAsync(x => x.Id == jobId);
        if (j is null) return;

        var prefUsers = await MatchingUserIdsAsync(j.RoleId, j.Facility.CityId, null);
        var alertUsers = await JobAlertUserIdsAsync(j);
        var users = prefUsers.Union(alertUsers).Distinct().ToList();
        await AddAsync(users, $"استخدام جدید: {j.Title}", j.Facility.Name, $"/Jobs/Details/{j.Id}");
    }

    /// <summary>Notify the facility owner that someone declared interest in their shift.</summary>
    public async Task NotifyShiftApplicationAsync(int shiftId)
    {
        var s = await _db.Shifts.Include(x => x.Facility).Include(x => x.Role)
            .FirstOrDefaultAsync(x => x.Id == shiftId);
        if (s?.Facility?.OwnerUserId is not int owner) return;   // admin-managed facility → no owner to notify
        var body = $"{s.Facility.Name} — {JalaliDate.WeekDayName(s.Date)} {JalaliDate.Time(s.StartTime)}";
        await AddAsync(new List<int> { owner }, $"متقاضی جدید برای شیفت {s.Role?.Name}", body,
            $"/Employer/Listings?facilityId={s.FacilityId}");
    }

    /// <summary>Notify the facility owner that someone declared interest in their hiring opening.</summary>
    public async Task NotifyJobApplicationAsync(int jobId)
    {
        var j = await _db.JobOpenings.Include(x => x.Facility)
            .FirstOrDefaultAsync(x => x.Id == jobId);
        if (j?.Facility?.OwnerUserId is not int owner) return;
        await AddAsync(new List<int> { owner }, $"متقاضی جدید: {j.Title}", j.Facility.Name,
            $"/Employer/Listings?facilityId={j.FacilityId}");
    }

    /// <summary>Tell an applicant their application was accepted or rejected.</summary>
    public Task NotifyApplicantStatusAsync(int userId, string listingTitle, bool accepted, string url)
    {
        var title = accepted ? "درخواست شما پذیرفته شد ✅" : "درخواست شما رد شد";
        return AddAsync(new List<int> { userId }, title, listingTitle, url);
    }

    /// <summary>Users with a non-empty preference that matches the listing (via their visitor link).</summary>
    private async Task<List<int>> MatchingUserIdsAsync(int roleId, int cityId, ShiftType? shiftType)
    {
        var q = from up in _db.UserPreferences
                join v in _db.Visitors on up.VisitorId equals v.Id
                where v.UserId != null
                    && (up.RoleId != null || up.CityId != null)            // must have a real preference
                    && (up.RoleId == null || up.RoleId == roleId)
                    && (up.CityId == null || up.CityId == cityId)
                    && (shiftType == null || up.PreferredShiftType == null || up.PreferredShiftType == shiftType)
                select v.UserId!.Value;
        return await q.Distinct().ToListAsync();
    }

    /// <summary>Owners of active job alerts that match this shift.</summary>
    private async Task<List<int>> ShiftAlertUserIdsAsync(Shift s)
    {
        var cityId = s.Facility.CityId;
        var districtId = s.Facility.DistrictId;
        return await _db.JobAlerts.Where(a => a.IsActive
                && a.Scope != AlertScope.Jobs
                && (a.RoleId == null || a.RoleId == s.RoleId)
                && (a.CityId == null || a.CityId == cityId)
                && (a.DistrictId == null || a.DistrictId == districtId)
                && (a.ShiftType == null || a.ShiftType == s.ShiftType)
                && (a.MinPay == null || (s.PayAmount != null && s.PayAmount >= a.MinPay)))
            .Select(a => a.UserId).Distinct().ToListAsync();
    }

    /// <summary>Owners of active job alerts that match this hiring opening.</summary>
    private async Task<List<int>> JobAlertUserIdsAsync(JobOpening j)
    {
        var cityId = j.Facility.CityId;
        var districtId = j.Facility.DistrictId;
        return await _db.JobAlerts.Where(a => a.IsActive
                && a.Scope != AlertScope.Shifts
                && (a.RoleId == null || a.RoleId == j.RoleId)
                && (a.CityId == null || a.CityId == cityId)
                && (a.DistrictId == null || a.DistrictId == districtId)
                && (a.EmploymentType == null || a.EmploymentType == j.EmploymentType)
                && (a.MinPay == null || ((j.SalaryMax ?? j.SalaryMin) != null && (j.SalaryMax ?? j.SalaryMin) >= a.MinPay)))
            .Select(a => a.UserId).Distinct().ToListAsync();
    }

    private async Task AddAsync(List<int> userIds, string title, string? body, string url)
    {
        if (userIds.Count == 0) return;

        var settings = await _db.AppSettings.FindAsync(1);
        var webOn = settings?.WebNotificationsEnabled ?? true;   // master toggle for the in-app/web channel

        // Web / in-app notifications channel (bell list + live SSE). Admin can turn it off.
        if (webOn)
        {
            foreach (var uid in userIds)
                _db.Notifications.Add(new Notification { UserId = uid, Title = title, Body = body, Url = url });
            await _db.SaveChangesAsync();
            _log.LogInformation("Notified {Count} users: {Title}", userIds.Count, title);

            // Live: stream to any open tab/PWA over SSE (our own origin — works in Iran).
            // The browser updates the bell instantly + shows a local toast/OS notification.
            var notice = new LiveNotice(title, body, url);
            foreach (var uid in userIds) _hub.Publish(uid, notice);
        }

        // Web Push channel (lock screen). Self-gates on PushEnabled + VAPID keys; best-effort
        // since Web Push depends on the browser's push service, which is filtered in Iran.
        try { await _push.PushToUsersAsync(userIds, title, body, url); }
        catch (Exception ex) { _log.LogWarning(ex, "Web push fan-out failed"); }
    }
}
