package com.storycove.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.storycove.repository.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.sql.DataSource;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

@Service
public class DatabaseManagementService {

    @Autowired
    @Qualifier("dataSource") // Use the primary routing datasource
    private DataSource dataSource;
    
    // Use the routing datasource which automatically handles library switching
    private DataSource getDataSource() {
        return dataSource;
    }

    @Autowired
    private StoryRepository storyRepository;

    @Autowired
    private AuthorRepository authorRepository;

    @Autowired
    private SeriesRepository seriesRepository;

    @Autowired
    private TagRepository tagRepository;

    @Autowired
    private CollectionRepository collectionRepository;

    @Autowired
    private SearchServiceAdapter searchServiceAdapter;
    
    @Autowired
    private LibraryService libraryService;

    @Autowired
    private ReadingPositionRepository readingPositionRepository;

    @Value("${storycove.images.upload-dir:/app/images}")
    private String uploadDir;
    
    // Helper methods to extract database connection details
    private String extractDatabaseUrl() {
        try (Connection connection = getDataSource().getConnection()) {
            return connection.getMetaData().getURL();
        } catch (SQLException e) {
            throw new RuntimeException("Failed to extract database URL", e);
        }
    }

    private String extractDatabaseHost() {
        String url = extractDatabaseUrl();
        // Extract host from jdbc:postgresql://host:port/database
        if (url.startsWith("jdbc:postgresql://")) {
            String hostPort = url.substring("jdbc:postgresql://".length());
            if (hostPort.contains("/")) {
                hostPort = hostPort.substring(0, hostPort.indexOf("/"));
            }
            if (hostPort.contains(":")) {
                return hostPort.substring(0, hostPort.indexOf(":"));
            }
            return hostPort;
        }
        return "localhost"; // fallback
    }

    private String extractDatabasePort() {
        String url = extractDatabaseUrl();
        // Extract port from jdbc:postgresql://host:port/database
        if (url.startsWith("jdbc:postgresql://")) {
            String hostPort = url.substring("jdbc:postgresql://".length());
            if (hostPort.contains("/")) {
                hostPort = hostPort.substring(0, hostPort.indexOf("/"));
            }
            if (hostPort.contains(":")) {
                return hostPort.substring(hostPort.indexOf(":") + 1);
            }
        }
        return "5432"; // default PostgreSQL port
    }

    private String extractDatabaseName() {
        String url = extractDatabaseUrl();
        // Extract database name from jdbc:postgresql://host:port/database
        if (url.startsWith("jdbc:postgresql://")) {
            String remaining = url.substring("jdbc:postgresql://".length());
            if (remaining.contains("/")) {
                String dbPart = remaining.substring(remaining.indexOf("/") + 1);
                // Remove any query parameters
                if (dbPart.contains("?")) {
                    dbPart = dbPart.substring(0, dbPart.indexOf("?"));
                }
                return dbPart;
            }
        }
        return "storycove"; // fallback
    }

    private String extractDatabaseUsername() {
        // Get from environment variable or default
        return System.getenv("SPRING_DATASOURCE_USERNAME") != null ?
               System.getenv("SPRING_DATASOURCE_USERNAME") : "storycove";
    }

    private String extractDatabasePassword() {
        // Get from environment variable or default
        return System.getenv("SPRING_DATASOURCE_PASSWORD") != null ?
               System.getenv("SPRING_DATASOURCE_PASSWORD") : "password";
    }

