using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Application.Common.Interfaces;

namespace CMSMicroservice.Infrastructure.BackgroundServices;

/// <summary>
/// سرویس دیپلوی خودکار Stored Procedureها در استارتاپ (Q26).
/// فایل‌های .sql از EmbeddedResource خوانده شده، SHA256 محاسبه و با جدول
/// [CMS].[__StoredProcedureVersions] مقایسه می‌شود.
/// اگر checksum تغییر کرده باشد، SP دوباره اجرا (CREATE OR ALTER) می‌شود.
/// این سرویس فقط یکبار در استارتاپ اجرا شده و سپس متوقف می‌شود.
/// </summary>
public class StoredProcedureDeploymentService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<StoredProcedureDeploymentService> _logger;

    /// <summary>
    /// پیشوند نام‌فضای EmbeddedResource — فقط فایل‌های .sql واقعی (بدون README)
    /// </summary>
    private const string ResourcePrefix = "CMSMicroservice.Infrastructure.Persistence.StoredProcedures.";

    public StoredProcedureDeploymentService(
        IServiceScopeFactory scopeFactory,
        ILogger<StoredProcedureDeploymentService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // صبر برای آماده شدن دیتابیس و اتمام Migrationها
        await Task.Delay(TimeSpan.FromSeconds(15), stoppingToken);

        _logger.LogInformation("StoredProcedureDeploymentService started — checking stored procedures...");

        try
        {
            using var scope = _scopeFactory.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();

            var connection = context.Database.GetDbConnection();
            await connection.OpenAsync(stoppingToken);

            // ── ساخت جدول ورژن اگر وجود ندارد ──
            await EnsureVersionTableAsync(connection, stoppingToken);

            // ── خواندن فایل‌های SP از Embedded Resources ──
            var assembly = Assembly.GetExecutingAssembly();
            var resourceNames = assembly.GetManifestResourceNames()
                .Where(r => r.StartsWith(ResourcePrefix, StringComparison.OrdinalIgnoreCase)
                            && r.EndsWith(".sql", StringComparison.OrdinalIgnoreCase)
                            && !r.Contains("README", StringComparison.OrdinalIgnoreCase))
                .OrderBy(r => r)
                .ToList();

            _logger.LogInformation("Found {Count} SQL resources to check", resourceNames.Count);

            var deployed = 0;
            var skipped = 0;

            foreach (var resourceName in resourceNames)
            {
                if (stoppingToken.IsCancellationRequested) break;

                var fileName = resourceName[ResourcePrefix.Length..]; // e.g. "sp_CalculateWeeklyBalances.sql"
                var sqlContent = await ReadResourceAsync(assembly, resourceName);
                var checksum = ComputeSha256(sqlContent);

                var existingChecksum = await GetStoredChecksumAsync(connection, fileName, stoppingToken);

                if (string.Equals(existingChecksum, checksum, StringComparison.OrdinalIgnoreCase))
                {
                    _logger.LogDebug("SP '{FileName}' is up-to-date (checksum match), skipping", fileName);
                    skipped++;
                    continue;
                }

                // ── اجرای SP (CREATE OR ALTER) ──
                _logger.LogInformation("Deploying SP '{FileName}' (checksum changed: {Old} → {New})",
                    fileName,
                    existingChecksum ?? "NEW",
                    checksum[..12]);

                try
                {
                    await ExecuteSqlAsync(connection, sqlContent, stoppingToken);
                    await UpsertChecksumAsync(connection, fileName, checksum, stoppingToken);
                    deployed++;

                    _logger.LogInformation("Successfully deployed SP '{FileName}'", fileName);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Failed to deploy SP '{FileName}' — skipping", fileName);
                }
            }

            _logger.LogInformation(
                "StoredProcedureDeploymentService completed — deployed: {Deployed}, skipped: {Skipped}, total: {Total}",
                deployed, skipped, resourceNames.Count);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "StoredProcedureDeploymentService encountered a fatal error");
        }
    }

    // ══════════════════════════════════════════════════════════════
    // Helper Methods
    // ══════════════════════════════════════════════════════════════

    /// <summary>
    /// ساخت جدول [CMS].[__StoredProcedureVersions] اگر وجود ندارد.
    /// از Raw SQL استفاده می‌کنیم تا نیازی به Entity و Migration نباشد.
    /// </summary>
    private static async Task EnsureVersionTableAsync(
        System.Data.Common.DbConnection connection,
        CancellationToken ct)
    {
        const string sql = """
            IF NOT EXISTS (
                SELECT 1 FROM INFORMATION_SCHEMA.TABLES 
                WHERE TABLE_SCHEMA = 'CMS' AND TABLE_NAME = '__StoredProcedureVersions'
            )
            BEGIN
                CREATE TABLE [CMS].[__StoredProcedureVersions] (
                    [FileName]    NVARCHAR(256)  NOT NULL PRIMARY KEY,
                    [Checksum]    NVARCHAR(64)   NOT NULL,
                    [DeployedAt]  DATETIME2      NOT NULL DEFAULT GETUTCDATE(),
                    [DeployCount] INT            NOT NULL DEFAULT 1
                );
            END
            """;

        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;
        await cmd.ExecuteNonQueryAsync(ct);
    }

    /// <summary>
    /// خواندن checksum ذخیره‌شده برای یک فایل
    /// </summary>
    private static async Task<string?> GetStoredChecksumAsync(
        System.Data.Common.DbConnection connection,
        string fileName,
        CancellationToken ct)
    {
        const string sql = "SELECT [Checksum] FROM [CMS].[__StoredProcedureVersions] WHERE [FileName] = @FileName";

        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;

        var param = cmd.CreateParameter();
        param.ParameterName = "@FileName";
        param.Value = fileName;
        cmd.Parameters.Add(param);

        var result = await cmd.ExecuteScalarAsync(ct);
        return result as string;
    }

    /// <summary>
    /// ذخیره/به‌روزرسانی checksum بعد از دیپلوی موفق
    /// </summary>
    private static async Task UpsertChecksumAsync(
        System.Data.Common.DbConnection connection,
        string fileName,
        string checksum,
        CancellationToken ct)
    {
        const string sql = """
            MERGE [CMS].[__StoredProcedureVersions] AS target
            USING (SELECT @FileName AS [FileName]) AS source
            ON target.[FileName] = source.[FileName]
            WHEN MATCHED THEN
                UPDATE SET [Checksum] = @Checksum, 
                           [DeployedAt] = GETUTCDATE(),
                           [DeployCount] = target.[DeployCount] + 1
            WHEN NOT MATCHED THEN
                INSERT ([FileName], [Checksum], [DeployedAt], [DeployCount])
                VALUES (@FileName, @Checksum, GETUTCDATE(), 1);
            """;

        await using var cmd = connection.CreateCommand();
        cmd.CommandText = sql;

        var pFileName = cmd.CreateParameter();
        pFileName.ParameterName = "@FileName";
        pFileName.Value = fileName;
        cmd.Parameters.Add(pFileName);

        var pChecksum = cmd.CreateParameter();
        pChecksum.ParameterName = "@Checksum";
        pChecksum.Value = checksum;
        cmd.Parameters.Add(pChecksum);

        await cmd.ExecuteNonQueryAsync(ct);
    }

    /// <summary>
    /// اجرای محتوای SQL روی کانکشن فعلی
    /// </summary>
    private static async Task ExecuteSqlAsync(
        System.Data.Common.DbConnection connection,
        string sqlContent,
        CancellationToken ct)
    {
        // SP ها ممکن است GO separator داشته باشند — هر بلاک را جدا اجرا می‌کنیم
        var batches = SplitBatches(sqlContent);

        foreach (var batch in batches)
        {
            var trimmed = batch.Trim();
            if (string.IsNullOrWhiteSpace(trimmed)) continue;

            await using var cmd = connection.CreateCommand();
            cmd.CommandText = trimmed;
            cmd.CommandTimeout = 120; // SP های بزرگ ممکن است زمان ببرند
            await cmd.ExecuteNonQueryAsync(ct);
        }
    }

    /// <summary>
    /// تقسیم SQL به بلاک‌ها بر اساس GO separator
    /// </summary>
    private static List<string> SplitBatches(string sql)
    {
        // GO باید تنها در خط خودش باشد (case-insensitive)
        var lines = sql.Split('\n');
        var batch = new StringBuilder();
        var result = new List<string>();

        foreach (var line in lines)
        {
            if (line.Trim().Equals("GO", StringComparison.OrdinalIgnoreCase))
            {
                if (batch.Length > 0)
                {
                    result.Add(batch.ToString());
                    batch.Clear();
                }
            }
            else
            {
                batch.AppendLine(line);
            }
        }

        if (batch.Length > 0)
            result.Add(batch.ToString());

        return result;
    }

    /// <summary>
    /// خواندن محتوای یک Embedded Resource
    /// </summary>
    private static async Task<string> ReadResourceAsync(Assembly assembly, string resourceName)
    {
        await using var stream = assembly.GetManifestResourceStream(resourceName)
            ?? throw new InvalidOperationException($"Embedded resource not found: {resourceName}");
        using var reader = new StreamReader(stream, Encoding.UTF8);
        return await reader.ReadToEndAsync();
    }

    /// <summary>
    /// محاسبه SHA256 از محتوای SQL
    /// </summary>
    private static string ComputeSha256(string content)
    {
        var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(content));
        return Convert.ToHexString(bytes).ToLowerInvariant();
    }
}
