using JSMR.Application.Scanning.Contracts;
using JSMR.Application.Scanning.Ports;
using JSMR.Domain.Entities;
using JSMR.Domain.Enums;
using JSMR.Domain.ValueObjects;
using JSMR.Infrastructure.Common.Time;
using JSMR.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;

namespace JSMR.Infrastructure.Ingestion;

public class VoiceWorkUpdater(AppDbContext dbContext, ITimeProvider timeProvider) : IVoiceWorkUpdater
{
    public async Task<VoiceWorkUpsertResult[]> UpsertAsync(IReadOnlyCollection<VoiceWorkIngest> ingests, CancellationToken cancellationToken)
    {
        VoiceWorkUpsertContext upsertContext = await CreateUpsertContextAsync(ingests, cancellationToken);

        foreach (VoiceWorkIngest ingest in ingests)
        {
            Validate(ingest, upsertContext);

            VoiceWorkUpsertResult result = upsertContext.Results[ingest.ProductId];

            if (result.Issues.Count > 0)
            {
                result.Status = VoiceWorkUpsertStatus.Skipped;
                continue;
            }

            result.Status = Upsert(ingest, upsertContext);

            VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
            result.UpdateStatus = (VoiceWorkStatus)voiceWork.Status;
        }

        await dbContext.SaveChangesAsync(cancellationToken);

        foreach (VoiceWorkIngest ingest in ingests)
        {
            VoiceWorkUpsertResult result = upsertContext.Results[ingest.ProductId];

            if (result.Status is VoiceWorkUpsertStatus.Skipped)
                continue;

            VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];

            result.VoiceWorkId = voiceWork.VoiceWorkId;
        }