    /**
     * Create a comprehensive backup including database and files in ZIP format
     * Returns a streaming resource to avoid loading large backups into memory
     */
    public Resource createCompleteBackup() throws SQLException, IOException {
        // Create temp file with deleteOnExit as safety net
        Path tempZip = Files.createTempFile("storycove-backup", ".zip");
        tempZip.toFile().deleteOnExit();

        try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(tempZip))) {
            // 1. Add database dump
            addDatabaseDumpToZip(zipOut);

            // 2. Add all image files
            addFilesToZip(zipOut);

            // 3. Add metadata
            addMetadataToZip(zipOut);
        }

        // Return the ZIP file as a FileSystemResource for streaming
        // This avoids loading the entire file into memory
        return new org.springframework.core.io.FileSystemResource(tempZip.toFile()) {
            @Override
            public InputStream getInputStream() throws IOException {
                // Wrap the input stream to delete the temp file after it's fully read
                return new java.io.FilterInputStream(super.getInputStream()) {
                    @Override
                    public void close() throws IOException {
                        try {
                            super.close();
                        } finally {
                            // Clean up temp file after streaming is complete
                            try {
                                Files.deleteIfExists(tempZip);
                            } catch (IOException e) {
                                // Log but don't fail - deleteOnExit will handle it
                                System.err.println("Warning: Could not delete temp backup file: " + e.getMessage());
                            }
                        }
                    }
                };
            }
        };
    }

    /**
     * Restore from complete backup (ZIP format)
     */
    // Not @Transactional: this method launches psql as an external process. If a Spring
    // transaction were active here, the ROW EXCLUSIVE locks held by clearAllDataAndFiles()
    // would block psql's DROP TABLE IF EXISTS (ACCESS EXCLUSIVE), causing a deadlock.
    // Each inner method (clearAllDataAndFiles, restoreFromBackup) manages its own transaction.
    public void restoreFromCompleteBackup(InputStream backupStream) throws IOException, SQLException {
        String currentLibraryId = libraryService.getCurrentLibraryId();
        System.err.println("Starting complete backup restore for library: " + currentLibraryId);
        if (currentLibraryId == null) {
            throw new IllegalStateException("No current library active - please authenticate and select a library first");
        }
        
        Path tempDir = Files.createTempDirectory("storycove-restore");
        System.err.println("Created temp directory: " + tempDir);
        
        try {
            // 1. Extract ZIP to temp directory
            System.err.println("Extracting ZIP archive...");
            extractZipArchive(backupStream, tempDir);
            System.err.println("ZIP extraction completed.");
            
            // 2. Validate backup structure
            System.err.println("Validating backup structure...");
            validateBackupStructure(tempDir);
            System.err.println("Backup structure validation completed.");
            
            // 3. Clear existing data and files
            System.err.println("Clearing existing data and files...");
            clearAllDataAndFiles();
            System.err.println("Clear operation completed.");
            
            // 4. Restore database
            Path databaseFile = tempDir.resolve("database.sql");
            if (Files.exists(databaseFile)) {
                System.err.println("Restoring database from SQL file...");
                try (InputStream sqlStream = Files.newInputStream(databaseFile)) {
                    restoreFromBackup(sqlStream);
                }
                System.err.println("Database restore completed.");
            } else {
                System.err.println("Warning: No database.sql file found in backup.");
            }
            
            // 5. Restore files
            Path filesDir = tempDir.resolve("files");
            if (Files.exists(filesDir)) {
                System.err.println("Restoring files...");
                restoreFiles(filesDir);
                System.err.println("File restore completed.");
            } else {
                System.err.println("No files directory found in backup - skipping file restore.");
            }
            
            // 6. Trigger complete search index reindex after data restoration.
            // We fetch the data directly from the repositories already injected into this service
            // and bulk-index it. This avoids the incomplete performCompleteReindex() path which
            // only recreates the schema without repopulating data.
            try {
                System.err.println("Starting search index reindex after restore...");
                if (searchServiceAdapter.isSearchServiceAvailable()) {
                    List<com.storycove.entity.Story> stories = storyRepository.findAllWithAssociations();
                    List<com.storycove.entity.Author> authors = authorRepository.findAll();
                    List<com.storycove.entity.Collection> collections = collectionRepository.findAllWithTags();
                    searchServiceAdapter.bulkIndexStories(stories);
                    searchServiceAdapter.bulkIndexAuthors(authors);
                    searchServiceAdapter.bulkIndexCollections(collections);
                    System.err.println("Search index reindex completed: " + stories.size() + " stories, "
                        + authors.size() + " authors, " + collections.size() + " collections.");
                } else {
                    System.err.println("Solr not available — skipping search reindex after restore.");
                }
            } catch (Exception e) {
                System.err.println("Warning: Failed to reindex search after restore: " + e.getMessage());
                // Don't fail the entire restore for search issues
            }
            
            System.err.println("Complete backup restore finished successfully.");
            
        } catch (Exception e) {
            System.err.println("Error during complete backup restore: " + e.getMessage());
            e.printStackTrace();
            throw e;
        } finally {
            // Clean up temp directory
            System.err.println("Cleaning up temp directory: " + tempDir);
            deleteDirectory(tempDir);
            System.err.println("Cleanup completed.");
        }
    }

    public Resource createBackup() throws SQLException, IOException {
        // Use PostgreSQL's native pg_dump for reliable backup
        String dbHost = extractDatabaseHost();
        String dbPort = extractDatabasePort();
        String dbName = extractDatabaseName();
        String dbUser = extractDatabaseUsername();
        String dbPassword = extractDatabasePassword();

        // Create temporary file for backup
        Path tempBackupFile = Files.createTempFile("storycove_backup_", ".sql");

        try {
            // Build pg_dump command.
            // --no-owner / --no-acl: omit ownership and privilege SQL that would fail
            // when restoring to a different environment or under a different role.
            // --create is intentionally omitted: it generates ALTER DATABASE / \connect
            // commands for the source database name which break cross-environment restores.
            ProcessBuilder pb = new ProcessBuilder(
                "pg_dump",
                "--host=" + dbHost,
                "--port=" + dbPort,
                "--username=" + dbUser,
                "--dbname=" + dbName,
                "--no-password",
                "--verbose",
                "--clean",
                "--if-exists",
                "--no-owner",
                "--no-acl",
                "--file=" + tempBackupFile.toString()
            );

            // Set PGPASSWORD environment variable
            Map<String, String> env = pb.environment();
            env.put("PGPASSWORD", dbPassword);

            System.err.println("Starting PostgreSQL backup using pg_dump...");
            Process process = pb.start();

            // Capture output
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    System.err.println("pg_dump: " + line);
                }
            }

            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("pg_dump failed with exit code: " + exitCode);
            }

            System.err.println("PostgreSQL backup completed successfully");

            // Return the backup file as a streaming resource to avoid memory issues with large databases
            tempBackupFile.toFile().deleteOnExit();
            return new org.springframework.core.io.FileSystemResource(tempBackupFile.toFile()) {
                @Override
                public InputStream getInputStream() throws IOException {
                    // Wrap the input stream to delete the temp file after it's fully read
                    return new java.io.FilterInputStream(super.getInputStream()) {
                        @Override
                        public void close() throws IOException {
                            try {
                                super.close();
                            } finally {
                                // Clean up temp file after streaming is complete
                                try {
                                    Files.deleteIfExists(tempBackupFile);
                                } catch (IOException e) {
                                    // Log but don't fail - deleteOnExit will handle it
                                    System.err.println("Warning: Could not delete temp backup file: " + e.getMessage());
                                }
                            }
                        }
                    };
                }
            };

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Backup process was interrupted", e);
        }
    }

    @Transactional(timeout = 1800) // 30 minutes timeout for large backup restores
    public void restoreFromBackup(InputStream backupStream) throws IOException, SQLException {
        // Use PostgreSQL's native psql for reliable restore
        String dbHost = extractDatabaseHost();
        String dbPort = extractDatabasePort();
        String dbName = extractDatabaseName();
        String dbUser = extractDatabaseUsername();
        String dbPassword = extractDatabasePassword();

        // Create temporary file for the backup
        Path tempBackupFile = Files.createTempFile("storycove_restore_", ".sql");

        try {
            // Write backup stream to temporary file, filtering out incompatible commands
            System.err.println("Writing backup data to temporary file...");
            try (InputStream input = backupStream;
                 BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
                 BufferedWriter writer = Files.newBufferedWriter(tempBackupFile, StandardCharsets.UTF_8)) {

                String line;
                while ((line = reader.readLine()) != null) {
                    // Skip database-level commands that were generated by older backups using
                    // pg_dump --create and that would fail when restoring to a different environment.
                    // New backups no longer use --create so these lines won't appear, but we keep
                    // the filters for backward compatibility with previously created backups.
                    String trimmed = line.trim();
                    if (trimmed.startsWith("DROP DATABASE") ||
                        trimmed.startsWith("CREATE DATABASE") ||
                        trimmed.startsWith("\\connect") ||
                        trimmed.startsWith("\\c ") ||         // short-form \connect alias
                        trimmed.startsWith("ALTER DATABASE") ||
                        trimmed.startsWith("COMMENT ON DATABASE")) {
                        System.err.println("Skipping database-level command: " + line.substring(0, Math.min(80, line.length())));
                        continue;
                    }
                    writer.write(line);
                    writer.newLine();
                }
            }

            System.err.println("Starting PostgreSQL restore using psql...");

            // Build psql command to restore the backup.
            // --single-transaction wraps the entire restore in one atomic transaction:
            // if any statement fails (e.g. a COPY for story_tags), the whole restore rolls
            // back and an error is returned rather than leaving the DB in a partial state.
            ProcessBuilder pb = new ProcessBuilder(
                "psql",
                "--host=" + dbHost,
                "--port=" + dbPort,
                "--username=" + dbUser,
                "--dbname=" + dbName,
                "--no-password",
                "--echo-errors",
                "--single-transaction",
                "--file=" + tempBackupFile.toString()
            );

            // Set PGPASSWORD environment variable
            Map<String, String> env = pb.environment();
            env.put("PGPASSWORD", dbPassword);

            Process process = pb.start();

            // Capture output
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
                 BufferedReader outputReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {

                // Read stderr in a separate thread
                Thread errorThread = new Thread(() -> {
                    try {
                        String line;
                        while ((line = reader.readLine()) != null) {
                            System.err.println("psql stderr: " + line);
                        }
                    } catch (IOException e) {
                        System.err.println("Error reading psql stderr: " + e.getMessage());
                    }
                });
                errorThread.start();

                // Read stdout
                String line;
                while ((line = outputReader.readLine()) != null) {
                    System.err.println("psql stdout: " + line);
                }

                errorThread.join();
            }

            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("psql restore failed with exit code: " + exitCode);
            }

            System.err.println("PostgreSQL restore completed successfully");

            // Reindex search after successful restore
            try {
                String currentLibraryId = libraryService.getCurrentLibraryId();
                System.err.println("Starting search reindex after successful restore for library: " + currentLibraryId);
                if (currentLibraryId == null) {
                    System.err.println("ERROR: No current library set during restore - cannot reindex search!");
                    throw new IllegalStateException("No current library active during restore");
                }

                // Manually trigger reindexing using the correct database connection
                System.err.println("Triggering manual reindex from library-specific database for library: " + currentLibraryId);
                reindexStoriesAndAuthorsFromCurrentDatabase();

                // Note: Collections collection will be recreated when needed by the service
                System.err.println("Search reindex completed successfully for library: " + currentLibraryId);
            } catch (Exception e) {
                // Log the error but don't fail the restore
                System.err.println("Warning: Failed to reindex search after restore: " + e.getMessage());
                e.printStackTrace();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Restore process was interrupted", e);
        } finally {
            // Clean up temporary file
            try {
                Files.deleteIfExists(tempBackupFile);
            } catch (IOException e) {
                System.err.println("Warning: Could not delete temporary restore file: " + e.getMessage());
            }
        }
    }

    @Transactional
    public int clearAllData() {
        int totalDeleted = 0;
        
        try {
            // Count entities before deletion
            int collectionCount = (int) collectionRepository.count();
            int storyCount = (int) storyRepository.count();
            int authorCount = (int) authorRepository.count();
            int seriesCount = (int) seriesRepository.count();
            int tagCount = (int) tagRepository.count();
            
            // Clean up reading positions first (to avoid foreign key constraint violations)
            readingPositionRepository.deleteAll();
            
            // Delete main entities (cascade will handle junction tables)
            collectionRepository.deleteAll();
            storyRepository.deleteAll();
            authorRepository.deleteAll();
            seriesRepository.deleteAll();
            tagRepository.deleteAll();
            
            totalDeleted = collectionCount + storyCount + authorCount + seriesCount + tagCount;
            
            // Note: Search indexes will need to be manually recreated after clearing
            // Use the settings page to recreate search indices after clearing the database
            
        } catch (Exception e) {
            throw new RuntimeException("Failed to clear database: " + e.getMessage(), e);
        }
        
        return totalDeleted;
    }
    
    /**
     * Clear all data AND files (for complete restore)
     */
    @Transactional(timeout = 600) // 10 minutes timeout for clearing large datasets
    public int clearAllDataAndFiles() {
        // First clear the database
        int totalDeleted = clearAllData();
        
        // Then clear all uploaded files
        clearAllFiles();
        
        // Clear search indexes
        clearSearchIndexes();
        
        return totalDeleted;
    }

    /**
     * Clear all uploaded files for the current library
     */
    private void clearAllFiles() {
        // Use library-specific image path
        String libraryImagePath = libraryService.getCurrentImagePath();
        Path imagesPath = Paths.get(uploadDir + libraryImagePath);
        
        System.err.println("Clearing files for library: " + libraryService.getCurrentLibraryId() + " at path: " + imagesPath);
        
        if (Files.exists(imagesPath)) {
            try {
                Files.walk(imagesPath)
                    .filter(Files::isRegularFile)
                    .forEach(filePath -> {
                        try {
                            Files.deleteIfExists(filePath);
                            System.err.println("Deleted file: " + filePath);
                        } catch (IOException e) {
                            System.err.println("Warning: Failed to delete file: " + filePath + " - " + e.getMessage());
                        }
                    });
            } catch (IOException e) {
                System.err.println("Warning: Failed to clear files directory: " + e.getMessage());
            }
        } else {
            System.err.println("Library image directory does not exist: " + imagesPath);
        }
    }

    /**
     * Clear search indexes (recreate empty collections)
     */
    private void clearSearchIndexes() {
        try {
            String currentLibraryId = libraryService.getCurrentLibraryId();
            System.err.println("Clearing search indexes after complete clear for library: " + currentLibraryId);
            if (currentLibraryId == null) {
                System.err.println("WARNING: No current library set during clear - skipping search index clear");
                return;
            }
            
            // For clearing, we only want to recreate empty collections (no data to index)
            searchServiceAdapter.recreateIndices();
            // Note: Collections collection will be recreated when needed by the service
            System.err.println("Search indexes cleared successfully for library: " + currentLibraryId);
        } catch (Exception e) {
            // Log the error but don't fail the clear operation
            System.err.println("Warning: Failed to clear search indexes: " + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * Add database dump to ZIP archive
     */
    private void addDatabaseDumpToZip(ZipOutputStream zipOut) throws SQLException, IOException {
        Resource sqlBackup = createBackup();
        
        ZipEntry sqlEntry = new ZipEntry("database.sql");
        zipOut.putNextEntry(sqlEntry);
        
        try (InputStream sqlStream = sqlBackup.getInputStream()) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = sqlStream.read(buffer)) != -1) {
                zipOut.write(buffer, 0, bytesRead);
            }
        }
        
        zipOut.closeEntry();
    }

    /**
     * Add all files to ZIP archive for the current library
     */
    private void addFilesToZip(ZipOutputStream zipOut) throws IOException {
        // Use library-specific image path
        String libraryImagePath = libraryService.getCurrentImagePath();
        Path imagesPath = Paths.get(uploadDir + libraryImagePath);
        
        System.err.println("Adding files to backup for library: " + libraryService.getCurrentLibraryId() + " from path: " + imagesPath);
        
        if (!Files.exists(imagesPath)) {
            System.err.println("Library image directory does not exist, skipping file backup: " + imagesPath);
            return;
        }
        
        Files.walk(imagesPath)
            .filter(Files::isRegularFile)
            .forEach(filePath -> {
                try {
                    Path relativePath = imagesPath.relativize(filePath);
                    String zipEntryName = "files/" + relativePath.toString().replace('\\', '/');
                    
                    ZipEntry entry = new ZipEntry(zipEntryName);
                    zipOut.putNextEntry(entry);
                    Files.copy(filePath, zipOut);
                    zipOut.closeEntry();
                    System.err.println("Added file to backup: " + zipEntryName);
                } catch (IOException e) {
                    throw new RuntimeException("Failed to add file to backup: " + filePath, e);
                }
            });
    }

    /**
     * Add metadata to ZIP archive
     */
    private void addMetadataToZip(ZipOutputStream zipOut) throws IOException, SQLException {
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("version", "1.0");
        metadata.put("format", "storycove-complete-backup");
        metadata.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        metadata.put("generator", "StoryCove Database Management Service");
        
        // Add library information
        var currentLibrary = libraryService.getCurrentLibrary();
        if (currentLibrary != null) {
            Map<String, Object> libraryInfo = new HashMap<>();
            libraryInfo.put("id", currentLibrary.getId());
            libraryInfo.put("name", currentLibrary.getName());
            libraryInfo.put("description", currentLibrary.getDescription());
            metadata.put("library", libraryInfo);
        }
        
        // Add statistics
        Map<String, Object> stats = new HashMap<>();
        try (Connection connection = getDataSource().getConnection()) {
            stats.put("stories", getTableCount(connection, "stories"));
            stats.put("authors", getTableCount(connection, "authors"));
            stats.put("collections", getTableCount(connection, "collections"));
            stats.put("tags", getTableCount(connection, "tags"));
            stats.put("series", getTableCount(connection, "series"));
        }
        metadata.put("statistics", stats);
        
        // Count files for current library
        String libraryImagePath = libraryService.getCurrentImagePath();
        Path imagesPath = Paths.get(uploadDir + libraryImagePath);
        int fileCount = 0;
        if (Files.exists(imagesPath)) {
            fileCount = (int) Files.walk(imagesPath).filter(Files::isRegularFile).count();
        }
        metadata.put("fileCount", fileCount);
        
        ObjectMapper mapper = new ObjectMapper();
        String metadataJson = mapper.writeValueAsString(metadata);
        
        ZipEntry metadataEntry = new ZipEntry("metadata.json");
        zipOut.putNextEntry(metadataEntry);
        zipOut.write(metadataJson.getBytes(StandardCharsets.UTF_8));
        zipOut.closeEntry();
    }

    /**
     * Extract ZIP archive to directory
     */
    private void extractZipArchive(InputStream zipStream, Path targetDir) throws IOException {
        try (ZipInputStream zis = new ZipInputStream(zipStream)) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                Path entryPath = targetDir.resolve(entry.getName());
                
                // Security check: ensure the entry path is within the target directory
                if (!entryPath.normalize().startsWith(targetDir.normalize())) {
                    throw new IOException("ZIP entry is outside of target directory: " + entry.getName());
                }
                
                if (entry.isDirectory()) {
                    Files.createDirectories(entryPath);
                } else {
                    Files.createDirectories(entryPath.getParent());
                    Files.copy(zis, entryPath, StandardCopyOption.REPLACE_EXISTING);
                }
                
                zis.closeEntry();
            }
        }
    }

    /**
     * Validate backup structure
     */
    private void validateBackupStructure(Path backupDir) throws IOException {
        Path metadataFile = backupDir.resolve("metadata.json");
        Path databaseFile = backupDir.resolve("database.sql");
        
        if (!Files.exists(metadataFile)) {
            throw new IOException("Invalid backup: metadata.json not found");
        }
        
        if (!Files.exists(databaseFile)) {
            throw new IOException("Invalid backup: database.sql not found");
        }
        
        // Validate metadata
        try {
            ObjectMapper mapper = new ObjectMapper();
            @SuppressWarnings("unchecked")
            Map<String, Object> metadata = mapper.readValue(Files.newInputStream(metadataFile), Map.class);
            
            String format = (String) metadata.get("format");
            if (!"storycove-complete-backup".equals(format)) {
                throw new IOException("Invalid backup format: " + format);
            }
            
            String version = (String) metadata.get("version");
            if (!"1.0".equals(version)) {
                throw new IOException("Unsupported backup version: " + version);
            }
            
        } catch (Exception e) {
            throw new IOException("Failed to validate backup metadata: " + e.getMessage(), e);
        }
    }

    /**
     * Restore files from backup to the current library's directory
     */
    private void restoreFiles(Path filesDir) throws IOException {
        // Use library-specific image path
        String libraryImagePath = libraryService.getCurrentImagePath();
        Path targetDir = Paths.get(uploadDir + libraryImagePath);
        
        System.err.println("Restoring files for library: " + libraryService.getCurrentLibraryId() + " to path: " + targetDir);
        Files.createDirectories(targetDir);
        
        Files.walk(filesDir)
            .filter(Files::isRegularFile)
            .forEach(sourceFile -> {
                try {
                    Path relativePath = filesDir.relativize(sourceFile);
                    Path targetFile = targetDir.resolve(relativePath);
                    
                    Files.createDirectories(targetFile.getParent());
                    Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
                    System.err.println("Restored file: " + relativePath + " to " + targetFile);
                } catch (IOException e) {
                    throw new RuntimeException("Failed to restore file: " + sourceFile, e);
                }
            });
    }

    /**
     * Delete directory recursively
     */
    private void deleteDirectory(Path directory) throws IOException {
        if (Files.exists(directory)) {
            Files.walk(directory)
                .sorted(Comparator.reverseOrder())
                .forEach(path -> {
                    try {
                        Files.delete(path);
                    } catch (IOException e) {
                        System.err.println("Warning: Failed to delete temp file: " + path);
                    }
                });
        }
    }

    /**
     * Get count of records in a table
     */
    private int getTableCount(Connection connection, String tableName) throws SQLException {
        try (PreparedStatement stmt = connection.prepareStatement("SELECT COUNT(*) FROM \"" + tableName + "\"");
             ResultSet rs = stmt.executeQuery()) {
            if (rs.next()) {
                return rs.getInt(1);
            }
            return 0;
        }
    }
    
    /**
     * Manually reindex stories and authors from the current library's database
     * This bypasses the repository layer and uses direct database access
     */
    private void reindexStoriesAndAuthorsFromCurrentDatabase() throws SQLException {
        try (Connection connection = getDataSource().getConnection()) {
            // First, recreate empty collections
            try {
                searchServiceAdapter.recreateIndices();
            } catch (Exception e) {
                throw new SQLException("Failed to recreate search indices", e);
            }
            
            // Count and reindex stories with full author and series information
            int storyCount = 0;
            String storyQuery = "SELECT s.id, s.title, s.summary, s.description, s.content_html, s.content_plain, s.source_url, s.cover_path, " +
                               "s.word_count, s.rating, s.volume, s.is_read, s.reading_position, s.last_read_at, s.author_id, s.series_id, " +
                               "s.created_at, s.updated_at, " +
                               "a.name as author_name, a.notes as author_notes, a.avatar_image_path as author_avatar, a.author_rating, " +
                               "a.created_at as author_created_at, a.updated_at as author_updated_at, " +
                               "ser.name as series_name, ser.description as series_description, " +
                               "ser.created_at as series_created_at " +
                               "FROM stories s " +
                               "LEFT JOIN authors a ON s.author_id = a.id " +
                               "LEFT JOIN series ser ON s.series_id = ser.id";
            
            try (PreparedStatement stmt = connection.prepareStatement(storyQuery);
                 ResultSet rs = stmt.executeQuery()) {
                
                while (rs.next()) {
                    // Create a complete Story object for indexing
                    var story = createStoryFromResultSet(rs);
                    searchServiceAdapter.indexStory(story);
                    storyCount++;
                }
            }
            
            // Count and reindex authors
            int authorCount = 0;
            String authorQuery = "SELECT id, name, notes, avatar_image_path, author_rating, created_at, updated_at FROM authors";
            
            try (PreparedStatement stmt = connection.prepareStatement(authorQuery);
                 ResultSet rs = stmt.executeQuery()) {
                
                while (rs.next()) {
                    // Create a minimal Author object for indexing
                    var author = createAuthorFromResultSet(rs);
                    searchServiceAdapter.indexAuthor(author);
                    authorCount++;
                }
            }
            
            System.err.println("Reindexed " + storyCount + " stories and " + authorCount + " authors from library database");
        }
    }
    
    /**
     * Create a Story entity from ResultSet for indexing purposes (includes joined author/series data)
     */
    private com.storycove.entity.Story createStoryFromResultSet(ResultSet rs) throws SQLException {
        var story = new com.storycove.entity.Story();
        story.setId(UUID.fromString(rs.getString("id")));
        story.setTitle(rs.getString("title"));
        story.setSummary(rs.getString("summary"));
        story.setDescription(rs.getString("description"));
        story.setContentHtml(rs.getString("content_html"));
        // Note: contentPlain will be auto-generated from contentHtml by the entity
        story.setSourceUrl(rs.getString("source_url"));
        story.setCoverPath(rs.getString("cover_path"));
        story.setWordCount(rs.getInt("word_count"));
        story.setRating(rs.getInt("rating"));
        story.setVolume(rs.getObject("volume") != null ? rs.getDouble("volume") : null);
        story.setIsRead(rs.getBoolean("is_read"));
        story.setReadingPosition(rs.getInt("reading_position"));
        
        var lastReadAtTimestamp = rs.getTimestamp("last_read_at");
        if (lastReadAtTimestamp != null) {
            story.setLastReadAt(lastReadAtTimestamp.toLocalDateTime());
        }
        
        var createdAtTimestamp = rs.getTimestamp("created_at");
        if (createdAtTimestamp != null) {
            story.setCreatedAt(createdAtTimestamp.toLocalDateTime());
        }
        
        var updatedAtTimestamp = rs.getTimestamp("updated_at");
        if (updatedAtTimestamp != null) {
            story.setUpdatedAt(updatedAtTimestamp.toLocalDateTime());
        }
        
        // Set complete author information
        String authorIdStr = rs.getString("author_id");
        if (authorIdStr != null) {
            var author = new com.storycove.entity.Author();
            author.setId(UUID.fromString(authorIdStr));
            author.setName(rs.getString("author_name"));
            author.setNotes(rs.getString("author_notes"));
            author.setAvatarImagePath(rs.getString("author_avatar"));
            
            Integer authorRating = rs.getInt("author_rating");
            if (!rs.wasNull()) {
                author.setAuthorRating(authorRating);
            }
            
            var authorCreatedAt = rs.getTimestamp("author_created_at");
            if (authorCreatedAt != null) {
                author.setCreatedAt(authorCreatedAt.toLocalDateTime());
            }
            
            var authorUpdatedAt = rs.getTimestamp("author_updated_at");
            if (authorUpdatedAt != null) {
                author.setUpdatedAt(authorUpdatedAt.toLocalDateTime());
            }
            
            story.setAuthor(author);
        }
        
        // Set complete series information
        String seriesIdStr = rs.getString("series_id");
        if (seriesIdStr != null) {
            var series = new com.storycove.entity.Series();
            series.setId(UUID.fromString(seriesIdStr));
            series.setName(rs.getString("series_name"));
            series.setDescription(rs.getString("series_description"));
            
            var seriesCreatedAt = rs.getTimestamp("series_created_at");
            if (seriesCreatedAt != null) {
                series.setCreatedAt(seriesCreatedAt.toLocalDateTime());
            }
            
            story.setSeries(series);
        }
        
        return story;
    }
    
    /**
     * Create an Author entity from ResultSet for indexing purposes
     */
    private com.storycove.entity.Author createAuthorFromResultSet(ResultSet rs) throws SQLException {
        var author = new com.storycove.entity.Author();
        author.setId(UUID.fromString(rs.getString("id")));
        author.setName(rs.getString("name"));
        author.setNotes(rs.getString("notes"));
        author.setAvatarImagePath(rs.getString("avatar_image_path"));
        
        Integer rating = rs.getInt("author_rating");
        if (!rs.wasNull()) {
            author.setAuthorRating(rating);
        }
        
        var createdAtTimestamp = rs.getTimestamp("created_at");
        if (createdAtTimestamp != null) {
            author.setCreatedAt(createdAtTimestamp.toLocalDateTime());
        }
        
        var updatedAtTimestamp = rs.getTimestamp("updated_at");
        if (updatedAtTimestamp != null) {
            author.setUpdatedAt(updatedAtTimestamp.toLocalDateTime());
        }
        
        return author;
    }
}