using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Predictalytics.Api;
using Predictalytics.Api.Endpoints;
using Predictalytics.Worker;
using Predictalytics.Application.Interfaces;
using Predictalytics.Domain.Interfaces;
using Predictalytics.Domain.Enums;
using Microsoft.Extensions.DependencyInjection;
using Serilog;

namespace Predictalytics.WinFormsHost.Services;

/// <summary>
/// Manages the lifecycle of the embedded Kestrel web server and background workers.
/// </summary>
public class EmbeddedWebServer
{
    private WebApplication? _app;
    private Task? _runTask;
    private CancellationTokenSource? _cts;
    private readonly object _lock = new();
    public string? ConnectionString { get; set; }
    public bool DbConnectionDebug { get; set; }
    public string? EgressChannelsText { get; set; }

    public async Task UpdateDatabaseAsync()
    {
        var services = new ServiceCollection();
        Predictalytics.Infrastructure.DependencyInjection.AddPredictalytics(services, new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(), ConnectionString, DbConnectionDebug);
        var provider = services.BuildServiceProvider();
        await Predictalytics.Infrastructure.DependencyInjection.EnsureDatabaseAsync(provider, DbConnectionDebug);
    }

    /// <summary>
    /// One-time data repair: derives ResolutionOutcome from snapped outcome prices,
    /// wipes all DERIVED analytics data (rebuildable positions, daily snapshots,
    /// category stats, analytics aggregates) and marks every trader for re-analysis.
    /// Raw trades are NOT touched. The analytics worker rebuilds everything with the
    /// current engine on the next server run. Run only while workers are stopped.
    /// </summary>
    public async Task<string> RunRecalculateAllTradersAsync(CancellationToken ct)
    {
        Log.Warning("🔄 Full trader recalculation reset requested...");

        var services = new ServiceCollection();
        Predictalytics.Infrastructure.DependencyInjection.AddPredictalytics(services, new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build(), ConnectionString, DbConnectionDebug);
        using var provider = services.BuildServiceProvider();
        using var scope = provider.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<Predictalytics.Infrastructure.Data.AppDbContext>();
        db.Database.SetCommandTimeout(600);

        // 1. Winner-detection backfill: markets synced before the A8 mapper fix have
        //    ResolutionOutcome = NULL; derive it from the snapped outcome prices.
        var backfilled = await db.Database.ExecuteSqlRawAsync(@"
            UPDATE Markets m
            JOIN MarketOutcomes o ON o.MarketId = m.Id AND o.CurrentPrice >= 0.98
            SET m.ResolutionOutcome = o.Label
            WHERE m.IsResolved = 1 AND (m.ResolutionOutcome IS NULL OR m.ResolutionOutcome = '');", ct);
        Log.Information("Backfilled ResolutionOutcome for {Count} markets", backfilled);

        // 1b. Offline Category/Subcategory backfill:
        // Query all markets that have events with tags in the DB, and re-classify them
        var dbMarkets = await db.Markets.Include(m => m.Event).ToListAsync(ct);
        int categoryBackfilledCount = 0;
        foreach (var m in dbMarkets)
        {
            if (m.Event != null && !string.IsNullOrEmpty(m.Event.Tags))
            {
                var (newCategory, newSubcategory) = Predictalytics.Infrastructure.Helpers.MarketCategoryMapper.Map(string.Empty, m.Event.Tags, m.Question);
                if (m.Category != newCategory || m.Subcategory != newSubcategory)
                {
                    m.Category = newCategory;
                    m.Subcategory = newSubcategory;
                    categoryBackfilledCount++;
                }
            }
        }
        if (categoryBackfilledCount > 0)
        {
            await db.SaveChangesAsync(ct);
            Log.Information("Backfilled Category/Subcategory for {Count} markets based on Event tags", categoryBackfilledCount);
        }

        // 2. Wipe derived data computed by earlier engine versions.
        var snapshots = await db.Database.ExecuteSqlRawAsync("DELETE FROM TraderDailySnapshots;", ct);
        var catPerf = await db.Database.ExecuteSqlRawAsync("DELETE FROM TraderCategoryPerformances;", ct);
        // Positions with pruned history are kept: their RealizedPnl is the only
        // remaining record of trades already deleted by retention (Deep Resync replaces them).
        var positions = await db.Database.ExecuteSqlRawAsync("DELETE FROM TraderPositions WHERE IsHistoryPruned = 0;", ct);
        var analytics = await db.Database.ExecuteSqlRawAsync(@"
            UPDATE TraderAnalytics SET
                OverallPnL = 0, PnL30d = 0, PnL7d = 0, PnL24h = 0,
                OverallWinRate = 0, WinRate30d = 0, WinRate7d = 0, WinRate24h = 0,
                CurrentBalance = 0, EstimatedBankroll = 0, Trades30d = 0,
                CopytradingScore = 0, CopytradingQualityScore = 0, CopytradingCopyabilityScore = 0;", ct);

        // 3. Trigger re-analysis of every trader on the next worker run.
        var traders = await db.Database.ExecuteSqlRawAsync("UPDATE Traders SET LastAnalyzedAt = NULL;", ct);

        var summary = $"ResolutionOutcome backfilled: {backfilled} markets\n" +
                      $"Category/Subcategory backfilled: {categoryBackfilledCount} markets\n" +
                      $"Daily snapshots deleted: {snapshots}\n" +
                      $"Category stats deleted: {catPerf}\n" +
                      $"Positions reset: {positions}\n" +
                      $"Analytics zeroed: {analytics}\n" +
                      $"Traders queued for re-analysis: {traders}";
        Log.Warning("🔄 Recalculation reset complete. {Summary}", summary.Replace('\n', ' '));
        return summary;
    }

    public async Task StartWebServerAsync(int port = 5000)
    {
        lock (_lock) { if (_app != null) return; }

        await Task.Run(async () =>
        {
            try
            {
                var builder = WebApplication.CreateBuilder();
                builder.WebHost.UseUrls($"http://localhost:{port}");
                
                string? effectiveConnString = ConnectionString;
                if (!string.IsNullOrEmpty(effectiveConnString) && (effectiveConnString.Contains("Database=;") || effectiveConnString.Contains("Database= ")))
                {
                    throw new InvalidOperationException("Connection string contains an empty 'Database' value.");
                }

                Log.Information("Initializing Predictalytics infrastructure with connection: {ConnectionString}", System.Text.RegularExpressions.Regex.Replace(effectiveConnString ?? "NULL", "Password=[^;]+", "Password=****"));
                Predictalytics.Infrastructure.DependencyInjection.AddPredictalytics(builder.Services, builder.Configuration, effectiveConnString, DbConnectionDebug);
                
                if (!string.IsNullOrEmpty(EgressChannelsText))
                {
                    var egressOptions = ParseEgressOptions(EgressChannelsText);
                    builder.Services.Configure<Predictalytics.Infrastructure.Configuration.EgressOptions>(options =>
                    {
                        options.Channels = egressOptions.Channels;
                    });
                }

                builder.Services.AddEndpointsApiExplorer();
                builder.Services.AddSwaggerGen(c => c.SwaggerDoc("v1",
                    new() { Title = "Predictalytics Analytics API", Version = "v1" }));
                
                var allowedOrigins = builder.Configuration.GetSection("ApiSettings:AllowedOrigins").Get<string[]>() 
                    ?? new[] { "http://localhost:5000" };
                builder.Services.AddCors(o => o.AddDefaultPolicy(p =>
                    p.WithOrigins(allowedOrigins).AllowAnyMethod().AllowAnyHeader()));
                builder.Host.UseSerilog();

                var app = builder.Build();
                app.UseCors();
                app.UseSwagger();
                app.UseSwaggerUI();

                var wwwrootPath = FindWwwrootPath();
                if (wwwrootPath != null)
                {
                    app.UseStaticFiles(new StaticFileOptions
                    {
                        FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(wwwrootPath),
                        // Internal dashboard: always revalidate so UI fixes reach the browser immediately.
                        OnPrepareResponse = ctx => ctx.Context.Response.Headers.CacheControl = "no-cache"
                    });
                    app.MapGet("/", () => Results.File(
                        Path.Combine(wwwrootPath, "index.html"), "text/html"));
                }

                // Single shared registration — see ApiConfiguration.MapPredictalyticsEndpoints.
                // Do NOT map endpoints individually here.
                app.MapPredictalyticsReadEndpoints();
                app.MapPredictalyticsControlEndpoints();

                await Predictalytics.Infrastructure.DependencyInjection.EnsureDatabaseAsync(app.Services, DbConnectionDebug);

                lock (_lock) { _app = app; }
                _cts = new CancellationTokenSource();
                _runTask = app.RunAsync(_cts.Token);

                Log.Information("Kestrel webserver started on port {Port}", port);
            }
            catch (Exception ex)
            {
                Log.Error(ex, "Failed to start embedded web server");
                throw;
            }
        });
    }

    public async Task StopWebServerAsync()
    {
        WebApplication? app;
        lock (_lock) { app = _app; _app = null; }

        if (app != null)
        {
            _cts?.Cancel();
            try { await app.StopAsync(TimeSpan.FromSeconds(5)); }
            catch (OperationCanceledException) { }
            await (app as IAsyncDisposable).DisposeAsync();
            Log.Information("Kestrel webserver stopped");
        }
    }

    public async Task StartWorkersAsync(CancellationToken ct)
    {
        Log.Information("Starting workers (no web server)...");

        var host = Host.CreateDefaultBuilder()
            .UseSerilog()
            .ConfigureServices((ctx, services) =>
            {
                Serilog.Log.Warning("🔌 [StartWorkers] Using ConnectionString: {ConnectionString}", System.Text.RegularExpressions.Regex.Replace(ConnectionString ?? "NULL", "Password=[^;]+", "Password=****"));
                Predictalytics.Infrastructure.DependencyInjection.AddPredictalytics(services, ctx.Configuration, ConnectionString, DbConnectionDebug);
                if (!string.IsNullOrEmpty(EgressChannelsText))
                {
                    var egressOptions = ParseEgressOptions(EgressChannelsText);
                    services.Configure<Predictalytics.Infrastructure.Configuration.EgressOptions>(options =>
                    {
                        options.Channels = egressOptions.Channels;
                    });
                }
                services.AddWorkerServices();
            })
            .Build();

        await Predictalytics.Infrastructure.DependencyInjection.EnsureDatabaseAsync(host.Services, DbConnectionDebug);
        await host.RunAsync(ct);
    }

    /// <summary>
    /// Performs a one-time full market sync for all supported platforms.
    /// </summary>
    public async Task RunSingleMarketSyncAsync(CancellationToken ct)
    {
        Log.Information("🏛️ Starting manual full market sync...");

        var host = Host.CreateDefaultBuilder()
            .UseSerilog()
            .ConfigureServices((ctx, services) =>
            {
                Predictalytics.Infrastructure.DependencyInjection.AddPredictalytics(services, ctx.Configuration, ConnectionString, DbConnectionDebug);
            })
            .Build();

        var rateLimiter = host.Services.GetRequiredService<IRateLimiter>();
        var platformProviders = host.Services.GetRequiredService<IEnumerable<IPlatformProvider>>();

        foreach (var p in platformProviders.Where(x => x.IsImplemented))
        {
            try
            {
                foreach (var includeClosed in new[] { true, false })
                {
                    Log.Warning("[{Platform}] Starting manual market sync (includeClosed={Closed})...", p.PlatformName, includeClosed);
                    int totalSynced = 0;
                    int offset = 0;
                    const int batchSize = 1000;

                    while (!ct.IsCancellationRequested)
                    {
                        try
                        {
                            // Create a fresh scope per batch to keep the DbContext change tracker small
                            using var scope = host.Services.CreateScope();
                            var marketRepo = scope.ServiceProvider.GetRequiredService<IMarketRepository>();
                            // We need to get the provider from the scope to ensure its dependencies (like DbContext) are correct if injected
                            var provider = scope.ServiceProvider.GetRequiredService<IEnumerable<IPlatformProvider>>()
                                .First(x => x.Platform == p.Platform);

                            await rateLimiter.WaitAsync(provider.Platform, ct);
                            var events = await provider.GetEventsAsync(batchSize, offset.ToString(), includeClosed, ct);

                            if (events == null || events.Count == 0)
                            {
                                Log.Warning("[{Platform}] No more events found at offset {Offset} (includeClosed={Closed}). Ending pass.", provider.PlatformName, offset, includeClosed);
                                break;
                            }

                            await marketRepo.AddOrUpdateEventsAsync(events, ct);

                            totalSynced += events.Count;
                            offset += events.Count;

                            if (totalSynced % 500 == 0 || events.Count < batchSize)
                                Log.Information("[{Platform}] Synced {Total} events so far (offset={Offset}, includeClosed={Closed})...", provider.PlatformName, totalSynced, offset, includeClosed);

                        }
                        catch (Exception ex)
                        {
                            Log.Error(ex, "Error processing batch at offset {Offset} for {Platform}", offset, p.PlatformName);
                            offset += batchSize;
                            if (offset > 50000) break; // Safety break
                        }
                    }

                    Log.Warning("✅ [{Platform}] Sync pass complete (includeClosed={Closed}). {Synced} markets synced.", p.PlatformName, includeClosed, totalSynced);
                }
            }
            catch (Exception ex)
            {
                Log.Error(ex, "Error during manual sync for {Platform}", p.PlatformName);
            }
        }
    }

    private static string? FindWwwrootPath()
    {
        var baseDir = AppContext.BaseDirectory;
        // Try to find the src root by traversing up from the build output
        // Typical: src/Predictalytics.WinFormsHost/bin/Debug/net8.0-windows/
        var candidates = new[]
        {
            Path.Combine(baseDir, "wwwroot"),
            // From bin/Debug/net8.0-windows/ up to src/, then into Api/wwwroot
            Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "Predictalytics.Api", "wwwroot")),
            Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..", "src", "Predictalytics.Api", "wwwroot")),
            // Absolute fallback for this specific workspace
            @"j:\Softwareprojekte\Predictalytics\Predictalytics\src\Predictalytics.Api\wwwroot"
        };

        foreach (var path in candidates)
        {
            var fullPath = Path.GetFullPath(path);
            if (Directory.Exists(fullPath))
            {
                Log.Information("Found wwwroot at: {Path}", fullPath);
                return fullPath;
            }
        }

        Log.Warning("wwwroot directory not found! Searched: {Paths}", string.Join(", ", candidates));
        return null;
    }

    private Predictalytics.Infrastructure.Configuration.EgressOptions ParseEgressOptions(string text)
    {
        var options = new Predictalytics.Infrastructure.Configuration.EgressOptions();
        if (string.IsNullOrWhiteSpace(text)) return options;

        var lines = text.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
        foreach (var line in lines)
        {
            var parts = line.Split('|');
            if (parts.Length >= 3)
            {
                var id = parts[0].Trim();
                var typeStr = parts[1].Trim();
                var val = parts[2].Trim();

                if (Enum.TryParse<Predictalytics.Infrastructure.Configuration.EgressChannelType>(typeStr, true, out var type))
                {
                    options.Channels.Add(new Predictalytics.Infrastructure.Configuration.EgressChannelOptions
                    {
                        Id = id,
                        Type = type,
                        Value = val
                    });
                }
            }
        }
        return options;
    }
}
