#nullable enable

using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.DbWriteSmoke;

internal static class ProductionDevelopmentDatabaseSmokePlan
{
    internal static IReadOnlyList<DevelopmentDatabaseSmokeScenario> Create(
        string? configurationPath)
    {
        var options = new DatabaseOptionsLoader().Load(configurationPath);

        // This executable never inherits the production read retry count. A
        // timeout is surfaced once so the harness cannot conceal an uncertain
        // state transition behind another attempt.
        options.Resilience.MaximumRetryCount = 0;
        options.Resilience.InitialRetryDelayMilliseconds = 0;
        var runtime = DatabaseRuntime.Create(options);

        var manualMutation = new OracleManualFinancialMutationExecutor(
            runtime.ConnectionFactory,
            options.Resilience);
        var namedMutation = new OracleNamedPlaylistMutationExecutor(
            runtime.ConnectionFactory,
            options.Resilience);
        var catalogMutation = new OperatorCatalogMutationExecutor(
            runtime.ConnectionFactory,
            options.Resilience);

        var manual = new LegacyManualFinancialScreenService(
            runtime.Executor,
            manualMutation);
        var named = new LegacyNamedPlaylistPersistenceService(
            runtime.Executor,
            namedMutation);
        var themePersistence = new LegacyThemeCatalogPersistenceService(
            runtime.Executor,
            catalogMutation);
        var themeSelection = new LegacyThemeSelectionService(runtime.Executor);
        var expertPersistence = new LegacyExpertCatalogPersistenceService(
            runtime.Executor,
            catalogMutation);
        var expertSelection = new LegacyExpertSelectionService(runtime.Executor);
        var childGuard = new ExistingChildRowGuard(runtime.Executor);

        var run = SmokeRunIdentity.Create(DateTimeOffset.UtcNow, Guid.NewGuid());
        return
        [
            CreateManualScenario(
                manual,
                CreateManualRecord(ManualFinancialScreenKind.RevenueComposition, run.For("G_PIE"))),
            CreateManualScenario(
                manual,
                CreateManualRecord(ManualFinancialScreenKind.GrowthMetrics, run.For("G_GROW"))),
            CreateManualScenario(
                manual,
                CreateManualRecord(ManualFinancialScreenKind.Sales, run.For("G_SELL"))),
            CreateManualScenario(
                manual,
                CreateManualRecord(ManualFinancialScreenKind.OperatingProfit, run.For("G_PROFIT"))),
            CreateNamedPlaylistScenario(named, childGuard, run.For("PLAYLIST")),
            CreateThemeScenario(
                themePersistence,
                themeSelection,
                childGuard,
                ThemeMarket.Krx,
                run.For("THEME_KRX")),
            CreateThemeScenario(
                themePersistence,
                themeSelection,
                childGuard,
                ThemeMarket.Nxt,
                run.For("THEME_NXT")),
            CreateExpertScenario(
                expertPersistence,
                expertSelection,
                childGuard,
                run.For("EXPERT"))
        ];
    }

    private static DevelopmentDatabaseSmokeScenario CreateManualScenario(
        IManualFinancialScreenService service,
        ManualFinancialRecord expectedRecord)
    {
        var identity = expectedRecord.Identity;
        ManualFinancialSnapshot? freshSnapshot = null;
        return new DevelopmentDatabaseSmokeScenario(
            "GraphE/" + identity.Screen,
            async cancellationToken =>
            {
                var search = await service.SearchAsync(
                    identity.Screen,
                    identity.StockName,
                    10,
                    cancellationToken).ConfigureAwait(false);
                if (search.IsTruncated || search.Items.Any(item =>
                        string.Equals(
                            item.Record.Identity.StockName,
                            identity.StockName,
                            StringComparison.Ordinal)))
                {
                    throw Collision("GraphE preflight did not prove absence.");
                }
            },
            cancellationToken => IgnoreResultAsync(
                service.CreateAsync(expectedRecord, cancellationToken)),
            async cancellationToken =>
            {
                freshSnapshot = await service.GetAsync(identity, cancellationToken)
                    .ConfigureAwait(false);
                VerifyManualRecord(expectedRecord, freshSnapshot);
            },
            save: null,
            verifySave: null,
            async cancellationToken =>
            {
                freshSnapshot ??= await service.GetAsync(identity, cancellationToken)
                    .ConfigureAwait(false);
                await service.DeleteAsync(freshSnapshot, cancellationToken).ConfigureAwait(false);
            },
            cancellationToken => EnsureManualAbsentAsync(service, identity, cancellationToken),
            IsOutcomeUnknown);
    }

