// <copyright file="PostgresDatabaseProvider.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>

#pragma warning disable SA1201 // Elements should appear in the correct order

namespace Jellyfin.Database.Providers.Postgres;

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.DbConfiguration;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Security;
using Jellyfin.Database.Implementations.Entities.Views;
using Jellyfin.Database.Providers.Postgres.Exceptions;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Npgsql;

/// <summary>
/// Configures Jellyfin to use a PostgreSQL database.
/// </summary>
[JellyfinDatabaseProviderKey("Jellyfin-PostgreSQL")]
public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
{
    private readonly IApplicationPaths applicationPaths;
    private readonly ILogger<PostgresDatabaseProvider> logger;
    private readonly IConfigurationManager? configurationManager;
    private PostgresBackupService? backupService;
    private string? currentHost;
    private DatabaseConfigurationOptions? databaseConfig;

    /// <summary>
    /// Initializes a new instance of the <see cref="PostgresDatabaseProvider"/> class.
    /// </summary>
    /// <param name="applicationPaths">Service to construct the fallback when the old data path configuration is used.</param>
    /// <param name="logger">A logger.</param>
    /// <param name="configurationManager">The configuration manager (optional, for backup support).</param>
    public PostgresDatabaseProvider(
        IApplicationPaths applicationPaths,
        ILogger<PostgresDatabaseProvider> logger,
        IConfigurationManager? configurationManager = null)
    {
        this.applicationPaths = applicationPaths;
        this.logger = logger;
        this.configurationManager = configurationManager;
    }

    /// <summary>
    /// Schema names mapping to legacy database files.
    /// </summary>
    public static class Schemas
    {
        /// <summary>
        /// Schema for activity log tables (legacy: activitylog.db).
        /// </summary>
        public const string ActivityLog = "activitylog";

        /// <summary>
        /// Schema for authentication tables (legacy: authentication.db).
        /// </summary>
        public const string Authentication = "authentication";

        /// <summary>
        /// Schema for display preferences tables (legacy: displaypreferences.db).
        /// </summary>
        public const string DisplayPreferences = "displaypreferences";

        /// <summary>
        /// Schema for library tables (legacy: library.db).
        /// </summary>
        public const string Library = "library";

        /// <summary>
        /// Schema for user tables (legacy: users.db).
        /// </summary>
        public const string Users = "users";

        /// <summary>
        /// Gets all schema names.
        /// </summary>
        public static string[] All => [ActivityLog, Authentication, DisplayPreferences, Library, Users];
    }

    /// <inheritdoc/>
    public IDbContextFactory<JellyfinDbContext>? DbContextFactory { get; set; }

    /// <inheritdoc/>
    public void Initialise(DbContextOptionsBuilder options, DatabaseConfigurationOptions databaseConfiguration)
    {
        // Store the configuration for later use
        databaseConfig = databaseConfiguration;

        static T? GetOption<T>(ICollection<CustomDatabaseOption>? options, string key, Func<string, T> converter, T? defaultValue = default)
        {
            if (options is null)
            {
                return defaultValue;
            }

            var value = options.FirstOrDefault(e => e.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
            if (value is null)
            {
                return defaultValue;
            }

            return converter(value.Value);
        }

        var customOptions = databaseConfiguration.CustomProviderOptions?.Options;

        // Start with connection string if provided, then override with individual options
        NpgsqlConnectionStringBuilder connectionBuilder;

        if (!string.IsNullOrWhiteSpace(databaseConfiguration.CustomProviderOptions?.ConnectionString))
        {
            // Use the provided connection string as base
            connectionBuilder = new NpgsqlConnectionStringBuilder(databaseConfiguration.CustomProviderOptions.ConnectionString);
            logger.LogDebug("Using ConnectionString from configuration as base");
        }
        else
        {
            // Start with defaults
            connectionBuilder = new NpgsqlConnectionStringBuilder
            {
                Host = "localhost",
                Port = 5432,
                Database = "jellyfin",
                Username = "jellyfin",
                Password = string.Empty,
                Pooling = true,
                CommandTimeout = 120,
                Timeout = 15,
                Multiplexing = false,
                MaxPoolSize = 100,
                MinPoolSize = 0
            };
            logger.LogDebug("Using default connection parameters");
        }

        // Override with individual options if provided (these take precedence over ConnectionString)
        if (customOptions is not null && customOptions.Count > 0)
        {
            var host = GetOption<string>(customOptions, "host", e => e, null);
            if (host is not null)
            {
                connectionBuilder.Host = host;
            }

            var port = GetOption<int?>(customOptions, "port", e => int.Parse(e), null);
            if (port.HasValue)
            {
                connectionBuilder.Port = port.Value;
            }

            var database = GetOption<string>(customOptions, "database", e => e, null);
            if (database is not null)
            {
                connectionBuilder.Database = database;
            }

            var username = GetOption<string>(customOptions, "username", e => e, null);
            if (username is not null)
            {
                connectionBuilder.Username = username;
            }

            var password = GetOption<string>(customOptions, "password", e => e, null);
            if (password is not null)
            {
                connectionBuilder.Password = password;
            }

            var pooling = GetOption<bool?>(customOptions, "pooling", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), null);
            if (pooling.HasValue)
            {
                connectionBuilder.Pooling = pooling.Value;
            }

            var commandTimeout = GetOption<int?>(customOptions, "command-timeout", e => int.Parse(e), null);
            if (commandTimeout.HasValue)
            {
                connectionBuilder.CommandTimeout = commandTimeout.Value;
            }

            var timeout = GetOption<int?>(customOptions, "connection-timeout", e => int.Parse(e), null);
            if (timeout.HasValue)
            {
                connectionBuilder.Timeout = timeout.Value;
            }

            var multiplexing = GetOption<bool?>(customOptions, "multiplexing", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), null);
            if (multiplexing.HasValue)
            {
                connectionBuilder.Multiplexing = multiplexing.Value;
            }

            var maxPoolSize = GetOption<int?>(customOptions, "max-pool-size", e => int.Parse(e), null);
            if (maxPoolSize.HasValue)
            {
                connectionBuilder.MaxPoolSize = maxPoolSize.Value;
            }

            var minPoolSize = GetOption<int?>(customOptions, "min-pool-size", e => int.Parse(e), null);
            if (minPoolSize.HasValue)
            {
                connectionBuilder.MinPoolSize = minPoolSize.Value;
            }

            logger.LogDebug("Applied {Count} individual option overrides", customOptions.Count);
        }

        // Optimize for bulk operations (especially media library scans)
        // These settings reduce dead tuples and improve write throughput
        connectionBuilder.ApplicationName = "jellyfin-mediaserver";

        var connectionString = connectionBuilder.ToString();

        // Store the host for backup operations
        currentHost = connectionBuilder.Host;

        // Log PostgreSQL connection parameters (without password)
        logger.LogInformation(
            "PostgreSQL connection: Host={Host}, Port={Port}, Database={Database}, Username={Username}, Pooling={Pooling}, MaxPoolSize={MaxPoolSize}, Multiplexing={Multiplexing}",
            connectionBuilder.Host,
            connectionBuilder.Port,
            connectionBuilder.Database,
            connectionBuilder.Username,
            connectionBuilder.Pooling,
            connectionBuilder.MaxPoolSize,
            connectionBuilder.Multiplexing);

        // Initialize backup service if configuration manager is available
        // Backup works for both local and remote servers via pg_dump/pg_restore client tools
        if (configurationManager is not null)
        {
            // Check if backups are explicitly disabled
            var disableBackups = GetOption<bool?>(customOptions, "disable-backups", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), null) ?? false;

            if (disableBackups)
            {
                logger.LogInformation("PostgreSQL backup service is disabled (disable-backups=true in configuration)");
                backupService = null;
            }
            else
            {
                var loggerFactory = Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance;
                var backupLogger = loggerFactory.CreateLogger<PostgresBackupService>();
                backupService = new PostgresBackupService(backupLogger, configurationManager);

                if (IsLocalHost(currentHost))
                {
                    logger.LogInformation("PostgreSQL backup service initialized for local database at {Host} (using pg_dump/pg_restore tools)", currentHost);
                }
                else
                {
                    logger.LogInformation("PostgreSQL backup service initialized for remote database at {Host} (using pg_dump/pg_restore tools)", currentHost);
                    logger.LogWarning("Note: Ensure pg_dump and pg_restore client tools are installed locally and network connectivity to {Host} is available", currentHost);
                }

                logger.LogInformation("PostgreSQL client binaries (pg_dump/pg_restore) must be in system PATH or specify full paths in BackupOptions");
            }
        }
        else
        {
            logger.LogWarning("Configuration manager not available - PostgreSQL backup service disabled");
        }

        options
            .UseNpgsql(
                connectionString,
                npgsqlOptions =>
                {
                    npgsqlOptions.MigrationsAssembly(GetType().Assembly.FullName);
                    npgsqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);

                    // Enable automatic retry on transient failures
                    npgsqlOptions.EnableRetryOnFailure(
                        maxRetryCount: 3,
                        maxRetryDelay: TimeSpan.FromSeconds(5),
                        errorCodesToAdd: new[] { "57P01" }); // Add "terminating connection" to retryable errors
                })
            .ConfigureWarnings(warnings =>
                warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));

        var enableSensitiveDataLogging = GetOption<bool?>(customOptions, "EnableSensitiveDataLogging", e => e.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase), null) ?? false;
        if (enableSensitiveDataLogging)
        {
            options.EnableSensitiveDataLogging(enableSensitiveDataLogging);
            logger.LogInformation("EnableSensitiveDataLogging is enabled on PostgreSQL connection");
        }
    }

    /// <summary>
    /// Ensures the PostgreSQL database exists, creating it if necessary.
    /// </summary>
    /// <param name="databaseConfiguration">The database configuration.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task EnsureDatabaseExistsAsync(DatabaseConfigurationOptions databaseConfiguration, CancellationToken cancellationToken = default)
    {
        var customOptions = databaseConfiguration.CustomProviderOptions?.Options;

        // Start with connection string if provided, then override with individual options
        NpgsqlConnectionStringBuilder connectionBuilder;
        if (!string.IsNullOrWhiteSpace(databaseConfiguration.CustomProviderOptions?.ConnectionString))
        {
            // Parse the existing connection string to get all parameters
            connectionBuilder = new NpgsqlConnectionStringBuilder(databaseConfiguration.CustomProviderOptions.ConnectionString);
            logger.LogDebug("EnsureDatabaseExists: Using ConnectionString from configuration as base");
        }
        else
        {
            // Start with defaults
            connectionBuilder = new NpgsqlConnectionStringBuilder
            {
                Host = "localhost",
                Port = 5432,
                Database = "jellyfin",
                Username = "jellyfin",
                Password = string.Empty,
                Timeout = 15
            };
            logger.LogDebug("EnsureDatabaseExists: Using default connection parameters");
        }

        // Override with individual options if provided (these take precedence over ConnectionString)
        if (customOptions is not null && customOptions.Count > 0)
        {
            var hostOpt = customOptions.FirstOrDefault(e => e.Key.Equals("host", StringComparison.OrdinalIgnoreCase));
            if (hostOpt is not null)
            {
                connectionBuilder.Host = hostOpt.Value;
            }

            var portOpt = customOptions.FirstOrDefault(e => e.Key.Equals("port", StringComparison.OrdinalIgnoreCase));
            if (portOpt is not null && int.TryParse(portOpt.Value, out var portValue))
            {
                connectionBuilder.Port = portValue;
            }

            var databaseOpt = customOptions.FirstOrDefault(e => e.Key.Equals("database", StringComparison.OrdinalIgnoreCase));
            if (databaseOpt is not null)
            {
                connectionBuilder.Database = databaseOpt.Value;
            }

            var usernameOpt = customOptions.FirstOrDefault(e => e.Key.Equals("username", StringComparison.OrdinalIgnoreCase));
            if (usernameOpt is not null)
            {
                connectionBuilder.Username = usernameOpt.Value;
            }

            var passwordOpt = customOptions.FirstOrDefault(e => e.Key.Equals("password", StringComparison.OrdinalIgnoreCase));
            if (passwordOpt is not null)
            {
                connectionBuilder.Password = passwordOpt.Value;
            }

            var timeoutOpt = customOptions.FirstOrDefault(e => e.Key.Equals("connection-timeout", StringComparison.OrdinalIgnoreCase));
            if (timeoutOpt is not null && int.TryParse(timeoutOpt.Value, out var timeoutValue))
            {
                connectionBuilder.Timeout = timeoutValue;
            }

            logger.LogDebug("EnsureDatabaseExists: Applied individual option overrides");
        }

        // Extract final values from builder (ensure non-null for parameters)
        var host = connectionBuilder.Host ?? "localhost";
        var port = connectionBuilder.Port;
        var database = connectionBuilder.Database ?? "jellyfin";
        var username = connectionBuilder.Username ?? "jellyfin";
        var password = connectionBuilder.Password ?? string.Empty;
        var timeout = connectionBuilder.Timeout;

        // Connect to 'postgres' maintenance database to check if target database exists
        var maintenanceConnectionBuilder = new NpgsqlConnectionStringBuilder
        {
            Host = host,
            Port = port,
            Database = "postgres", // Connect to maintenance database
            Username = username,
            Password = password,
            Timeout = timeout,
            Pooling = false // Don't pool maintenance connections
        };

        try
        {
            await using var connection = new NpgsqlConnection(maintenanceConnectionBuilder.ToString());
            await connection.OpenAsync(cancellationToken).ConfigureAwait(false);

            // Check if database exists
            var checkQuery = "SELECT 1 FROM pg_database WHERE datname = @databaseName";
            await using var checkCommand = new NpgsqlCommand(checkQuery, connection);
            checkCommand.Parameters.AddWithValue("databaseName", database);

            var exists = await checkCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);

            if (exists is null)
            {
                // Database doesn't exist, create it
                logger.LogInformation("PostgreSQL database '{Database}' does not exist. Creating...", database);

                // Use identifier quoting for safety
                var createQuery = $"CREATE DATABASE \"{database}\" OWNER \"{username}\"";
                await using var createCommand = new NpgsqlCommand(createQuery, connection);
                await createCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);

                logger.LogInformation("PostgreSQL database '{Database}' created successfully", database);

                // Grant privileges
                var grantQuery = $"GRANT ALL PRIVILEGES ON DATABASE \"{database}\" TO \"{username}\"";
                await using var grantCommand = new NpgsqlCommand(grantQuery, connection);
                await grantCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);

                logger.LogInformation("Granted all privileges on database '{Database}' to user '{Username}'", database, username);
            }
            else
            {
                logger.LogInformation("PostgreSQL database '{Database}' already exists", database);
            }

            // Now ensure all schemas exist in the database
            await EnsureSchemasExistAsync(host, port, database, username, password, timeout, cancellationToken).ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to ensure PostgreSQL database '{Database}' exists. Error: {Message}", database, ex.Message);
            throw;
        }
    }

    /// <summary>
    /// Ensures all required schemas exist in the PostgreSQL database.
    /// </summary>
    private async Task EnsureSchemasExistAsync(
        string host,
        int port,
        string database,
        string username,
        string password,
        int timeout,
        CancellationToken cancellationToken)
    {
        var connectionBuilder = new NpgsqlConnectionStringBuilder
        {
            Host = host,
            Port = port,
            Database = database,
            Username = username,
            Password = password,
            Timeout = timeout,
            Pooling = false
        };

        try
        {
            await using var connection = new NpgsqlConnection(connectionBuilder.ToString());
            await connection.OpenAsync(cancellationToken).ConfigureAwait(false);

            foreach (var schema in Schemas.All)
            {
                // Check if schema exists
                var checkQuery = "SELECT 1 FROM information_schema.schemata WHERE schema_name = @schemaName";
                await using var checkCommand = new NpgsqlCommand(checkQuery, connection);
                checkCommand.Parameters.AddWithValue("schemaName", schema);

                var exists = await checkCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);

                if (exists is null)
                {
                    // Schema doesn't exist, create it
                    logger.LogInformation("Creating PostgreSQL schema '{Schema}'...", schema);

                    var createQuery = $"CREATE SCHEMA \"{schema}\" AUTHORIZATION \"{username}\"";
                    await using var createCommand = new NpgsqlCommand(createQuery, connection);
                    await createCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);

                    logger.LogInformation("Schema '{Schema}' created successfully", schema);
                }
                else
                {
                    logger.LogDebug("Schema '{Schema}' already exists", schema);
                }
            }

            logger.LogInformation("All required PostgreSQL schemas are ready");
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to ensure PostgreSQL schemas exist. Error: {Message}", ex.Message);
            throw;
        }
    }

    /// <summary>
    /// Ensures all required database tables exist by applying pending migrations.
    /// </summary>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task EnsureTablesExistAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            // First ensure the database itself exists
            if (databaseConfig is not null)
            {
                await EnsureDatabaseExistsAsync(databaseConfig, cancellationToken).ConfigureAwait(false);
            }
            else
            {
                logger.LogWarning("Database configuration not available. Database existence check skipped.");
            }

            logger.LogInformation("Checking PostgreSQL database schema...");

            var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
            await using (context.ConfigureAwait(false))
            {
                var connection = context.Database.GetDbConnection();
                var wasConnectionOpened = false;

                try
                {
                    if (connection.State != System.Data.ConnectionState.Open)
                    {
                        await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
                        wasConnectionOpened = true;
                    }

                    // Check if database has tables (not just schemas)
                    // Query for a critical table that must exist - if it doesn't, database needs initialization
                    var tableCheckQuery = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'users' AND table_name = 'Users' AND table_type = 'BASE TABLE'";
                    await using (var tableCheckCommand = connection.CreateCommand())
                    {
                        tableCheckCommand.CommandText = tableCheckQuery;
                        var usersTableExists = (long?)await tableCheckCommand.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);

                        if (usersTableExists == 0)
                        {
                            // Database tables don't exist - initialize from SQL script
                            logger.LogInformation("Database tables not found (users.Users table missing). Initializing from SQL script...");

                            var createDbScriptPath = FindSchemaScriptPath();
                            if (string.IsNullOrEmpty(createDbScriptPath))
                            {
                                throw new FileNotFoundException("Could not find the schema initialization script: create_database_schema.sql");
                            }

                            var psqlPath = FindPsqlPath();

                            // The SQL file contains psql meta-commands (\restrict, etc.) that can't be executed via ExecuteNonQuery
                            // We need to use psql subprocess instead
                            var connectionString = connection.ConnectionString;
                            var builder = new NpgsqlConnectionStringBuilder(connectionString);

                            // Build psql command
                            var psqlArgs = $"-h {builder.Host} -p {builder.Port} -U {builder.Username} -d {builder.Database} -f \"{createDbScriptPath}\"";

                            // Log command (password not in args, so safe to log)
                            logger.LogInformation("Executing via psql: psql {Args}", psqlArgs);

                            var psqlProcess = new System.Diagnostics.Process
                            {
                                StartInfo = new System.Diagnostics.ProcessStartInfo
                                {
                                    FileName = "psql",
                                    Arguments = psqlArgs,
                                    RedirectStandardOutput = true,
                                    RedirectStandardError = true,
                                    RedirectStandardInput = true,
                                    UseShellExecute = false,
                                    CreateNoWindow = true
                                }
                            };

                            // Set password via environment variable
                            if (!string.IsNullOrEmpty(builder.Password))
                            {
                                psqlProcess.StartInfo.EnvironmentVariables["PGPASSWORD"] = builder.Password;
                            }

                            psqlProcess.Start();

                            // Read output
                            var output = await psqlProcess.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
                            var error = await psqlProcess.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);

                            await psqlProcess.WaitForExitAsync(cancellationToken).ConfigureAwait(false);

                            if (psqlProcess.ExitCode == 0)
                            {
                                logger.LogInformation("✅ Successfully initialized database from SQL script via psql");
                                logger.LogInformation("Database is ready. You can now start Jellyfin.");
                                if (!string.IsNullOrWhiteSpace(output))
                                {
                                    logger.LogDebug("psql output: {Output}", output);
                                }
                            }
                            else
                            {
                                logger.LogError("psql exited with code {ExitCode}", psqlProcess.ExitCode);
                                if (!string.IsNullOrWhiteSpace(error))
                                {
                                    logger.LogError("psql error: {Error}", error);
                                }

                                throw new InvalidOperationException($"psql failed with exit code {psqlProcess.ExitCode}. Error: {error}");
                            }
                        }
                        else
                        {
                            logger.LogInformation("Database tables exist (users.Users table found)");
                        }
                    }

                    // Verify all expected schemas and tables exist
                    foreach (var schema in Schemas.All)
                    {
                        var tableCountQuery = "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE'";
                        await using (var command = connection.CreateCommand())
                        {
                            command.CommandText = tableCountQuery;
                            var parameter = command.CreateParameter();
                            parameter.Value = schema;
                            command.Parameters.Add(parameter);

                            var tableCount = (long?)await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);

                            if (tableCount > 0)
                            {
                                logger.LogDebug("Schema '{Schema}' contains {TableCount} tables", schema, tableCount);
                            }
                            else
                            {
                                logger.LogWarning("Schema '{Schema}' exists but contains no tables. This may indicate an incomplete initialization.", schema);
                            }
                        }
                    }

                    logger.LogInformation("✅ Database schema verification complete");
                }
                finally
                {
                    if (wasConnectionOpened && connection.State == System.Data.ConnectionState.Open)
                    {
                        await connection.CloseAsync().ConfigureAwait(false);
                    }
                }
            }

            // NOTE: EF Core migrations are DISABLED for .NET 11 preview
            // Database schema is created via SQL scripts (create_database_schema.sql)
            // Any future migrations must be applied manually using SQL scripts
            logger.LogInformation("EF Core migration system is disabled for .NET 11 preview. Use SQL scripts for schema changes.");
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Failed to verify or initialize PostgreSQL database");
            throw;
        }
    }

    /// <inheritdoc/>
    public async Task RunScheduledOptimisation(CancellationToken cancellationToken)
    {
        var context = await DbContextFactory!.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
        await using (context.ConfigureAwait(false))
        {
            // Get the database connection
            var connection = context.Database.GetDbConnection();
            var wasOpened = false;

            try
            {
                if (connection.State != System.Data.ConnectionState.Open)
                {
                    await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
                    wasOpened = true;
                }

                // Run PostgreSQL optimization commands on all tables in each schema
                foreach (var schema in Schemas.All)
                {
                    // Get all tables in the schema
                    var tablesQuery = @"
                        SELECT table_name
                        FROM information_schema.tables
                        WHERE table_schema = @schema AND table_type = 'BASE TABLE'";

                    var tables = new List<string>();
                    await using (var command = connection.CreateCommand())
                    {
                        command.CommandText = tablesQuery;
                        var parameter = command.CreateParameter();
                        parameter.ParameterName = "@schema";
                        parameter.Value = schema;
                        command.Parameters.Add(parameter);

                        await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
                        while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
                        {
                            tables.Add(reader.GetString(0));
                        }
                    }

                    // Vacuum each table in the schema
                    foreach (var table in tables)
                    {
#pragma warning disable EF1002 // Schema and table names are from database metadata, not user input
                        await context.Database.ExecuteSqlRawAsync($"VACUUM ANALYZE \"{schema}\".\"{table}\"", cancellationToken).ConfigureAwait(false);
#pragma warning restore EF1002
                        logger.LogDebug("Optimized PostgreSQL table '{Schema}.{Table}'", schema, table);
                    }

                    logger.LogDebug("Completed optimization for PostgreSQL schema '{Schema}' ({TableCount} tables)", schema, tables.Count);
                }

                logger.LogInformation("PostgreSQL database optimized successfully!");
            }
            finally
            {
                if (wasOpened)
                {
                    await connection.CloseAsync().ConfigureAwait(false);
                }
            }
        }
    }

    /// <inheritdoc/>
    public void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.SetDefaultDateTimeKind(DateTimeKind.Utc);

        // Assign entities to schemas based on their legacy database origin
        AssignEntitiesToSchemas(modelBuilder);

        // Register read-only view projections (PostgreSQL only)
        RegisterViewMappings(modelBuilder);
    }

    /// <summary>
    /// Assigns entities to PostgreSQL schemas based on their legacy database origin.
    /// </summary>
    /// <param name="modelBuilder">The model builder.</param>
    private static void AssignEntitiesToSchemas(ModelBuilder modelBuilder)
    {
        // ActivityLog schema (legacy: activitylog.db)
        modelBuilder.Entity<ActivityLog>().ToTable("activity_logs", Schemas.ActivityLog);

        // Authentication schema (legacy: authentication.db)
        modelBuilder.Entity<ApiKey>().ToTable("api_keys", Schemas.Authentication);
        modelBuilder.Entity<Device>().ToTable("devices", Schemas.Authentication);
        modelBuilder.Entity<DeviceOptions>().ToTable("device_options", Schemas.Authentication);

        // DisplayPreferences schema (legacy: displaypreferences.db)
        modelBuilder.Entity<DisplayPreferences>().ToTable("display_preferences", Schemas.DisplayPreferences);
        modelBuilder.Entity<ItemDisplayPreferences>().ToTable("item_display_preferences", Schemas.DisplayPreferences);
        modelBuilder.Entity<CustomItemDisplayPreferences>().ToTable("custom_item_display_preferences", Schemas.DisplayPreferences);
        modelBuilder.Entity<HomeSection>().ToTable("home_sections", Schemas.DisplayPreferences);

        // Users schema (legacy: users.db)
        modelBuilder.Entity<User>().ToTable("users", Schemas.Users);
        modelBuilder.Entity<Permission>().ToTable("permissions", Schemas.Users);
        modelBuilder.Entity<Preference>().ToTable("preferences", Schemas.Users);
        modelBuilder.Entity<AccessSchedule>().ToTable("access_schedules", Schemas.Users);

        // Library schema (legacy: library.db) - All remaining entities
        modelBuilder.Entity<BaseItemEntity>().ToTable("base_items", Schemas.Library);
        modelBuilder.Entity<Chapter>().ToTable("chapters", Schemas.Library);
        modelBuilder.Entity<MediaStreamInfo>().ToTable("media_stream_infos", Schemas.Library);
        modelBuilder.Entity<AttachmentStreamInfo>().ToTable("attachment_stream_infos", Schemas.Library);
        modelBuilder.Entity<ImageInfo>().ToTable("image_infos", Schemas.Library);
        modelBuilder.Entity<BaseItemImageInfo>().ToTable("base_item_image_infos", Schemas.Library);
        modelBuilder.Entity<BaseItemProvider>().ToTable("base_item_providers", Schemas.Library);
        modelBuilder.Entity<BaseItemMetadataField>().ToTable("base_item_metadata_fields", Schemas.Library);
        modelBuilder.Entity<BaseItemTrailerType>().ToTable("base_item_trailer_types", Schemas.Library);
        modelBuilder.Entity<ItemValue>().ToTable("item_values", Schemas.Library);
        modelBuilder.Entity<ItemValueMap>().ToTable("item_values_map", Schemas.Library);
        modelBuilder.Entity<People>().ToTable("peoples", Schemas.Library);
        modelBuilder.Entity<PeopleBaseItemMap>().ToTable("people_base_item_map", Schemas.Library);
        modelBuilder.Entity<UserData>().ToTable("user_data", Schemas.Library);
        modelBuilder.Entity<AncestorId>().ToTable("ancestor_ids", Schemas.Library);
        modelBuilder.Entity<TrickplayInfo>().ToTable("trickplay_infos", Schemas.Library);
        modelBuilder.Entity<MediaSegment>().ToTable("media_segments", Schemas.Library);
        modelBuilder.Entity<KeyframeData>().ToTable("keyframe_data", Schemas.Library);
        modelBuilder.Entity<LibraryOptionsEntity>().ToTable("library_options", Schemas.Library);

        modelBuilder.Entity<LibraryOptionsEntity>().HasKey(e => e.LibraryPath);
        modelBuilder.Entity<LibraryOptionsEntity>().HasIndex(e => e.DateModified);
    }

    /// <summary>
    /// Registers read-only keyless view projections for PostgreSQL.
    /// Each view lives in the <c>library</c> schema and is mapped with
    /// <c>HasNoKey().ToView()</c> so EF Core never tries to create a table or migration for it.
    /// </summary>
    /// <param name="modelBuilder">The model builder.</param>
    private static void RegisterViewMappings(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MediaStreamInfoView>()
            .HasNoKey()
            .ToView("media_stream_infos_v", Schemas.Library);

        modelBuilder.Entity<VideoItemView>()
            .HasNoKey()
            .ToView("video_items_v", Schemas.Library);

        modelBuilder.Entity<UserPlaybackHistoryView>()
            .HasNoKey()
            .ToView("user_playback_history_v", Schemas.Library);

        modelBuilder.Entity<ItemProviderView>()
            .HasNoKey()
            .ToView("item_providers_v", Schemas.Library);

        modelBuilder.Entity<ItemPersonView>()
            .HasNoKey()
            .ToView("item_people_v", Schemas.Library);

        modelBuilder.Entity<LibrarySummaryView>()
            .HasNoKey()
            .ToView("library_summary_v", Schemas.Library);
    }

    /// <inheritdoc/>
    public Task RunShutdownTask(CancellationToken cancellationToken)
    {
        if (DbContextFactory is null)
        {
            return Task.CompletedTask;
        }

        // Clear connection pools
        NpgsqlConnection.ClearAllPools();
        return Task.CompletedTask;
    }

    /// <inheritdoc/>
    public void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        configurationBuilder.Conventions.Add(_ => new SnakeCaseNamingConvention());
    }

    /// <inheritdoc />
    public async Task<string> MigrationBackupFast(CancellationToken cancellationToken)
    {
        if (IsLocalHost(currentHost) && backupService is not null)
        {
            try
            {
                var backupDirectory = Path.Combine(applicationPaths.DataPath, "backups");
                var backupPath = await backupService.CreateBackupAsync(backupDirectory, cancellationToken).ConfigureAwait(false);
                logger.LogInformation("PostgreSQL local backup created: {BackupPath}", backupPath);
                return Path.GetFileName(backupPath);
            }
            catch (PostgresVersionMismatchException versionEx)
            {
                logger.LogError(
                    versionEx,
                    "PostgreSQL version mismatch: Server version {ServerVersion} is incompatible with pg_dump version {ClientVersion}. " +
                    "Please upgrade your PostgreSQL client tools (pg_dump/pg_restore) to match or exceed the server version. " +
                    "You can install the latest PostgreSQL client tools from: https://www.postgresql.org/download/",
                    versionEx.ServerVersion,
                    versionEx.ClientVersion);
                throw;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Failed to create local PostgreSQL backup using pg_dump");
                throw;
            }
        }

        var key = DateTime.UtcNow.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
        logger.LogWarning("Fast backup is not implemented for PostgreSQL on a remote server. Use pg_dump manually for proper backups.");
        return await Task.FromResult(key).ConfigureAwait(false);
    }

    /// <inheritdoc />
    public async Task RestoreBackupFast(string key, CancellationToken cancellationToken)
    {
        if (IsLocalHost(currentHost) && backupService is not null)
        {
            try
            {
                var backupDirectory = Path.Combine(applicationPaths.DataPath, "backups");
                var backupPath = Path.Combine(backupDirectory, key);

                if (!File.Exists(backupPath))
                {
                    // Try to find the backup file with common extensions
                    var possibleExtensions = new[] { ".dump", ".sql", ".tar" };
                    foreach (var ext in possibleExtensions)
                    {
                        var pathWithExt = backupPath + ext;
                        if (File.Exists(pathWithExt))
                        {
                            backupPath = pathWithExt;
                            break;
                        }
                    }
                }

                if (File.Exists(backupPath))
                {
                    await backupService.RestoreBackupAsync(backupPath, cancellationToken).ConfigureAwait(false);
                    logger.LogInformation("PostgreSQL local backup restored from: {BackupPath}", backupPath);
                    return;
                }

                logger.LogWarning("Backup file not found: {BackupPath}", backupPath);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Failed to restore local PostgreSQL backup using pg_restore");
                throw;
            }
        }

        logger.LogWarning("Fast restore is not implemented for PostgreSQL on a remote server. Use pg_restore manually for proper restoration.");
    }

    /// <inheritdoc />
    public Task DeleteBackup(string key)
    {
        if (IsLocalHost(currentHost) && backupService is not null)
        {
            try
            {
                var backupDirectory = Path.Combine(applicationPaths.DataPath, "backups");
                var backupPath = Path.Combine(backupDirectory, key);

                // Try to find and delete the backup file
                if (File.Exists(backupPath))
                {
                    File.Delete(backupPath);
                    logger.LogInformation("Deleted local PostgreSQL backup: {BackupPath}", backupPath);
                }
                else
                {
                    // Try with common extensions
                    var possibleExtensions = new[] { ".dump", ".sql", ".tar" };
                    foreach (var ext in possibleExtensions)
                    {
                        var pathWithExt = backupPath + ext;
                        if (File.Exists(pathWithExt))
                        {
                            File.Delete(pathWithExt);
                            logger.LogInformation("Deleted local PostgreSQL backup: {BackupPath}", pathWithExt);
                            return Task.CompletedTask;
                        }
                    }

                    logger.LogWarning("Backup file not found for deletion: {BackupPath}", backupPath);
                }
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Failed to delete local PostgreSQL backup: {BackupKey}", key);
            }

            return Task.CompletedTask;
        }

        logger.LogWarning("Backup deletion is not implemented for PostgreSQL on a remote server. Manage backups externally.");
        return Task.CompletedTask;
    }

    /// <summary>
    /// Determines if the given host is localhost.
    /// </summary>
    /// <param name="host">The host to check.</param>
    /// <returns>True if the host is localhost or 127.0.0.1, false otherwise.</returns>
    private static bool IsLocalHost(string? host)
    {
        if (string.IsNullOrWhiteSpace(host))
        {
            return false;
        }

        return host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
               host.Equals("127.0.0.1", StringComparison.Ordinal) ||
               host.Equals("::1", StringComparison.Ordinal); // IPv6 localhost
    }

    /// <inheritdoc/>
    public async Task PurgeDatabase(JellyfinDbContext dbContext, IEnumerable<string>? tableNames)
    {
        ArgumentNullException.ThrowIfNull(tableNames);

        var deleteQueries = new List<string>();
        foreach (var tableName in tableNames)
        {
            // Find the schema for this table
            var entityType = dbContext.Model.GetEntityTypes()
                .FirstOrDefault(et => et.GetTableName() == tableName);

            if (entityType is not null)
            {
                var schema = entityType.GetSchema() ?? "public";
                deleteQueries.Add($"TRUNCATE TABLE \"{schema}\".\"{tableName}\" CASCADE;");
            }
            else
            {
                // Fallback to public schema if entity not found
                deleteQueries.Add($"TRUNCATE TABLE \"public\".\"{tableName}\" CASCADE;");
            }
        }

        var deleteAllQuery = string.Join('\n', deleteQueries);

        await dbContext.Database.ExecuteSqlRawAsync(deleteAllQuery).ConfigureAwait(false);
    }

    /// <summary>
    /// Finds the full path to the schema initialization script.
    /// </summary>
    /// <returns>The full path to the script, or null if not found.</returns>
    private string? FindSchemaScriptPath()
    {
        var scriptName = Path.Combine("schema_init", "create_database_schema.sql");

        // 1. Check relative to the executing assembly
        var assemblyLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        if (!string.IsNullOrEmpty(assemblyLocation))
        {
            var assemblySqlPath = Path.Combine(assemblyLocation, "sql", scriptName);
            if (File.Exists(assemblySqlPath))
            {
                logger.LogInformation("Found schema script at assembly path: {Path}", assemblySqlPath);
                return assemblySqlPath;
            }
        }

        // 2. Check relative to the current working directory
        var currentDirSqlPath = Path.Combine(Directory.GetCurrentDirectory(), "sql", scriptName);
        if (File.Exists(currentDirSqlPath))
        {
            logger.LogInformation("Found schema script at current directory path: {Path}", currentDirSqlPath);
            return currentDirSqlPath;
        }

        // 3. Fallback for development environment (navigate up from assembly location)
        if (!string.IsNullOrEmpty(assemblyLocation))
        {
            var dir = new DirectoryInfo(assemblyLocation);
            var root = dir.Parent?.Parent?.Parent?.Parent?.Parent; // Adjust depth as needed
            if (root != null)
            {
                var devSqlPath = Path.Combine(root.FullName, "sql", scriptName);
                if (File.Exists(devSqlPath))
                {
                    logger.LogInformation("Found schema script at development path: {Path}", devSqlPath);
                    return devSqlPath;
                }
            }
        }

        logger.LogError("Schema initialization script not found in any known location.");
        return null;
    }

    /// <summary>
    /// Finds the path to the psql executable.
    /// </summary>
    /// <returns>The path to psql, or just "psql" if it's in the system PATH.</returns>
    private static string FindPsqlPath()
    {
        // For now, assume psql is in the PATH.
        // Future improvements could search known installation directories.
        return "psql";
    }
}
