import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
import { databaseLogger } from "./logger.js";
import { DatabaseFileEncryption } from "./database-file-encryption.js";

export interface MigrationResult {
  success: boolean;
  error?: string;
  migratedTables: number;
  migratedRows: number;
  backupPath?: string;
  duration: number;
}

export interface MigrationStatus {
  needsMigration: boolean;
  hasUnencryptedDb: boolean;
  hasEncryptedDb: boolean;
  unencryptedDbSize: number;
  reason: string;
}

export class DatabaseMigration {
  private dataDir: string;
  private unencryptedDbPath: string;
  private encryptedDbPath: string;

  constructor(dataDir: string) {
    this.dataDir = dataDir;
    this.unencryptedDbPath = path.join(dataDir, "db.sqlite");
    this.encryptedDbPath = `${this.unencryptedDbPath}.encrypted`;
  }

  checkMigrationStatus(): MigrationStatus {
    const hasUnencryptedDb = fs.existsSync(this.unencryptedDbPath);
    const hasEncryptedDb = DatabaseFileEncryption.isEncryptedDatabaseFile(
      this.encryptedDbPath,
    );

    let unencryptedDbSize = 0;
    if (hasUnencryptedDb) {
      try {
        unencryptedDbSize = fs.statSync(this.unencryptedDbPath).size;
      } catch (error) {
        databaseLogger.warn("Could not get unencrypted database file size", {
          operation: "migration_status_check",
          error: error instanceof Error ? error.message : "Unknown error",
        });
      }
    }

    let needsMigration = false;
    let reason = "";

    if (hasEncryptedDb && hasUnencryptedDb) {
      const unencryptedSize = fs.statSync(this.unencryptedDbPath).size;

      if (unencryptedSize === 0) {
        needsMigration = false;
        reason =
          "Empty unencrypted database found alongside encrypted database. Removing empty file.";
        try {
          fs.unlinkSync(this.unencryptedDbPath);
        } catch (error) {
          databaseLogger.warn("Failed to remove empty unencrypted database", {
            operation: "migration_cleanup_empty_failed",
            error: error instanceof Error ? error.message : "Unknown error",
          });
        }
      } else {
        needsMigration = false;
        reason =
          "Both encrypted and unencrypted databases exist. Skipping migration for safety. Manual intervention may be required.";
      }
    } else if (hasEncryptedDb && !hasUnencryptedDb) {
      needsMigration = false;
      reason = "Only encrypted database exists. No migration needed.";
    } else if (!hasEncryptedDb && hasUnencryptedDb) {
      needsMigration = true;
      reason =
        "Unencrypted database found. Migration to encrypted format required.";
    } else {
      needsMigration = false;
      reason = "No existing database found. This is a fresh installation.";
    }

    return {
      needsMigration,
      hasUnencryptedDb,
      hasEncryptedDb,
      unencryptedDbSize,
      reason,
    };
  }