    private static DevelopmentDatabaseSmokeScenario CreateNamedPlaylistScenario(
        INamedPlaylistPersistenceService service,
        ExistingChildRowGuard childGuard,
        string title)
    {
        string? programCode = null;
        var savedItem = new NamedPlaylistStoredItem(
            0,
            true,
            new LegacySceneSelection(
                "KOSPI",
                "SMOKE",
                "PLATE",
                "CURRENT",
                string.Empty),
            new NamedPlaylistPageState(1, 1));

        return new DevelopmentDatabaseSmokeScenario(
            "AList/PList named playlist",
            async cancellationToken =>
            {
                programCode = await service.SuggestNextProgramCodeAsync(cancellationToken)
                    .ConfigureAwait(false);
                var list = await service.ListAsync(
                    LegacyNamedPlaylistPersistenceService.MaximumDefinitions,
                    cancellationToken).ConfigureAwait(false);
                if (list.IsTruncated || list.Items.Any(item =>
                        string.Equals(item.Title, title, StringComparison.Ordinal)))
                {
                    throw Collision("Named-playlist title absence was not proven.");
                }

                await EnsureNamedPlaylistAbsentAsync(
                    service,
                    programCode,
                    cancellationToken).ConfigureAwait(false);
                await childGuard.EnsureNamedPlaylistChildrenAbsentAsync(
                    programCode,
                    cancellationToken).ConfigureAwait(false);
            },
            cancellationToken => service.CreateDefinitionAsync(
                Required(programCode),
                title,
                cancellationToken),
            async cancellationToken =>
            {
                var document = await service.LoadAsync(
                    Required(programCode),
                    cancellationToken).ConfigureAwait(false);
                if (!string.Equals(
                        document.Definition.Title,
                        title,
                        StringComparison.Ordinal) ||
                    document.Items.Count != 0)
                {
                    throw Verification("Named-playlist create readback did not match.");
                }
            },
            cancellationToken => service.ReplaceItemsAsync(
                Required(programCode),
                [savedItem],
                cancellationToken),
            async cancellationToken =>
            {
                var document = await service.LoadAsync(
                    Required(programCode),
                    cancellationToken).ConfigureAwait(false);
                if (!string.Equals(
                        document.Definition.Title,
                        title,
                        StringComparison.Ordinal) ||
                    document.Items.Count != 1 ||
                    document.Items[0] != savedItem)
                {
                    throw Verification("Named-playlist save readback did not match.");
                }
            },
            cancellationToken => service.DeleteAsync(
                Required(programCode),
                cancellationToken),
            async cancellationToken =>
            {
                await EnsureNamedPlaylistAbsentAsync(
                    service,
                    Required(programCode),
                    cancellationToken).ConfigureAwait(false);
                var list = await service.ListAsync(
                    LegacyNamedPlaylistPersistenceService.MaximumDefinitions,
                    cancellationToken).ConfigureAwait(false);
                if (list.IsTruncated || list.Items.Any(item =>
                        string.Equals(item.Title, title, StringComparison.Ordinal)))
                {
                    throw Verification("Named-playlist cleanup absence was not proven.");
                }
            },
            IsOutcomeUnknown);
    }

    private static DevelopmentDatabaseSmokeScenario CreateThemeScenario(
        IThemeCatalogPersistenceService persistence,
        IThemeSelectionService selection,
        ExistingChildRowGuard childGuard,
        ThemeMarket market,
        string title)
    {
        string? themeCode = null;
        ThemeSelectionIdentity? freshIdentity = null;
        var session = market == ThemeMarket.Krx
            ? ThemeSession.NotApplicable
            : ThemeSession.PreMarket;

        return new DevelopmentDatabaseSmokeScenario(
            "ThemeA/" + market,
            async cancellationToken =>
            {
                await EnsureThemeAbsentAsync(selection, title, cancellationToken)
                    .ConfigureAwait(false);
                themeCode = await persistence.SuggestNextCodeAsync(market, cancellationToken)
                    .ConfigureAwait(false);
                await childGuard.EnsureThemeChildrenAbsentAsync(
                    market,
                    themeCode,
                    cancellationToken).ConfigureAwait(false);
            },
            cancellationToken => IgnoreResultAsync(
                persistence.CreateAsync(
                    new ThemeCatalogDefinition(
                        market,
                        Required(themeCode),
                        title,
                        []),
                    cancellationToken)),
            async cancellationToken =>
            {
                var search = await selection.SearchAsync(
                    title,
                    ThemeSession.PreMarket,
                    LegacyThemeSelectionService.MaximumResults,
                    cancellationToken).ConfigureAwait(false);
                if (search.IsTruncated)
                {
                    throw Verification("Theme readback was truncated.");
                }

                var matches = search.Items.Where(item =>
                    string.Equals(
                        item.Identity.ThemeTitle,
                        title,
                        StringComparison.Ordinal)).ToArray();
                if (matches.Length != 1 ||
                    matches[0].Identity.Market != market ||
                    !string.Equals(
                        matches[0].Identity.ThemeCode,
                        themeCode,
                        StringComparison.Ordinal))
                {
                    throw Verification("Theme create readback did not match.");
                }

                freshIdentity = matches[0].Identity with { Session = session };
                var preview = await selection.PreviewAsync(
                    freshIdentity,
                    LegacyThemeSelectionService.MaximumPreviewItems,
                    cancellationToken).ConfigureAwait(false);
                if (preview.Items.Count != 0 || preview.IsTruncated)
                {
                    throw Verification("Theme preview readback did not match.");
                }
            },
            save: null,
            verifySave: null,
            cancellationToken => IgnoreResultAsync(
                persistence.DeleteAsync(
                    freshIdentity ?? new ThemeSelectionIdentity(
                        market,
                        session,
                        Required(themeCode),
                        title),
                    cancellationToken)),
            cancellationToken => EnsureThemeAbsentAsync(selection, title, cancellationToken),
            IsOutcomeUnknown);
    }

