using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Infrastructure.Persistence;

namespace CMSMicroservice.Infrastructure.Repositories;

/// <summary>
/// ریپازیتوری هفته‌ها با کش در حافظه
/// موقع استارت برنامه از دیتابیس لود می‌شود و در حافظه نگهداری می‌شود
/// Thread-safe است و می‌توان از آن در محیط‌های multi-threaded استفاده کرد
/// Singleton است و از IServiceScopeFactory برای دسترسی به DbContext استفاده می‌کند
/// </summary>
public class WeekDefinitionRepository : IWeekDefinitionRepository
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<WeekDefinitionRepository> _logger;
    private readonly PersianCalendar _persianCalendar;
    
    // کش در حافظه
    private readonly ConcurrentDictionary<string, WeekDefinition> _cacheByGregorianWeek = new();
    private readonly ConcurrentDictionary<string, WeekDefinition> _cacheByPersianWeek = new();
    private readonly ConcurrentDictionary<int, WeekDefinition> _cacheByOrder = new();
    private List<WeekDefinition> _allWeeks = new();
    private readonly object _cacheLock = new();
    private bool _isCacheLoaded;

    public WeekDefinitionRepository(
        IServiceScopeFactory scopeFactory,
        ILogger<WeekDefinitionRepository> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
        _persianCalendar = new PersianCalendar();
    }

    public bool IsCacheLoaded => _isCacheLoaded;
    public int CachedWeeksCount => _allWeeks.Count;

    #region Load Cache

    /// <summary>
    /// بارگذاری اولیه کش از دیتابیس
    /// این متد باید موقع استارت برنامه فراخوانی شود
    /// </summary>
    public async Task ReloadCacheAsync(CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Loading WeekDefinitions into memory cache...");
        
        try
        {
            using var scope = _scopeFactory.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            
            var weeks = await context.WeekDefinitions
                .AsNoTracking()
                .Where(w => w.IsActive)
                .OrderBy(w => w.WeekOrder)
                .ToListAsync(cancellationToken);

            lock (_cacheLock)
            {
                _cacheByGregorianWeek.Clear();
                _cacheByPersianWeek.Clear();
                _cacheByOrder.Clear();
                _allWeeks = weeks;

                foreach (var week in weeks)
                {
                    _cacheByGregorianWeek.TryAdd(week.GregorianWeekNumber, week);
                    _cacheByPersianWeek.TryAdd(week.PersianWeekNumber, week);
                    _cacheByOrder.TryAdd(week.WeekOrder, week);
                }

                _isCacheLoaded = true;
            }

            _logger.LogInformation("Successfully loaded {Count} week definitions into cache", weeks.Count);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error loading WeekDefinitions into cache");
            throw;
        }
    }

    #endregion

    #region Current Week Methods

    public WeekDefinition? GetCurrentWeek()
    {
        EnsureCacheLoaded();
        return GetWeekByDate(DateTime.Today);
    }

    public Task<WeekDefinition?> GetCurrentWeekAsync(CancellationToken cancellationToken = default)
    {
        return Task.FromResult(GetCurrentWeek());
    }

    public string GetCurrentGregorianWeekNumber()
    {
        var currentWeek = GetCurrentWeek();
        return currentWeek?.GregorianWeekNumber ?? CalculateGregorianWeekNumber(DateTime.Today);
    }

    public string GetCurrentPersianWeekNumber()
    {
        var currentWeek = GetCurrentWeek();
        return currentWeek?.PersianWeekNumber ?? CalculatePersianWeekNumber(DateTime.Today);
    }

    public string GetCurrentWeekDisplayName()
    {
        var currentWeek = GetCurrentWeek();
        return currentWeek?.DisplayName ?? "نامشخص";
    }

    public bool IsDateInCurrentWeek(DateTime date)
    {
        var currentWeek = GetCurrentWeek();
        if (currentWeek == null) return false;
        
        return date.Date >= currentWeek.StartDate.Date && date.Date <= currentWeek.EndDate.Date;
    }

    #endregion

    #region Navigation Methods

    public WeekDefinition? GetNextWeek()
    {
        var currentWeek = GetCurrentWeek();
        if (currentWeek == null) return null;
        return GetNextWeek(currentWeek);
    }

    public WeekDefinition? GetNextWeek(WeekDefinition currentWeek)
    {
        EnsureCacheLoaded();
        return _cacheByOrder.TryGetValue(currentWeek.WeekOrder + 1, out var nextWeek) ? nextWeek : null;
    }

    public WeekDefinition? GetPreviousWeek()
    {
        var currentWeek = GetCurrentWeek();
        if (currentWeek == null) return null;
        return GetPreviousWeek(currentWeek);
    }

    public WeekDefinition? GetPreviousWeek(WeekDefinition currentWeek)
    {
        EnsureCacheLoaded();
        if (currentWeek.WeekOrder <= 1) return null;
        return _cacheByOrder.TryGetValue(currentWeek.WeekOrder - 1, out var prevWeek) ? prevWeek : null;
    }

    #endregion

    #region Lookup Methods

    public WeekDefinition? GetWeekByDate(DateTime date)
    {
        EnsureCacheLoaded();
        
        // شنبه اول هفته (تقویم شمسی)
        var saturday = GetStartOfWeek(date);
        
        return _allWeeks.FirstOrDefault(w => 
            w.StartDate.Date == saturday.Date);
    }

    public WeekDefinition? GetWeekByGregorianWeekNumber(string gregorianWeekNumber)
    {
        EnsureCacheLoaded();
        return _cacheByGregorianWeek.TryGetValue(gregorianWeekNumber, out var week) ? week : null;
    }

    public WeekDefinition? GetWeekByPersianWeekNumber(string persianWeekNumber)
    {
        EnsureCacheLoaded();
        return _cacheByPersianWeek.TryGetValue(persianWeekNumber, out var week) ? week : null;
    }

    public WeekDefinition? GetWeekByOrder(int weekOrder)
    {
        EnsureCacheLoaded();
        return _cacheByOrder.TryGetValue(weekOrder, out var week) ? week : null;
    }

    public IReadOnlyList<WeekDefinition> GetAllWeeks()
    {
        EnsureCacheLoaded();
        return _allWeeks.AsReadOnly();
    }

    public IReadOnlyList<WeekDefinition> GetWeeksByGregorianYear(int year)
    {
        EnsureCacheLoaded();
        return _allWeeks.Where(w => w.GregorianYear == year).ToList().AsReadOnly();
    }

    public IReadOnlyList<WeekDefinition> GetWeeksByPersianYear(int year)
    {
        EnsureCacheLoaded();
        return _allWeeks.Where(w => w.PersianYear == year).ToList().AsReadOnly();
    }
    
    public (DateTime startDate, DateTime endDate)? GetWeekDateRange(string gregorianWeekNumber)
    {
        EnsureCacheLoaded();
        var week = GetWeekByGregorianWeekNumber(gregorianWeekNumber);
        if (week == null) return null;
        
        return (week.StartDate, week.EndDate.AddHours(23).AddMinutes(59).AddSeconds(59));
    }
    public (DateTime startDate, DateTime endDate)? GetWeekDateRange(long WeekDefinitionId)
    {
        EnsureCacheLoaded();
        var week = GetWeekById(WeekDefinitionId);
        if (week == null) return null;
        
        return (week.StartDate, week.EndDate.AddHours(23).AddMinutes(59).AddSeconds(59));
    }
    public string? GetPreviousWeekNumber(string gregorianWeekNumber)
    {
        EnsureCacheLoaded();
        var currentWeek = GetWeekByGregorianWeekNumber(gregorianWeekNumber);
        if (currentWeek == null) return null;
        
        var previousWeek = GetWeekByOrder(currentWeek.WeekOrder - 1);
        return previousWeek?.GregorianWeekNumber;
    }
    
    public string? GetNextWeekNumber(string gregorianWeekNumber)
    {
        EnsureCacheLoaded();
        var currentWeek = GetWeekByGregorianWeekNumber(gregorianWeekNumber);
        if (currentWeek == null) return null;
        
        var nextWeek = GetWeekByOrder(currentWeek.WeekOrder + 1);
        return nextWeek?.GregorianWeekNumber;
    }
    
    public string GetDisplayNameByGregorianWeekNumber(string gregorianWeekNumber)
    {
        EnsureCacheLoaded();
        var week = GetWeekByGregorianWeekNumber(gregorianWeekNumber);
        return week?.DisplayName ?? gregorianWeekNumber; // اگر پیدا نشد، خود شماره میلادی رو برگردون
    }
    
    public IEnumerable<WeekDefinition> SearchWeeksByDisplayName(string? filter = null)
    {
        EnsureCacheLoaded();
        
        var weeks = _cacheByOrder.Values.OrderBy(w => w.WeekOrder);
        
        if (string.IsNullOrWhiteSpace(filter))
        {
            return weeks;
        }
        
        return weeks.Where(w => w.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase));
    }
    
    public long? GetWeekDefinitionId(string gregorianWeekNumber)
    {
        EnsureCacheLoaded();
        var week = GetWeekByGregorianWeekNumber(gregorianWeekNumber);
        return week?.Id;
    }
    
    public string? GetGregorianWeekNumber(long weekDefinitionId)
    {
        EnsureCacheLoaded();
        var week = _allWeeks.FirstOrDefault(w => w.Id == weekDefinitionId);
        return week?.GregorianWeekNumber;
    }
    public WeekDefinition? GetWeekById(long weekDefinitionId)
    {
        EnsureCacheLoaded();
        var week = _allWeeks.FirstOrDefault(w => w.Id == weekDefinitionId);
        return week;
    }
    #endregion

    #region Calculation Methods (Fallback when not in cache)

    public string CalculateGregorianWeekNumber(DateTime date)
    {
        // پیدا کردن شنبه این هفته
        var saturday = GetStartOfWeek(date);
        // دوشنبه همان هفته ISO
        var monday = saturday.AddDays(2);
        
        var cal = CultureInfo.InvariantCulture.Calendar;
        var weekNumber = cal.GetWeekOfYear(monday, CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
        var year = monday.Year;
        
        if (weekNumber == 1 && monday.Month == 12) year++;
        else if (weekNumber >= 52 && monday.Month == 1) year--;
        
        return $"{year}-W{weekNumber:D2}";
    }

    public string CalculatePersianWeekNumber(DateTime date)
    {
        var saturday = GetStartOfWeek(date);
        var persianYear = _persianCalendar.GetYear(saturday);
        var dayOfYear = _persianCalendar.GetDayOfYear(saturday);
        
        var firstDayOfYear = _persianCalendar.ToDateTime(persianYear, 1, 1, 0, 0, 0, 0);
        var daysUntilFirstSaturday = ((int)DayOfWeek.Saturday - (int)firstDayOfYear.DayOfWeek + 7) % 7;
        
        int weekNumber;
        if (daysUntilFirstSaturday == 0)
        {
            weekNumber = ((dayOfYear - 1) / 7) + 1;
        }
        else
        {
            if (dayOfYear <= daysUntilFirstSaturday)
                weekNumber = 1;
            else
                weekNumber = ((dayOfYear - daysUntilFirstSaturday - 1) / 7) + 1;
        }
        
        return $"{persianYear}-W{weekNumber:D2}";
    }

    #endregion

    #region Helper Methods

    private void EnsureCacheLoaded()
    {
        if (!_isCacheLoaded)
        {
            _logger.LogWarning("WeekDefinition cache is not loaded. Some methods may return null.");
        }
    }

    /// <summary>
    /// پیدا کردن شنبه این هفته (شروع هفته شمسی)
    /// </summary>
    private static DateTime GetStartOfWeek(DateTime date)
    {
        var diff = (7 + (date.DayOfWeek - DayOfWeek.Saturday)) % 7;
        return date.AddDays(-diff).Date;
    }

    #endregion
}