  private createBackup(): string {
    const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
    const backupPath = `${this.unencryptedDbPath}.migration-backup-${timestamp}`;

    try {
      fs.copyFileSync(this.unencryptedDbPath, backupPath);

      const originalSize = fs.statSync(this.unencryptedDbPath).size;
      const backupSize = fs.statSync(backupPath).size;

      if (originalSize !== backupSize) {
        throw new Error(
          `Backup size mismatch: original=${originalSize}, backup=${backupSize}`,
        );
      }

      return backupPath;
    } catch (error) {
      databaseLogger.error("Failed to create migration backup", error, {
        operation: "migration_backup_failed",
        source: this.unencryptedDbPath,
        backup: backupPath,
      });
      throw new Error(
        `Backup creation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
      );
    }
  }

  private async verifyMigration(
    originalDb: Database.Database,
    memoryDb: Database.Database,
  ): Promise<boolean> {
    try {
      memoryDb.exec("PRAGMA foreign_keys = OFF");

      const originalTables = originalDb
        .prepare(
          `
          SELECT name FROM sqlite_master
          WHERE type='table' AND name NOT LIKE 'sqlite_%'
          ORDER BY name
        `,
        )
        .all() as { name: string }[];

      const memoryTables = memoryDb
        .prepare(
          `
          SELECT name FROM sqlite_master
          WHERE type='table' AND name NOT LIKE 'sqlite_%'
          ORDER BY name
        `,
        )
        .all() as { name: string }[];

      if (originalTables.length !== memoryTables.length) {
        databaseLogger.error(
          "Table count mismatch during migration verification",
          null,
          {
            operation: "migration_verify_failed",
            originalCount: originalTables.length,
            memoryCount: memoryTables.length,
          },
        );
        return false;
      }

      for (const table of originalTables) {
        const originalCount = originalDb
          .prepare(`SELECT COUNT(*) as count FROM ${table.name}`)
          .get() as { count: number };
        const memoryCount = memoryDb
          .prepare(`SELECT COUNT(*) as count FROM ${table.name}`)
          .get() as { count: number };

        if (originalCount.count !== memoryCount.count) {
          databaseLogger.error(
            "Row count mismatch for table during migration verification",
            null,
            {
              operation: "migration_verify_table_failed",
              table: table.name,
              originalRows: originalCount.count,
              memoryRows: memoryCount.count,
            },
          );
          return false;
        }
      }

      memoryDb.exec("PRAGMA foreign_keys = ON");

      return true;
    } catch (error) {
      databaseLogger.error("Migration verification failed", error, {
        operation: "migration_verify_error",
      });
      return false;
    }
  }

  async migrateDatabase(): Promise<MigrationResult> {
    const startTime = Date.now();
    let backupPath: string | undefined;
    let migratedTables = 0;
    let migratedRows = 0;

    try {
      backupPath = this.createBackup();

      const originalDb = new Database(this.unencryptedDbPath, {
        readonly: true,
      });

      const memoryDb = new Database(":memory:");

      try {
        const tables = originalDb
          .prepare(
            `
            SELECT name, sql FROM sqlite_master
            WHERE type='table' AND name NOT LIKE 'sqlite_%'
          `,
          )
          .all() as { name: string; sql: string }[];

        for (const table of tables) {
          memoryDb.exec(table.sql);
          migratedTables++;
        }

        memoryDb.exec("PRAGMA foreign_keys = OFF");

        for (const table of tables) {
          const rows = originalDb
            .prepare(`SELECT * FROM ${table.name}`)
            .all() as Record<string, unknown>[];

          if (rows.length > 0) {
            const columns = Object.keys(rows[0]);
            const placeholders = columns.map(() => "?").join(", ");
            const insertStmt = memoryDb.prepare(
              `INSERT INTO ${table.name} (${columns.join(", ")}) VALUES (${placeholders})`,
            );

            const insertTransaction = memoryDb.transaction(
              (dataRows: Record<string, unknown>[]) => {
                for (const row of dataRows) {
                  const values = columns.map((col) => row[col]);
                  insertStmt.run(values);
                }
              },
            );

            insertTransaction(rows);
            migratedRows += rows.length;
          }
        }

        memoryDb.exec("PRAGMA foreign_keys = ON");

        const fkCheckResult = memoryDb
          .prepare("PRAGMA foreign_key_check")
          .all();
        if (fkCheckResult.length > 0) {
          databaseLogger.error(
            "Foreign key constraints violations detected after migration",
            null,
            {
              operation: "migration_fk_check_failed",
              violations: fkCheckResult,
            },
          );
          throw new Error(
            `Foreign key violations detected: ${JSON.stringify(fkCheckResult)}`,
          );
        }

        const verificationPassed = await this.verifyMigration(
          originalDb,
          memoryDb,
        );
        if (!verificationPassed) {
          throw new Error("Migration integrity verification failed");
        }

        const buffer = memoryDb.serialize();

        await DatabaseFileEncryption.encryptDatabaseFromBuffer(
          buffer,
          this.encryptedDbPath,
        );

        if (
          !DatabaseFileEncryption.isEncryptedDatabaseFile(this.encryptedDbPath)
        ) {
          throw new Error("Encrypted database file verification failed");
        }

        const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
        const migratedPath = `${this.unencryptedDbPath}.migrated-${timestamp}`;

        fs.renameSync(this.unencryptedDbPath, migratedPath);

        databaseLogger.success("Database migration completed successfully", {
          operation: "migration_complete",
          migratedTables,
          migratedRows,
          duration: Date.now() - startTime,
          backupPath,
          migratedPath,
          encryptedDbPath: this.encryptedDbPath,
        });

        return {
          success: true,
          migratedTables,
          migratedRows,
          backupPath,
          duration: Date.now() - startTime,
        };
      } finally {
        originalDb.close();
        memoryDb.close();
      }
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : "Unknown error";

      databaseLogger.error("Database migration failed", error, {
        operation: "migration_failed",
        migratedTables,
        migratedRows,
        duration: Date.now() - startTime,
        backupPath,
      });

      return {
        success: false,
        error: errorMessage,
        migratedTables,
        migratedRows,
        backupPath,
        duration: Date.now() - startTime,
      };
    }
  }

  cleanupOldBackups(): void {
    try {
      const backupPattern =
        /\.migration-backup-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/;
      const migratedPattern =
        /\.migrated-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/;

      const files = fs.readdirSync(this.dataDir);

      const backupFiles = files
        .filter((f) => backupPattern.test(f))
        .map((f) => ({
          name: f,
          path: path.join(this.dataDir, f),
          mtime: fs.statSync(path.join(this.dataDir, f)).mtime,
        }))
        .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());

      const migratedFiles = files
        .filter((f) => migratedPattern.test(f))
        .map((f) => ({
          name: f,
          path: path.join(this.dataDir, f),
          mtime: fs.statSync(path.join(this.dataDir, f)).mtime,
        }))
        .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());

      const backupsToDelete = backupFiles.slice(3);
      const migratedToDelete = migratedFiles.slice(3);

      for (const file of [...backupsToDelete, ...migratedToDelete]) {
        try {
          fs.unlinkSync(file.path);
        } catch (error) {
          databaseLogger.warn("Failed to cleanup old migration file", {
            operation: "migration_cleanup_failed",
            file: file.name,
            error: error instanceof Error ? error.message : "Unknown error",
          });
        }
      }
    } catch (error) {
      databaseLogger.warn("Migration cleanup failed", {
        operation: "migration_cleanup_error",
        error: error instanceof Error ? error.message : "Unknown error",
      });
    }
  }
}
