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

namespace JobsMedical.Web.Services;

/// <summary>A recommended opportunity — either an open <see cref="Shift"/> or an open
/// <see cref="JobOpening"/>. Exactly one of the two is set.</summary>
public record Recommendation(double Score, List<string> Reasons, Shift? Shift = null, JobOpening? Job = null)
{
    public bool IsJob => Job is not null;
}

/// <summary>
/// Stage 1 of the recommendation engine: a transparent, rule-based pattern engine. It scores open
/// opportunities — BOTH shifts AND job openings — against a visitor's explicit preferences and their
/// recent behavior, and returns the top matches each with a human-readable reason. Covering jobs (not
/// just shifts) matters because most roles — especially doctors — exist as استخدام, not dated shifts;
/// a shift-only feed would only ever recommend the handful of (mostly nurse) shifts.
/// </summary>
public class RecommendationService
{
    private readonly AppDbContext _db;
    private readonly InterestService _interest;

    public RecommendationService(AppDbContext db, InterestService interest)
    {
        _db = db;
        _interest = interest;
    }

    private const double WRolePref = 40, WRoleBehavior = 15;
    private const double WCityPref = 20;
    private const double WShiftTypePref = 15, WShiftTypeBehavior = 8;
    private const double WPayMeetsExpectation = 10;
    private const double WFacilityAffinity = 12;
    private const double WFreshness = 5, WSoon = 6;
    private const double PenaltyDismissedFacility = 60;

    public async Task<List<Recommendation>> GetForVisitorAsync(int take = 6)
    {
        var today = DateOnly.FromDateTime(DateTime.UtcNow);
        var jobCutoff = Scraping.ListingPolicy.JobCutoffUtc;
        var prefs = await _interest.GetPreferencesAsync();
        var events = await _interest.RecentEventsAsync(150);

        var shifts = await _db.Shifts
            .Include(s => s.Facility).ThenInclude(f => f.City).Include(s => s.Role)
            .Where(s => s.Status == ShiftStatus.Open && s.Date >= today).ToListAsync();
        var jobs = await _db.JobOpenings
            .Include(j => j.Facility).ThenInclude(f => f.City).Include(j => j.Role)
            .Where(j => j.Status == ShiftStatus.Open && j.CreatedAt >= jobCutoff).ToListAsync();

        // Cold start: freshest of both, interleaved (jobs lead — that's where the volume/roles are).
        if (prefs is null && events.Count == 0)
        {
            var cj = jobs.OrderByDescending(j => j.CreatedAt)
                .Select(j => new Recommendation(0, new List<string> { "جدیدترین فرصت‌ها" }, Job: j));
            var cs = shifts.OrderBy(s => s.Date).ThenBy(s => s.StartTime)
                .Select(s => new Recommendation(0, new List<string> { "جدیدترین فرصت‌ها" }, Shift: s));
            return Interleave(cj, cs).Take(take).ToList();
        }

        // Behavioral affinities — derived from BOTH shift and job events (role/facility span both;
        // shift-type is shift-only). Look up each engaged item's role/facility/type once.
        var positive = new[] { InterestEventType.View, InterestEventType.Click, InterestEventType.Save, InterestEventType.Apply };
        var negative = new[] { InterestEventType.Dismiss, InterestEventType.HideFacility };

        var sIds = events.Where(e => e.ShiftId is not null).Select(e => e.ShiftId!.Value).Distinct().ToList();
        var jIds = events.Where(e => e.JobOpeningId is not null).Select(e => e.JobOpeningId!.Value).Distinct().ToList();
        var sMeta = (await _db.Shifts.Where(s => sIds.Contains(s.Id))
            .Select(s => new { s.Id, s.RoleId, s.FacilityId, s.ShiftType }).ToListAsync()).ToDictionary(x => x.Id);
        var jMeta = (await _db.JobOpenings.Where(j => jIds.Contains(j.Id))
            .Select(j => new { j.Id, j.RoleId, j.FacilityId }).ToListAsync()).ToDictionary(x => x.Id);

        var roleCount = new Dictionary<int, int>();
        var facCount = new Dictionary<int, int>();
        var stCount = new Dictionary<int, int>();
        var dismissedFacilities = new HashSet<int>();
        foreach (var e in events)
        {
            int roleId, facId; int? st = null;
            if (e.ShiftId is int si && sMeta.TryGetValue(si, out var sm)) { roleId = sm.RoleId; facId = sm.FacilityId; st = (int)sm.ShiftType; }
            else if (e.JobOpeningId is int ji && jMeta.TryGetValue(ji, out var jm)) { roleId = jm.RoleId; facId = jm.FacilityId; }
            else continue;

            if (positive.Contains(e.EventType))
            {
                Bump(roleCount, roleId); Bump(facCount, facId);
                if (st is int t) Bump(stCount, t);
            }
            else if (negative.Contains(e.EventType)) dismissedFacilities.Add(facId);
        }
        var roleAffinity = Top3(roleCount);
        var facilityAffinity = Top3(facCount);
        var shiftTypeAffinity = Top3(stCount);

        var results = new List<Recommendation>();

        foreach (var s in shifts)
        {
            if (GenderConflicts(prefs, s.GenderRequirement)) continue;
            double score = 0; var reasons = new List<string>();
            ScoreCommon(ref score, reasons, prefs, s.RoleId, s.Role?.Name, s.Facility.CityId, s.Facility.City?.Name,
                s.FacilityId, s.Facility?.Name, roleAffinity, facilityAffinity, dismissedFacilities);

            if (prefs?.PreferredShiftType is ShiftType pst && pst == s.ShiftType)
            { score += WShiftTypePref; reasons.Add($"نوع شیفت دلخواه شما ({ShiftTypeLabel(s.ShiftType)})"); }
            else if (shiftTypeAffinity.Contains((int)s.ShiftType))
            { score += WShiftTypeBehavior; reasons.Add($"شبیه شیفت‌هایی که دیده‌ای ({ShiftTypeLabel(s.ShiftType)})"); }

            if (prefs?.MinPay is long min && s.PayAmount is long pay && pay >= min)
            { score += WPayMeetsExpectation; reasons.Add("حقوق بالاتر از حد انتظار شما"); }

            var daysOut = s.Date.DayNumber - today.DayNumber;
            if (daysOut <= 3) score += WSoon;
            if ((DateTime.UtcNow - s.CreatedAt).TotalDays <= 2) score += WFreshness;

            if (reasons.Count == 0) reasons.Add("پیشنهاد بر اساس فعالیت شما");
            results.Add(new Recommendation(score, reasons, Shift: s));
        }

        foreach (var j in jobs)
        {
            if (GenderConflicts(prefs, j.GenderRequirement)) continue;
            double score = 0; var reasons = new List<string>();
            ScoreCommon(ref score, reasons, prefs, j.RoleId, j.Role?.Name, j.Facility.CityId, j.Facility.City?.Name,
                j.FacilityId, j.Facility?.Name, roleAffinity, facilityAffinity, dismissedFacilities);

            if (prefs?.MinPay is long min && j.SalaryMin is long pay && pay >= min)
            { score += WPayMeetsExpectation; reasons.Add("حقوق بالاتر از حد انتظار شما"); }
            if ((DateTime.UtcNow - j.CreatedAt).TotalDays <= 2) score += WFreshness;

            if (reasons.Count == 0) reasons.Add("پیشنهاد بر اساس فعالیت شما");
            results.Add(new Recommendation(score, reasons, Job: j));
        }

        return results
            .Where(r => r.Score > 0)
            .OrderByDescending(r => r.Score)
            .Take(take)
            .ToList();
    }