        return [.. upsertContext.Results.Select(x => x.Value)];
    }

    private async Task<VoiceWorkUpsertContext> CreateUpsertContextAsync(IReadOnlyCollection<VoiceWorkIngest> ingests, CancellationToken cancellationToken)
    {
        string[] makerIds = [.. ingests.Select(i => i.MakerId).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()];
        string[] productIds = [.. ingests.Select(i => i.ProductId).Distinct()];
        string[] tagNames = [.. ingests.SelectMany(i => i.Tags).Distinct()];
        string[] creatorNames = [.. ingests.SelectMany(i => i.Creators).Distinct()];
        string[] seriesIdentifiers = [.. ingests.Where(i => i.Series is not null).Select(i => i.Series!.Identifier).Distinct()];

        DateTimeOffset currentScanAnchor = GetCurrentScanAnchor();
        DateTimeOffset previousScanAnchor = PreviousScanAnchor(currentScanAnchor);

        VoiceWorkUpsertContext upsertContext = new(
            CurrentScanAnchor: currentScanAnchor,
            PreviousScanAnchor: previousScanAnchor,
            Circles: await dbContext.Circles
                .Where(c => makerIds.Contains(c.MakerId))
                .ToDictionaryAsync(c => c.MakerId, cancellationToken),
            VoiceWorks: await dbContext.VoiceWorks
                .Where(v => productIds.Contains(v.ProductId))
                .AsSplitQuery()
                .Include(v => v.Creators)
                .Include(v => v.Tags)
                .Include(v => v.EnglishVoiceWorks)
                .Include(v => v.Localizations)
                .Include(v => v.SupportedLanguages)
                .ToDictionaryAsync(v => v.ProductId, cancellationToken),
            Tags: await dbContext.Tags
                .Where(t => tagNames.Contains(t.Name))
                .ToDictionaryAsync(t => t.Name, cancellationToken),
            Creators: await dbContext.Creators
                .Where(cr => creatorNames.Contains(cr.Name))
                .ToDictionaryAsync(cr => cr.Name, cancellationToken),
            Series: await dbContext.Series
                .Where(s => seriesIdentifiers.Contains(s.Identifier))
                .ToDictionaryAsync(s => s.Identifier, cancellationToken),
            Results: productIds.ToDictionary(
                productId => productId,
                productId => new VoiceWorkUpsertResult()
                {
                    ProductId = productId,
                    Title = ingests.First(x => x.ProductId == productId).Title
                }
            )
        );

        return upsertContext;
    }

    private DateTimeOffset GetCurrentScanAnchor()
    {
        DateTimeOffset now = timeProvider.Now();
        DateTimeOffset midnight = timeProvider.Local(now.Year, now.Month, now.Day, 0);
        DateTimeOffset fourPm = timeProvider.Local(now.Year, now.Month, now.Day, 16);

        return now >= fourPm ? fourPm : midnight;
    }

    private DateTimeOffset PreviousScanAnchor(DateTimeOffset scanAnchorTokyo)
    {
        // Normalize to Tokyo (no-op if already)
        var a = timeProvider.Local(scanAnchorTokyo);

        return a.Hour == 16
            ? timeProvider.Local(a.Year, a.Month, a.Day, 0)
            : timeProvider.Local(a.AddDays(-1).Year, a.AddDays(-1).Month, a.AddDays(-1).Day, 16);
    }

    private static void Validate(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        VoiceWorkUpsertResult result = upsertContext.Results[ingest.ProductId];

        if (upsertContext.Circles.TryGetValue(ingest.MakerId, out Circle? circle) == false)
            return;

        if (circle.Spam)
        {
            string message = $"Circle {ingest.MakerName} ({ingest.MakerId}) is a spam circle";
            result.Issues.Add(new(message, VoiceWorkUpsertIssueSeverity.Error));
            return;
        }

        if (upsertContext.VoiceWorks.TryGetValue(ingest.ProductId, out VoiceWork? voiceWork) == false)
            return;

        int ingestDownloads = ingest.Downloads;
        int voiceWorkDownloads = voiceWork.Downloads ?? 0;

        if (ingestDownloads < voiceWorkDownloads)
        {
            string message = $"Downloads have decreased from {voiceWorkDownloads} to {ingestDownloads}";
            result.Issues.Add(new(message, VoiceWorkUpsertIssueSeverity.Error));
        }

        if (voiceWork.SalesDate is not null && ingest.SalesDate is null)
        {
            string message = $"Voice work has a sales date of {voiceWork.SalesDate.Value:d}, but ingest does not";
            result.Issues.Add(new(message, VoiceWorkUpsertIssueSeverity.Error));
        }
    }

    private VoiceWorkUpsertStatus Upsert(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        UpsertCircle(ingest, upsertContext);

        VoiceWork voiceWork = UpsertVoiceWork(ingest, upsertContext);

        UpsertTags(ingest, upsertContext);
        UpsertVoiceWorkTags(ingest, upsertContext);
        UpsertCreators(ingest, upsertContext);
        UpsertVoiceWorkCreators(ingest, upsertContext);
        UpsertVoiceWorkSupportedLanguages(ingest, upsertContext);
        UpsertSeries(ingest, upsertContext);
        UpsertVoiceWorkLocalizations(ingest, upsertContext);

        return dbContext.Entry(voiceWork).State switch
        {
            EntityState.Added => VoiceWorkUpsertStatus.Inserted,
            EntityState.Modified => VoiceWorkUpsertStatus.Updated,
            _ => VoiceWorkUpsertStatus.Unchanged,
        };
    }

    private void UpsertCircle(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        Circle circle = GetOrAddCircle(ingest, upsertContext);
        circle.Name = ingest.MakerName;
    }

    private Circle GetOrAddCircle(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        if (!upsertContext.Circles.TryGetValue(ingest.MakerId, out Circle? circle))
        {
            circle = new Circle
            {
                MakerId = ingest.MakerId,
                Name = ingest.MakerName,
            };

            dbContext.Circles.Add(circle);
            upsertContext.Circles[ingest.MakerId] = circle;
        }

        return circle;
    }

    private VoiceWork UpsertVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        VoiceWork voiceWork = GetOrAddVoiceWork(ingest, upsertContext);
        VoiceWorkUpsertState state = ComputeVoiceWorkUpsertState(voiceWork, ingest, upsertContext);

        voiceWork.Circle = upsertContext.Circles[ingest.MakerId];
        voiceWork.ProductName = ingest.Title;
        voiceWork.Description = ingest.Description;
        voiceWork.HasImage = ingest.HasImage;
        voiceWork.Rating = (int)ingest.AgeRating;
        voiceWork.Downloads = ingest.Downloads;
        voiceWork.WishlistCount = ingest.WishlistCount;
        voiceWork.HasTrial = ingest.HasTrial;
        voiceWork.HasChobit = ingest.HasChobit;
        voiceWork.StarRating = ingest.StarRating;
        voiceWork.Votes = ingest.Votes;
        voiceWork.OriginalProductId = ingest.Translation?.OriginalProductId;
        voiceWork.SubtitleLanguage = GetSubtitleLanguage(ingest);
        voiceWork.AIGeneration = (byte)ingest.AI;
        voiceWork.IsValid = true;
        voiceWork.LastScannedDate = ComputeLastScannedDate(voiceWork.LastScannedDate, state, upsertContext);

        if (ingest.SalesDate.HasValue)
        {
            voiceWork.SalesDate = ingest.SalesDate.Value.ToDateTime(new TimeOnly(0, 0));
            voiceWork.ExpectedDate = null;
            voiceWork.PlannedReleaseDate = null;
            voiceWork.Status = state.IsNewOnSale ? (byte)VoiceWorkStatus.NewRelease : (byte)VoiceWorkStatus.Available;
        }
        else
        {
            voiceWork.SalesDate = null;
            voiceWork.ExpectedDate = ingest.ExpectedDate?.ToDateTime(new TimeOnly(0, 0)) ?? voiceWork.ExpectedDate;
            voiceWork.PlannedReleaseDate = ingest.RegistrationDate > upsertContext.CurrentScanAnchor ? ingest.RegistrationDate : null;
            voiceWork.Status = state.IsNewUpcoming ? (byte)VoiceWorkStatus.NewAndUpcoming : (byte)VoiceWorkStatus.Upcoming;
        }

        return voiceWork;
    }

    private VoiceWork GetOrAddVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        if (!upsertContext.VoiceWorks.TryGetValue(ingest.ProductId, out VoiceWork? voiceWork))
        {
            voiceWork = new VoiceWork
            {
                ProductId = ingest.ProductId
            };

            dbContext.VoiceWorks.Add(voiceWork);
            upsertContext.VoiceWorks[ingest.ProductId] = voiceWork;
        }

        return voiceWork;
    }

    private sealed record VoiceWorkUpsertState(
        bool WentOnSale,
        bool IsNewUpcoming,
        bool IsNewOnSale
    );

    private VoiceWorkUpsertState ComputeVoiceWorkUpsertState(VoiceWork voiceWork, VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        bool isAdded = dbContext.Entry(voiceWork).State == EntityState.Added;

        DateTime currentScanAnchor = upsertContext.CurrentScanAnchor.DateTime;
        DateTime previousScanAnchor = upsertContext.PreviousScanAnchor.DateTime;

        bool scannedThis = voiceWork.LastScannedDate == currentScanAnchor;
        bool scannedPrevAt4pm = voiceWork.LastScannedDate == previousScanAnchor && previousScanAnchor.Hour == 16;
        bool hasSales = ingest.SalesDate is not null;
        bool wentOnSale = voiceWork.SalesDate is null && hasSales;

        bool isNewUpcoming = !hasSales && (isAdded || scannedThis || scannedPrevAt4pm);
        bool isNewOnSale = hasSales && (isAdded || wentOnSale || scannedThis);

        return new VoiceWorkUpsertState(
            WentOnSale: wentOnSale,
            IsNewUpcoming: isNewUpcoming,
            IsNewOnSale: isNewOnSale
        );
    }

    private static DateTime? ComputeLastScannedDate(DateTime? existing, VoiceWorkUpsertState state, VoiceWorkUpsertContext upsertContext)
    {
        if ((state.IsNewUpcoming || state.IsNewOnSale) == false)
            return null;

        var current = upsertContext.CurrentScanAnchor.DateTime;

        return state.WentOnSale ? current : existing ?? current;
    }

    private static byte GetSubtitleLanguage(VoiceWorkIngest ingest)
    {
        Language[] orderedLanguages =
        [
            Language.English,
            Language.ChineseSimplified,
            Language.ChineseTraditional,
            Language.Korean
        ];

        foreach (Language language in orderedLanguages)
        {
            if (ingest.SupportedLanguages.Any(x => x.Language == language))
            {
                switch (language)
                {
                    case Language.English:
                        return 1;
                    case Language.ChineseSimplified:
                    case Language.ChineseTraditional:
                        return 2;
                    case Language.Korean:
                        return 3;
                }
            }
        }

        return 0;
    }

    private void UpsertTags(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        foreach (string tagName in ingest.Tags)
        {
            GetOrAddTag(tagName, upsertContext);
        }
    }

    private Tag GetOrAddTag(string tagName, VoiceWorkUpsertContext upsertContext)
    {
        if (!upsertContext.Tags.TryGetValue(tagName, out Tag? tag))
        {
            tag = new Tag
            {
                Name = tagName
            };

            dbContext.Tags.Add(tag);
            upsertContext.Tags[tagName] = tag;
        }

        return tag;
    }

    private void UpsertCreators(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        foreach (string creatorName in ingest.Creators)
        {
            GetOrAddCreator(creatorName, upsertContext);
        }
    }

    private Creator GetOrAddCreator(string creatorName, VoiceWorkUpsertContext upsertContext)
    {
        if (!upsertContext.Creators.TryGetValue(creatorName, out Creator? creator))
        {
            creator = new Creator
            {
                Name = creatorName
            };

            dbContext.Creators.Add(creator);
            upsertContext.Creators[creatorName] = creator;
        }

        return creator;
    }

    private void UpsertVoiceWorkTags(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
        Dictionary<int, VoiceWorkTag> existingTagLinks = voiceWork.Tags.ToDictionary(x => x.TagId);

        int position = 1;

        string[] distinctTagNames = [.. ingest.Tags.Distinct()];

        foreach (string tagName in distinctTagNames)
        {
            Tag tag = upsertContext.Tags[tagName];

            if (!existingTagLinks.TryGetValue(tag.TagId, out VoiceWorkTag? voiceWorkTag))
            {
                voiceWorkTag = new VoiceWorkTag
                {
                    VoiceWork = voiceWork,
                    Tag = tag
                };

                dbContext.VoiceWorkTags.Add(voiceWorkTag);
            }

            voiceWorkTag.Position = position++;
            voiceWorkTag.IsValid = true;
        }
    }

    private void UpsertVoiceWorkCreators(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
        Dictionary<int, VoiceWorkCreator> existingCreatorLinks = voiceWork.Creators.ToDictionary(x => x.CreatorId);

        int position = 1;

        string[] distinctCreatorNames = [.. ingest.Creators.Distinct()];

        foreach (string creatorName in distinctCreatorNames)
        {
            Creator creator = upsertContext.Creators[creatorName];

            if (!existingCreatorLinks.TryGetValue(creator.CreatorId, out VoiceWorkCreator? voiceWorkCreator))
            {
                voiceWorkCreator = new VoiceWorkCreator
                {
                    VoiceWork = voiceWork,
                    Creator = creator
                };

                dbContext.VoiceWorkCreators.Add(voiceWorkCreator);
            }

            voiceWorkCreator.Position = position++;
            voiceWorkCreator.IsValid = true;
        }
    }

    private void UpsertVoiceWorkSupportedLanguages(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
        Dictionary<string, VoiceWorkSupportedLanguage> existingLanguageLinks = voiceWork.SupportedLanguages.ToDictionary(x => x.Language);

        if (ingest.SupportedLanguages.Count == 0)
            return;

        foreach (SupportedLanguage supportedLanguage in ingest.SupportedLanguages)
        {
            if (!existingLanguageLinks.TryGetValue(supportedLanguage.Code, out VoiceWorkSupportedLanguage? voiceWorkSupportedLanguage))
            {
                voiceWorkSupportedLanguage = new VoiceWorkSupportedLanguage
                {
                    VoiceWork = voiceWork,
                    Language = supportedLanguage.Code
                };

                dbContext.VoiceWorkSupportedLanguages.Add(voiceWorkSupportedLanguage);
            }
        }

        foreach (string existingLinkCode in existingLanguageLinks.Keys)
        {
            if (!ingest.SupportedLanguages.Any(x => x.Code == existingLinkCode))
            {
                dbContext.VoiceWorkSupportedLanguages.Remove(existingLanguageLinks[existingLinkCode]);
            }
        }
    }

    private void UpsertSeries(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        Series? series = TryGetOrAddSeries(ingest, upsertContext);

        VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
        voiceWork.Series = series;
    }

    private Series? TryGetOrAddSeries(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        if (ingest.Series is null)
            return null;

        Circle circle = upsertContext.Circles[ingest.MakerId];

        if (!upsertContext.Series.TryGetValue(ingest.Series.Identifier, out Series? series))
        {
            series = new Series
            {
                Name = ingest.Series.Name,
                Identifier = ingest.Series.Identifier,
                Circle = circle
            };

            dbContext.Series.Add(series);
            upsertContext.Series[ingest.Series.Identifier] = series;
        }
        else
        {
            series.Identifier = ingest.Series.Identifier;
        }

        return series;
    }

    private void UpsertVoiceWorkLocalizations(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        // For now, just adding/updating English voice works
        foreach (VoiceWorkLocalizationIngest localizationIngest in ingest.Localizations)
        {
            if (localizationIngest.Language is Language.English)
            {
                EnglishVoiceWork englishVoiceWork = GetOrAddEnglishVoiceWork(ingest, upsertContext);
                englishVoiceWork.ProductName = localizationIngest.Title ?? string.Empty;
                englishVoiceWork.Description = localizationIngest.Description ?? string.Empty;
                englishVoiceWork.IsValid = true;
            }
        }
    }

    private EnglishVoiceWork GetOrAddEnglishVoiceWork(VoiceWorkIngest ingest, VoiceWorkUpsertContext upsertContext)
    {
        VoiceWork voiceWork = upsertContext.VoiceWorks[ingest.ProductId];
        EnglishVoiceWork? englishVoiceWork = voiceWork.EnglishVoiceWorks.FirstOrDefault();

        if (englishVoiceWork is null)
        {
            englishVoiceWork = new EnglishVoiceWork
            {
                VoiceWork = voiceWork,
                ProductName = string.Empty,
                Description = string.Empty
            };

            dbContext.EnglishVoiceWorks.Add(englishVoiceWork);
        }

        return englishVoiceWork;
    }
}