    private static DevelopmentDatabaseSmokeScenario CreateExpertScenario(
        IExpertCatalogPersistenceService persistence,
        IExpertSelectionService selection,
        ExistingChildRowGuard childGuard,
        string name)
    {
        string? expertCode = null;
        ExpertSelectionIdentity? freshIdentity = null;
        return new DevelopmentDatabaseSmokeScenario(
            "EList expert catalog",
            async cancellationToken =>
            {
                await EnsureExpertAbsentAsync(selection, name, cancellationToken)
                    .ConfigureAwait(false);
                expertCode = await persistence.SuggestNextCodeAsync(cancellationToken)
                    .ConfigureAwait(false);
                await childGuard.EnsureExpertChildrenAbsentAsync(
                    expertCode,
                    cancellationToken).ConfigureAwait(false);
            },
            cancellationToken => IgnoreResultAsync(
                persistence.CreateAsync(
                    new ExpertCatalogDefinition(
                        new ExpertSelectionIdentity(Required(expertCode), name),
                        []),
                    cancellationToken)),
            async cancellationToken =>
            {
                var search = await selection.SearchAsync(
                    name,
                    LegacyExpertSelectionService.MaximumResults,
                    cancellationToken).ConfigureAwait(false);
                if (search.IsTruncated)
                {
                    throw Verification("Expert readback was truncated.");
                }

                var matches = search.Items.Where(item =>
                    string.Equals(
                        item.Identity.ExpertName,
                        name,
                        StringComparison.Ordinal)).ToArray();
                if (matches.Length != 1 ||
                    !string.Equals(
                        matches[0].Identity.ExpertCode,
                        expertCode,
                        StringComparison.Ordinal))
                {
                    throw Verification("Expert create readback did not match.");
                }

                freshIdentity = matches[0].Identity;
                var preview = await selection.PreviewAsync(
                    freshIdentity,
                    LegacyExpertSelectionService.MaximumPreviewItems,
                    cancellationToken).ConfigureAwait(false);
                if (preview.Items.Count != 0 || preview.IsTruncated)
                {
                    throw Verification("Expert preview readback did not match.");
                }
            },
            save: null,
            verifySave: null,
            cancellationToken => IgnoreResultAsync(
                persistence.DeleteAsync(
                    freshIdentity ?? new ExpertSelectionIdentity(
                        Required(expertCode),
                        name),
                    cancellationToken)),
            cancellationToken => EnsureExpertAbsentAsync(selection, name, cancellationToken),
            IsOutcomeUnknown);
    }

    private static ManualFinancialRecord CreateManualRecord(
        ManualFinancialScreenKind screen,
        string stockName)
    {
        var identity = new ManualFinancialIdentity(screen, stockName);
        return screen switch
        {
            ManualFinancialScreenKind.RevenueComposition =>
                new ManualRevenueCompositionRecord(
                    identity,
                    "2026-07",
                    [new("SMOKE", "100", 100), null, null, null, null]),
            ManualFinancialScreenKind.GrowthMetrics =>
                new ManualGrowthMetricsRecord(
                    identity,
                    new(1, 2, 3, 4),
                    new(5, 6, 7, 8),
                    new(9, 10, 11, 12),
                    new(13, 14, 15, 16),
                    new("P1", "P2", "P3", "P4")),
            ManualFinancialScreenKind.Sales =>
                new ManualSalesRecord(identity, SixQuarters()),
            ManualFinancialScreenKind.OperatingProfit =>
                new ManualOperatingProfitRecord(identity, SixQuarters()),
            _ => throw new ArgumentOutOfRangeException(nameof(screen))
        };
    }