    /// <summary>Role + city + facility scoring shared by shifts and jobs.</summary>
    private static void ScoreCommon(ref double score, List<string> reasons, UserPreferences? prefs,
        int roleId, string? roleName, int cityId, string? cityName, int facilityId, string? facilityName,
        HashSet<int> roleAffinity, HashSet<int> facilityAffinity, HashSet<int> dismissedFacilities)
    {
        if (prefs?.RoleId is int pr && pr == roleId)
        { score += WRolePref; reasons.Add($"متناسب با نقش مورد علاقه شما ({roleName})"); }
        else if (roleAffinity.Contains(roleId))
        { score += WRoleBehavior; reasons.Add($"چون به فرصت‌های «{roleName}» علاقه نشان دادی"); }

        if (prefs?.CityId is int pc && pc == cityId)
        { score += WCityPref; reasons.Add($"در شهر مورد علاقه شما ({cityName})"); }

        if (facilityAffinity.Contains(facilityId))
        { score += WFacilityAffinity; reasons.Add($"از مرکزی که قبلاً به آن علاقه نشان دادی ({facilityName})"); }

        if (dismissedFacilities.Contains(facilityId)) score -= PenaltyDismissedFacility;
    }

    private static bool GenderConflicts(UserPreferences? prefs, Gender req)
        => prefs?.Gender is Gender pg && pg != Gender.Any && req != Gender.Any && req != pg;

    private static void Bump(Dictionary<int, int> d, int k) => d[k] = d.GetValueOrDefault(k) + 1;
    private static HashSet<int> Top3(Dictionary<int, int> d) => d.OrderByDescending(k => k.Value).Take(3).Select(k => k.Key).ToHashSet();

    private static IEnumerable<Recommendation> Interleave(IEnumerable<Recommendation> a, IEnumerable<Recommendation> b)
    {
        using var ea = a.GetEnumerator();
        using var eb = b.GetEnumerator();
        bool ha = ea.MoveNext(), hb = eb.MoveNext();
        while (ha || hb)
        {
            if (ha) { yield return ea.Current; ha = ea.MoveNext(); }
            if (hb) { yield return eb.Current; hb = eb.MoveNext(); }
        }
    }

    private static string ShiftTypeLabel(ShiftType t) => t switch
    {
        ShiftType.Day => "صبح",
        ShiftType.Evening => "عصر",
        ShiftType.Night => "شب",
        _ => "آنکال",
    };
}
