using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using RolemasterDb.App.Data;
using RolemasterDb.App.Domain;
using RolemasterDb.ImportTool.Parsing;

namespace RolemasterDb.ImportTool;

public sealed class CriticalImportLoader(string databasePath)
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        WriteIndented = true
    };

    public async Task<int> ResetCriticalsAsync(CancellationToken cancellationToken = default)
    {
        await using var dbContext = CreateDbContext();
        await dbContext.Database.EnsureCreatedAsync(cancellationToken);
        await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext, cancellationToken);

        var removedTableCount = await dbContext.CriticalTables.CountAsync(cancellationToken);
        await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);

        await dbContext.CriticalEffects.ExecuteDeleteAsync(cancellationToken);
        await dbContext.CriticalBranches.ExecuteDeleteAsync(cancellationToken);
        await dbContext.CriticalResults.ExecuteDeleteAsync(cancellationToken);
        await dbContext.CriticalGroups.ExecuteDeleteAsync(cancellationToken);
        await dbContext.CriticalColumns.ExecuteDeleteAsync(cancellationToken);
        await dbContext.CriticalRollBands.ExecuteDeleteAsync(cancellationToken);
        await dbContext.CriticalTables.ExecuteDeleteAsync(cancellationToken);

        await transaction.CommitAsync(cancellationToken);
        return removedTableCount;
    }

    public async Task<ImportCommandResult> LoadAsync(ParsedCriticalTable table,
        CancellationToken cancellationToken = default)
    {
        await using var dbContext = CreateDbContext();
        await dbContext.Database.EnsureCreatedAsync(cancellationToken);
        await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext, cancellationToken);
        await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);

        var entity = await dbContext.CriticalTables
            .AsSplitQuery()
            .Include(item => item.Groups)
            .Include(item => item.Columns)
            .Include(item => item.RollBands)
            .Include(item => item.Results)
            .ThenInclude(result => result.CriticalGroup)
            .Include(item => item.Results)
            .ThenInclude(result => result.CriticalColumn)
            .Include(item => item.Results)
            .ThenInclude(result => result.CriticalRollBand)
            .Include(item => item.Results)
            .ThenInclude(result => result.Effects)
            .Include(item => item.Results)
            .ThenInclude(result => result.Branches)
            .ThenInclude(branch => branch.Effects)
            .SingleOrDefaultAsync(item => item.Slug == table.Slug, cancellationToken);

        if (entity is null)
        {
            entity = new CriticalTable
            {
                Slug = table.Slug
            };
            dbContext.CriticalTables.Add(entity);
        }

        entity.DisplayName = table.DisplayName;
        entity.Family = table.Family;
        entity.SourceDocument = table.SourceDocument;
        entity.Notes = table.Notes;

        var groupsByKey = SynchronizeGroups(entity, table);
        var columnsByKey = SynchronizeColumns(entity, table);
        var rollBandsByLabel = SynchronizeRollBands(entity, table);
        var existingResultsByKey = entity.Results.ToDictionary(
            item => CreateResultKey(item.CriticalGroup?.GroupKey, item.CriticalColumn.ColumnKey,
                item.CriticalRollBand.Label),
            StringComparer.Ordinal);
        var importedKeys = new HashSet<string>(StringComparer.Ordinal);

        foreach (var item in table.Results)
        {
            var resultKey = CreateResultKey(item.GroupKey, item.ColumnKey, item.RollBandLabel);
            importedKeys.Add(resultKey);

            if (!existingResultsByKey.TryGetValue(resultKey, out var existingResult))
            {
                var newResult = CreateResultEntity(item, entity, groupsByKey, columnsByKey, rollBandsByLabel);
                entity.Results.Add(newResult);
                continue;
            }

            existingResult.CriticalGroup = item.GroupKey is null ? null : groupsByKey[item.GroupKey];
            existingResult.CriticalColumn = columnsByKey[item.ColumnKey];
            existingResult.CriticalRollBand = rollBandsByLabel[item.RollBandLabel];
            ApplyImporterProvenance(existingResult, item);

            if (existingResult.IsCurated)
            {
                continue;
            }

            ApplyImportedContent(existingResult, item);
            ReplaceResultChildren(dbContext, existingResult, item);
        }

        foreach (var unmatchedResult in entity.Results
                     .Where(item => !importedKeys.Contains(CreateResultKey(item.CriticalGroup?.GroupKey,
                         item.CriticalColumn.ColumnKey, item.CriticalRollBand.Label)))
                     .ToList())
        {
            if (unmatchedResult.IsCurated)
            {
                continue;
            }

            RemoveResultGraph(dbContext, unmatchedResult);
            entity.Results.Remove(unmatchedResult);
        }

        RemoveUnusedAxes(dbContext, entity, table);
        await dbContext.SaveChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);

        return new ImportCommandResult(entity.Slug, entity.Columns.Count, entity.RollBands.Count, entity.Results.Count);
    }

    public async Task<int> RefreshImageArtifactsAsync(ParsedCriticalTable table,
        CancellationToken cancellationToken = default)
    {
        await using var dbContext = CreateDbContext();
        await dbContext.Database.EnsureCreatedAsync(cancellationToken);
        await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext, cancellationToken);
        await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);

        var entity = await dbContext.CriticalTables
            .AsSplitQuery()
            .Include(item => item.Results)
            .ThenInclude(result => result.CriticalGroup)
            .Include(item => item.Results)
            .ThenInclude(result => result.CriticalColumn)
            .Include(item => item.Results)
            .ThenInclude(result => result.CriticalRollBand)
            .SingleOrDefaultAsync(item => item.Slug == table.Slug, cancellationToken);

        if (entity is null)
        {
            throw new InvalidOperationException(
                $"Critical table '{table.Slug}' does not exist in the target database.");
        }

        var existingResultsByKey = entity.Results.ToDictionary(
            item => CreateResultKey(item.CriticalGroup?.GroupKey, item.CriticalColumn.ColumnKey,
                item.CriticalRollBand.Label),
            StringComparer.Ordinal);

        var refreshedCount = 0;
        foreach (var item in table.Results)
        {
            var resultKey = CreateResultKey(item.GroupKey, item.ColumnKey, item.RollBandLabel);
            if (!existingResultsByKey.TryGetValue(resultKey, out var existingResult))
            {
                continue;
            }

            ApplyImporterProvenance(existingResult, item);
            refreshedCount++;
        }

        await dbContext.SaveChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);
        return refreshedCount;
    }

    public async Task<IReadOnlyList<CriticalImageArtifactMetadata>> LoadImageArtifactMetadataAsync(
        string tableSlug,
        CancellationToken cancellationToken = default)
    {
        await using var dbContext = CreateDbContext();
        await dbContext.Database.EnsureCreatedAsync(cancellationToken);
        await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext, cancellationToken);

        var rows = await dbContext.CriticalResults
            .AsNoTracking()
            .Where(item => item.CriticalTable.Slug == tableSlug)
            .OrderBy(item => item.Id)
            .Select(item => new
            {
                item.Id,
                item.SourcePageNumber,
                item.SourceImagePath,
                item.SourceImageCropJson
            })
            .ToListAsync(cancellationToken);

        if (rows.Count == 0)
        {
            throw new InvalidOperationException($"Critical table '{tableSlug}' does not exist in the target database.");
        }

        var metadata = new List<CriticalImageArtifactMetadata>(rows.Count);
        foreach (var row in rows)
        {
            if (string.IsNullOrWhiteSpace(row.SourceImagePath))
            {
                throw new InvalidOperationException(
                    $"Critical result {row.Id} in table '{tableSlug}' is missing SourceImagePath.");
            }

            if (string.IsNullOrWhiteSpace(row.SourceImageCropJson))
            {
                throw new InvalidOperationException(
                    $"Critical result {row.Id} in table '{tableSlug}' is missing SourceImageCropJson.");
            }

            var crop = JsonSerializer.Deserialize<CriticalSourceImageCrop>(row.SourceImageCropJson);
            if (crop is null)
            {
                throw new InvalidOperationException(
                    $"Critical result {row.Id} in table '{tableSlug}' has invalid SourceImageCropJson.");
            }

            if (row.SourcePageNumber is not null && row.SourcePageNumber != crop.PageNumber)
            {
                throw new InvalidOperationException(
                    $"Critical result {row.Id} in table '{tableSlug}' has mismatched source page metadata.");
            }

            metadata.Add(new CriticalImageArtifactMetadata(row.Id, row.SourceImagePath, crop));
        }

        return metadata;
    }

    private RolemasterDbContext CreateDbContext()
    {
        var options = new DbContextOptionsBuilder<RolemasterDbContext>()
            .UseSqlite($"Data Source={databasePath}")
            .Options;

        return new RolemasterDbContext(options);
    }

    private static Dictionary<string, CriticalGroup> SynchronizeGroups(CriticalTable entity, ParsedCriticalTable table)
    {
        var groupsByKey = entity.Groups.ToDictionary(item => item.GroupKey, StringComparer.OrdinalIgnoreCase);

        foreach (var parsedGroup in table.Groups)
        {
            if (!groupsByKey.TryGetValue(parsedGroup.GroupKey, out var group))
            {
                group = new CriticalGroup
                {
                    CriticalTable = entity,
                    GroupKey = parsedGroup.GroupKey
                };
                entity.Groups.Add(group);
                groupsByKey[parsedGroup.GroupKey] = group;
            }

            group.Label = parsedGroup.Label;
            group.SortOrder = parsedGroup.SortOrder;
        }

        return groupsByKey;
    }

    private static Dictionary<string, CriticalColumn> SynchronizeColumns(CriticalTable entity,
        ParsedCriticalTable table)
    {
        var columnsByKey = entity.Columns.ToDictionary(item => item.ColumnKey, StringComparer.OrdinalIgnoreCase);

        foreach (var parsedColumn in table.Columns)
        {
            if (!columnsByKey.TryGetValue(parsedColumn.ColumnKey, out var column))
            {
                column = new CriticalColumn
                {
                    CriticalTable = entity,
                    ColumnKey = parsedColumn.ColumnKey
                };
                entity.Columns.Add(column);
                columnsByKey[parsedColumn.ColumnKey] = column;
            }

            column.Label = parsedColumn.Label;
            column.Role = parsedColumn.Role;
            column.SortOrder = parsedColumn.SortOrder;
        }

        return columnsByKey;
    }

    private static Dictionary<string, CriticalRollBand> SynchronizeRollBands(CriticalTable entity,
        ParsedCriticalTable table)
    {
        var rollBandsByLabel = entity.RollBands.ToDictionary(item => item.Label, StringComparer.OrdinalIgnoreCase);

        foreach (var parsedRollBand in table.RollBands)
        {
            if (!rollBandsByLabel.TryGetValue(parsedRollBand.Label, out var rollBand))
            {
                rollBand = new CriticalRollBand
                {
                    CriticalTable = entity,
                    Label = parsedRollBand.Label
                };
                entity.RollBands.Add(rollBand);
                rollBandsByLabel[parsedRollBand.Label] = rollBand;
            }

            rollBand.MinRoll = parsedRollBand.MinRoll;
            rollBand.MaxRoll = parsedRollBand.MaxRoll;
            rollBand.SortOrder = parsedRollBand.SortOrder;
        }

        return rollBandsByLabel;
    }

    private static CriticalResult CreateResultEntity(
        ParsedCriticalResult item,
        CriticalTable entity,
        IReadOnlyDictionary<string, CriticalGroup> groupsByKey,
        IReadOnlyDictionary<string, CriticalColumn> columnsByKey,
        IReadOnlyDictionary<string, CriticalRollBand> rollBandsByLabel)
    {
        var result = new CriticalResult
        {
            CriticalTable = entity,
            CriticalGroup = item.GroupKey is null ? null : groupsByKey[item.GroupKey],
            CriticalColumn = columnsByKey[item.ColumnKey],
            CriticalRollBand = rollBandsByLabel[item.RollBandLabel],
            IsCurated = false
        };

        ApplyImportedContent(result, item);
        ApplyImporterProvenance(result, item);
        result.Effects = item.Effects
            .Select(CreateEffectEntity)
            .ToList();
        result.Branches = item.Branches
            .Select(CreateBranchEntity)
            .ToList();
        return result;
    }

    private static void ApplyImportedContent(CriticalResult result, ParsedCriticalResult item)
    {
        result.RawCellText = item.RawCellText;
        result.DescriptionText = item.DescriptionText;
        result.RawAffixText = item.RawAffixText;
        result.ParsedJson = SerializeParsedEffects(item.Effects);
        result.ParseStatus = ResolveParseStatus(item.Effects, item.Branches);
    }

    private static void ApplyImporterProvenance(CriticalResult result, ParsedCriticalResult item)
    {
        result.SourcePageNumber = item.SourceBounds.PageNumber;
        result.SourceImagePath = NormalizeOptionalText(item.SourceImagePath);
        result.SourceImageCropJson = item.SourceImageCrop is null
            ? null
            : JsonSerializer.Serialize(item.SourceImageCrop, JsonOptions);
    }

    private static void ReplaceResultChildren(RolemasterDbContext dbContext, CriticalResult result,
        ParsedCriticalResult item)
    {
        foreach (var branch in result.Branches)
        {
            dbContext.CriticalEffects.RemoveRange(branch.Effects);
        }

        dbContext.CriticalEffects.RemoveRange(result.Effects);
        dbContext.CriticalBranches.RemoveRange(result.Branches);
        result.Effects.Clear();
        result.Branches.Clear();

        foreach (var effect in item.Effects)
        {
            result.Effects.Add(CreateEffectEntity(effect));
        }

        foreach (var branch in item.Branches)
        {
            result.Branches.Add(CreateBranchEntity(branch));
        }
    }

    private static void RemoveResultGraph(RolemasterDbContext dbContext, CriticalResult result)
    {
        foreach (var branch in result.Branches)
        {
            dbContext.CriticalEffects.RemoveRange(branch.Effects);
        }

        dbContext.CriticalEffects.RemoveRange(result.Effects);
        dbContext.CriticalBranches.RemoveRange(result.Branches);
        dbContext.CriticalResults.Remove(result);
    }

    private static void RemoveUnusedAxes(RolemasterDbContext dbContext, CriticalTable entity, ParsedCriticalTable table)
    {
        var activeGroupKeys = entity.Results
            .Where(item => item.CriticalGroup is not null)
            .Select(item => item.CriticalGroup!.GroupKey)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        var importedGroupKeys = table.Groups
            .Select(item => item.GroupKey)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        foreach (var group in entity.Groups
                     .Where(item =>
                         !activeGroupKeys.Contains(item.GroupKey) && !importedGroupKeys.Contains(item.GroupKey))
                     .ToList())
        {
            dbContext.CriticalGroups.Remove(group);
            entity.Groups.Remove(group);
        }

        var activeColumnKeys = entity.Results
            .Select(item => item.CriticalColumn.ColumnKey)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        var importedColumnKeys = table.Columns
            .Select(item => item.ColumnKey)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        foreach (var column in entity.Columns
                     .Where(item =>
                         !activeColumnKeys.Contains(item.ColumnKey) && !importedColumnKeys.Contains(item.ColumnKey))
                     .ToList())
        {
            dbContext.CriticalColumns.Remove(column);
            entity.Columns.Remove(column);
        }

        var activeRollBandLabels = entity.Results
            .Select(item => item.CriticalRollBand.Label)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);
        var importedRollBandLabels = table.RollBands
            .Select(item => item.Label)
            .ToHashSet(StringComparer.OrdinalIgnoreCase);

        foreach (var rollBand in entity.RollBands
                     .Where(item =>
                         !activeRollBandLabels.Contains(item.Label) && !importedRollBandLabels.Contains(item.Label))
                     .ToList())
        {
            dbContext.CriticalRollBands.Remove(rollBand);
            entity.RollBands.Remove(rollBand);
        }
    }

    private static CriticalEffect CreateEffectEntity(ParsedCriticalEffect effect) =>
        new()
        {
            EffectCode = effect.EffectCode,
            Target = effect.Target,
            ValueInteger = effect.ValueInteger,
            ValueExpression = effect.ValueExpression,
            DurationRounds = effect.DurationRounds,
            PerRound = effect.PerRound,
            Modifier = effect.Modifier,
            BodyPart = effect.BodyPart,
            IsPermanent = effect.IsPermanent,
            SourceType = effect.SourceType,
            SourceText = effect.SourceText
        };

    private static CriticalBranch CreateBranchEntity(ParsedCriticalBranch branch) =>
        new()
        {
            BranchKind = branch.BranchKind,
            ConditionKey = branch.ConditionKey,
            ConditionText = branch.ConditionText,
            ConditionJson = "{}",
            RawText = branch.RawText,
            DescriptionText = branch.DescriptionText,
            RawAffixText = branch.RawAffixText,
            ParsedJson = SerializeParsedEffects(branch.Effects),
            SortOrder = branch.SortOrder,
            Effects = branch.Effects
                .Select(CreateEffectEntity)
                .ToList()
        };

    private static string SerializeParsedEffects(IReadOnlyList<ParsedCriticalEffect> effects) =>
        effects.Count == 0
            ? "{}"
            : JsonSerializer.Serialize(new { effects }, JsonOptions);

    private static string ResolveParseStatus(
        IReadOnlyList<ParsedCriticalEffect> effects,
        IReadOnlyList<ParsedCriticalBranch> branches) =>
        effects.Count > 0 || branches.Any(branch => branch.Effects.Count > 0)
            ? "partial"
            : "raw";

    private static string? NormalizeOptionalText(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim();

    private static string CreateResultKey(string? groupKey, string columnKey, string rollBandLabel) =>
        $"{NormalizeKey(groupKey)}|{NormalizeKey(columnKey)}|{NormalizeKey(rollBandLabel)}";

    private static string NormalizeKey(string? value) =>
        string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim().ToUpperInvariant();
}