    private static IReadOnlyList<ManualQuarterValue> SixQuarters() =>
    [
        new("Q1", 10),
        new("Q2", -20),
        new("Q3", 30),
        new("Q4", 40),
        new("Q5", 50),
        new("Q6", 60)
    ];

    private static void VerifyManualRecord(
        ManualFinancialRecord expected,
        ManualFinancialSnapshot actual)
    {
        if (string.IsNullOrWhiteSpace(actual.RowVersion) ||
            actual.Record.Identity != expected.Identity)
        {
            throw Verification("GraphE identity or row version did not match.");
        }

        var matches = (expected, actual.Record) switch
        {
            (ManualRevenueCompositionRecord left, ManualRevenueCompositionRecord right) =>
                string.Equals(left.BaseDate, right.BaseDate, StringComparison.Ordinal) &&
                left.Slices.SequenceEqual(right.Slices),
            (ManualGrowthMetricsRecord left, ManualGrowthMetricsRecord right) =>
                left.SalesGrowth == right.SalesGrowth &&
                left.OperatingProfitGrowth == right.OperatingProfitGrowth &&
                left.NetAssetGrowth == right.NetAssetGrowth &&
                left.NetIncomeGrowth == right.NetIncomeGrowth &&
                left.Periods == right.Periods,
            (ManualSalesRecord left, ManualSalesRecord right) =>
                left.Quarters.SequenceEqual(right.Quarters),
            (ManualOperatingProfitRecord left, ManualOperatingProfitRecord right) =>
                left.Quarters.SequenceEqual(right.Quarters),
            _ => false
        };
        if (!matches)
        {
            throw Verification("GraphE create readback did not match.");
        }
    }

    private static async Task EnsureManualAbsentAsync(
        IManualFinancialScreenService service,
        ManualFinancialIdentity identity,
        CancellationToken cancellationToken)
    {
        try
        {
            _ = await service.GetAsync(identity, cancellationToken).ConfigureAwait(false);
        }
        catch (ManualFinancialNotFoundException)
        {
            return;
        }

        throw Verification("GraphE record still exists.");
    }

    private static async Task EnsureNamedPlaylistAbsentAsync(
        INamedPlaylistPersistenceService service,
        string programCode,
        CancellationToken cancellationToken)
    {
        try
        {
            _ = await service.LoadAsync(programCode, cancellationToken).ConfigureAwait(false);
        }
        catch (NamedPlaylistNotFoundException)
        {
            return;
        }

        throw Verification("Named playlist still exists.");
    }

    private static async Task EnsureThemeAbsentAsync(
        IThemeSelectionService selection,
        string title,
        CancellationToken cancellationToken)
    {
        var search = await selection.SearchAsync(
            title,
            ThemeSession.PreMarket,
            LegacyThemeSelectionService.MaximumResults,
            cancellationToken).ConfigureAwait(false);
        if (search.IsTruncated || search.Items.Any(item =>
                string.Equals(
                    item.Identity.ThemeTitle,
                    title,
                    StringComparison.Ordinal)))
        {
            throw Collision("Theme absence was not proven.");
        }
    }

    private static async Task EnsureExpertAbsentAsync(
        IExpertSelectionService selection,
        string name,
        CancellationToken cancellationToken)
    {
        var search = await selection.SearchAsync(
            name,
            LegacyExpertSelectionService.MaximumResults,
            cancellationToken).ConfigureAwait(false);
        if (search.IsTruncated || search.Items.Any(item =>
                string.Equals(
                    item.Identity.ExpertName,
                    name,
                    StringComparison.Ordinal)))
        {
            throw Collision("Expert absence was not proven.");
        }
    }

    private static async Task IgnoreResultAsync<T>(Task<T> operation)
    {
        _ = await operation.ConfigureAwait(false);
    }

    private static string Required(string? value) =>
        value ?? throw new InvalidOperationException("Smoke preflight did not produce an identity.");

    private static bool IsOutcomeUnknown(Exception exception)
    {
        ArgumentNullException.ThrowIfNull(exception);
        if (exception is ManualFinancialMutationException manual && manual.OutcomeUnknown ||
            exception is NamedPlaylistMutationException named && named.OutcomeUnknown ||
            exception is OperatorCatalogMutationException catalog && catalog.OutcomeUnknown)
        {
            return true;
        }

        if (exception is AggregateException aggregate &&
            aggregate.InnerExceptions.Any(IsOutcomeUnknown))
        {
            return true;
        }

        return exception.InnerException is not null && IsOutcomeUnknown(exception.InnerException);
    }

    private static InvalidOperationException Collision(string message) => new(message);

    private static InvalidOperationException Verification(string message) => new(message);
}
