/**
 * Database Migration Service for TimeSafari
 *
 * This module provides a comprehensive database migration system that manages
 * schema changes as users upgrade their TimeSafari application over time.
 * The system ensures that database changes are applied safely, tracked properly,
 * and can handle edge cases gracefully.
 *
 * ## Architecture Overview
 *
 * The migration system follows these key principles:
 *
 * 1. **Single Application**: Each migration runs exactly once per database
 * 2. **Tracked Execution**: All applied migrations are recorded in a migrations table
 * 3. **Schema Validation**: Actual database schema is validated before and after migrations
 * 4. **Graceful Recovery**: Handles cases where schema exists but tracking is missing
 * 5. **Comprehensive Logging**: Detailed logging for debugging and monitoring
 *
 * ## Migration Flow
 *
 * ```
 * 1. Create migrations table (if needed)
 * 2. Query existing applied migrations
 * 3. For each registered migration:
 *    a. Check if recorded as applied
 *    b. Check if schema already exists
 *    c. Skip if already applied
 *    d. Apply migration SQL
 *    e. Validate schema was created
 *    f. Record migration as applied
 * 4. Final validation of all migrations
 * ```
 *
 * ## Usage Example
 *
 * ```typescript
 * // Register migrations (typically in migration.ts)
 * registerMigration({
 *   name: "001_initial",
 *   sql: "CREATE TABLE accounts (id INTEGER PRIMARY KEY, ...)"
 * });
 *
 * // Run migrations (typically in platform service)
 * await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
 * ```
 *
 * ## Error Handling
 *
 * The system handles several error scenarios:
 * - Duplicate table/column errors (schema already exists)
 * - Migration tracking inconsistencies
 * - Database connection issues
 * - Schema validation failures
 *
 * @author Matthew Raymer
 * @version 1.0.0
 * @since 2025-06-30
 */

import { logger } from "../utils/logger";

/**
 * Migration interface for database schema migrations
 *
 * Represents a single database migration that can be applied to upgrade
 * the database schema. Each migration should be idempotent and focused
 * on a single schema change.
 *
 * @interface Migration
 */
interface Migration {
  /** Unique identifier for the migration (e.g., "001_initial", "002_add_column") */
  name: string;
  /** SQL statement(s) to execute for this migration */
  sql: string;
  /** Optional array of individual SQL statements for better error handling */
  statements?: string[];
}

/**
 * Migration validation result
 *
 * Contains the results of validating that a migration was successfully
 * applied by checking the actual database schema.
 *
 * @interface MigrationValidation
 */
interface MigrationValidation {
  /** Whether the migration validation passed overall */
  isValid: boolean;
  /** Whether expected tables exist */
  tableExists: boolean;
  /** Whether expected columns exist */
  hasExpectedColumns: boolean;
  /** List of validation errors encountered */
  errors: string[];
}

/**
 * Migration registry to store and manage database migrations
 *
 * This class maintains a registry of all migrations that need to be applied
 * to the database. It uses the singleton pattern to ensure migrations are
 * registered once and can be accessed globally.
 *
 * @class MigrationRegistry
 */
class MigrationRegistry {
  /** Array of registered migrations */
  private migrations: Migration[] = [];

  /**
   * Register a migration with the registry
   *
   * Adds a migration to the list of migrations that will be applied when
   * runMigrations() is called. Migrations should be registered in order
   * of their intended execution.
   *
   * @param migration - The migration to register
   * @throws {Error} If migration name is empty or already exists
   *
   * @example
   * ```typescript
   * registry.registerMigration({
   *   name: "001_create_users_table",
   *   sql: "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)"
   * });
   * ```
   */
  registerMigration(migration: Migration): void {
    if (!migration.name || migration.name.trim() === "") {
      throw new Error("Migration name cannot be empty");
    }

    if (this.migrations.some((m) => m.name === migration.name)) {
      throw new Error(`Migration with name '${migration.name}' already exists`);
    }

    this.migrations.push(migration);
  }

  /**
   * Get all registered migrations
   *
   * Returns a copy of all migrations that have been registered with this
   * registry. The migrations are returned in the order they were registered.
   *
   * @returns Array of registered migrations (defensive copy)
   */
  getMigrations(): Migration[] {
    return [...this.migrations];
  }

  /**
   * Clear all registered migrations
   *
   * Removes all migrations from the registry. This is primarily used for
   * testing purposes to ensure a clean state between test runs.
   *
   * @internal Used primarily for testing
   */
  clearMigrations(): void {
    this.migrations = [];
  }

  /**
   * Get the count of registered migrations
   *
   * @returns Number of migrations currently registered
   */
  getCount(): number {
    return this.migrations.length;
  }
}

// Create a singleton instance of the migration registry
const migrationRegistry = new MigrationRegistry();

/**
 * Register a migration with the migration service
 *
 * This is the primary public API for registering database migrations.
 * Each migration should represent a single, focused schema change that
 * can be applied atomically.
 *
 * @param migration - The migration to register
 * @throws {Error} If migration is invalid
 *
 * @example
 * ```typescript
 * registerMigration({
 *   name: "001_initial_schema",
 *   sql: `
 *     CREATE TABLE accounts (
 *       id INTEGER PRIMARY KEY,
 *       did TEXT UNIQUE NOT NULL,
 *       privateKeyHex TEXT NOT NULL,
 *       publicKeyHex TEXT NOT NULL,
 *       derivationPath TEXT,
 *       mnemonic TEXT
 *     );
 *   `
 * });
 * ```
 */
export function registerMigration(migration: Migration): void {
  migrationRegistry.registerMigration(migration);
}

/**
 * Validate that a migration was successfully applied by checking schema
 *
 * This function performs post-migration validation to ensure that the
 * expected database schema changes were actually applied. It checks for
 * the existence of tables, columns, and other schema elements that should
 * have been created by the migration.
 *
 * @param migration - The migration to validate
 * @param sqlQuery - Function to execute SQL queries
 * @returns Promise resolving to validation results
 *
 * @example
 * ```typescript
 * const validation = await validateMigrationApplication(migration, sqlQuery);
 * if (!validation.isValid) {
 *   console.error('Migration validation failed:', validation.errors);
 * }
 * ```
 */
/**
 * Helper function to check if a SQLite result indicates a table exists
 * @param result - The result from a sqlite_master query
 * @returns true if the table exists
 */
function checkSqliteTableResult(result: unknown): boolean {
  return (
    (result as unknown as { values: unknown[][] })?.values?.length > 0 ||
    (Array.isArray(result) && result.length > 0)
  );
}

/**
 * Helper function to validate that a table exists in the database
 * @param tableName - Name of the table to check
 * @param sqlQuery - Function to execute SQL queries
 * @returns Promise resolving to true if table exists
 */
async function validateTableExists<T>(
  tableName: string,
  sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<boolean> {
  try {
    const result = await sqlQuery(
      `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
    );
    return checkSqliteTableResult(result);
  } catch (error) {
    logger.error(`❌ [Validation] Error checking table ${tableName}:`, error);
    return false;
  }
}

/**
 * Helper function to validate that a column exists in a table
 * @param tableName - Name of the table
 * @param columnName - Name of the column to check
 * @param sqlQuery - Function to execute SQL queries
 * @returns Promise resolving to true if column exists
 */
async function validateColumnExists<T>(
  tableName: string,
  columnName: string,
  sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<boolean> {
  try {
    await sqlQuery(`SELECT ${columnName} FROM ${tableName} LIMIT 1`);
    return true;
  } catch (error) {
    logger.error(
      `❌ [Validation] Error checking column ${columnName} in ${tableName}:`,
      error,
    );
    return false;
  }
}

/**
 * Helper function to validate multiple tables exist
 * @param tableNames - Array of table names to check
 * @param sqlQuery - Function to execute SQL queries
 * @returns Promise resolving to array of validation results
 */
async function validateMultipleTables<T>(
  tableNames: string[],
  sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<{ exists: boolean; missing: string[] }> {
  const missing: string[] = [];

  for (const tableName of tableNames) {
    const exists = await validateTableExists(tableName, sqlQuery);
    if (!exists) {
      missing.push(tableName);
    }
  }

  return {
    exists: missing.length === 0,
    missing,
  };
}

/**
 * Helper function to add validation error with consistent logging
 * @param validation - The validation object to update
 * @param message - Error message to add
 * @param error - The error object for logging
 */
function addValidationError(
  validation: MigrationValidation,
  message: string,
  error: unknown,
): void {
  validation.isValid = false;
  validation.errors.push(message);
  logger.error(`❌ [Migration-Validation] ${message}:`, error);
}

async function validateMigrationApplication<T>(
  migration: Migration,
  sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<MigrationValidation> {
  const validation: MigrationValidation = {
    isValid: true,
    tableExists: false,
    hasExpectedColumns: false,
    errors: [],
  };

  try {
    if (migration.name === "001_initial") {
      // Validate core tables exist for initial migration
      const tables = [
        "accounts",
        "secret",
        "settings",
        "contacts",
        "logs",
        "temp",
      ];

      const tableValidation = await validateMultipleTables(tables, sqlQuery);
      if (!tableValidation.exists) {
        validation.isValid = false;
        validation.errors.push(
          `Missing tables: ${tableValidation.missing.join(", ")}`,
        );
        logger.error(
          `❌ [Migration-Validation] Missing tables:`,
          tableValidation.missing,
        );
      }
      validation.tableExists = tableValidation.exists;
    } else if (migration.name === "002_add_iViewContent_to_contacts") {
      // Validate iViewContent column exists in contacts table
      const columnExists = await validateColumnExists(
        "contacts",
        "iViewContent",
        sqlQuery,
      );
      if (!columnExists) {
        addValidationError(
          validation,
          "Column iViewContent missing from contacts table",
          new Error("Column not found"),
        );
      } else {
        validation.hasExpectedColumns = true;
      }
    } else if (migration.name === "004_active_identity_management") {
      // Validate active_identity table exists and has correct structure
      const activeIdentityExists = await validateTableExists(
        "active_identity",
        sqlQuery,
      );

      if (!activeIdentityExists) {
        addValidationError(
          validation,
          "Table active_identity missing",
          new Error("Table not found"),
        );
      } else {
        validation.tableExists = true;

        // Check that active_identity has the expected structure
        const hasExpectedColumns = await validateColumnExists(
          "active_identity",
          "id, activeDid, lastUpdated",
          sqlQuery,
        );

        if (!hasExpectedColumns) {
          addValidationError(
            validation,
            "active_identity table missing expected columns",
            new Error("Columns not found"),
          );
        } else {
          validation.hasExpectedColumns = true;
        }
      }

      // Check that hasBackedUpSeed column exists in settings table
      // Note: This validation is included here because migration 004 is consolidated
      // and includes the functionality from the original migration 003
      const hasBackedUpSeedExists = await validateColumnExists(
        "settings",
        "hasBackedUpSeed",
        sqlQuery,
      );

      if (!hasBackedUpSeedExists) {
        addValidationError(
          validation,
          "Column hasBackedUpSeed missing from settings table",
          new Error("Column not found"),
        );
      }
    }

    // Add validation for future migrations here
    // } else if (migration.name === "003_future_migration") {
    //   // Validate future migration schema changes
    // }
  } catch (error) {
    validation.isValid = false;
    validation.errors.push(`Validation error: ${error}`);
    logger.error(
      `❌ [Migration-Validation] Validation failed for ${migration.name}:`,
      error,
    );
  }

  return validation;
}

/**
 * Check if migration is already applied by examining actual schema
 *
 * This function performs schema introspection to determine if a migration
 * has already been applied, even if it's not recorded in the migrations
 * table. This is useful for handling cases where the database schema exists
 * but the migration tracking got out of sync.
 *
 * @param migration - The migration to check
 * @param sqlQuery - Function to execute SQL queries
 * @returns Promise resolving to true if schema already exists
 *
 * @example
 * ```typescript
 * const schemaExists = await isSchemaAlreadyPresent(migration, sqlQuery);
 * if (schemaExists) {
 *   console.log('Schema already exists, skipping migration');
 * }
 * ```
 */
async function isSchemaAlreadyPresent<T>(
  migration: Migration,
  sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<boolean> {
  try {
    if (migration.name === "001_initial") {
      // Check if accounts table exists (primary indicator of initial migration)
      const result = (await sqlQuery(
        `SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'`,
      )) as unknown as { values: unknown[][] };
      const hasTable =
        result?.values?.length > 0 ||
        (Array.isArray(result) && result.length > 0);
      // Reduced logging - only log on error
      return hasTable;
    } else if (migration.name === "002_add_iViewContent_to_contacts") {
      // Check if iViewContent column exists in contacts table
      try {
        await sqlQuery(`SELECT iViewContent FROM contacts LIMIT 1`);
        // Reduced logging - only log on error
        return true;
      } catch (error) {
        // Reduced logging - only log on error
        return false;
      }
    } else if (migration.name === "003_add_hasBackedUpSeed_to_settings") {
      // Check if hasBackedUpSeed column exists in settings table
      try {
        await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
        return true;
      } catch (error) {
        return false;
      }
    } else if (migration.name === "004_active_identity_management") {
      // Check if active_identity table exists and has correct structure
      try {
        // Check that active_identity table exists
        const activeIdentityResult = await sqlQuery(
          `SELECT name FROM sqlite_master WHERE type='table' AND name='active_identity'`,
        );
        const hasActiveIdentityTable =
          (activeIdentityResult as unknown as { values: unknown[][] })?.values
            ?.length > 0 ||
          (Array.isArray(activeIdentityResult) &&
            activeIdentityResult.length > 0);

        if (!hasActiveIdentityTable) {
          return false;
        }

        // Check that active_identity has the expected structure
        try {
          await sqlQuery(
            `SELECT id, activeDid, lastUpdated FROM active_identity LIMIT 1`,
          );

          // Also check that hasBackedUpSeed column exists in settings
          // This is included because migration 004 is consolidated
          try {
            await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
            return true;
          } catch (error) {
            return false;
          }
        } catch (error) {
          return false;
        }
      } catch (error) {
        logger.error(
          `🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`,
          error,
        );
        return false;
      }
    }

    // Add schema checks for future migrations here
    // } else if (migration.name === "003_future_migration") {
    //   // Check if future migration schema already exists
    // }
  } catch (error) {
    logger.error(
      `🔍 [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`,
      error,
    );
    return false;
  }

  return false;
}

/**
 * Run all registered migrations against the database
 *
 * This is the main function that executes the migration process. It:
 * 1. Creates the migrations tracking table if needed
 * 2. Determines which migrations have already been applied
 * 3. Applies any pending migrations in order
 * 4. Validates that migrations were applied correctly
 * 5. Records successful migrations in the tracking table
 * 6. Performs final validation of the migration state
 *
 * The function is designed to be idempotent - it can be run multiple times
 * safely without re-applying migrations that have already been completed.
 *
 * @template T - The type returned by SQL query operations
 * @param sqlExec - Function to execute SQL statements (INSERT, UPDATE, CREATE, etc.)
 * @param sqlQuery - Function to execute SQL queries (SELECT)
 * @param extractMigrationNames - Function to extract migration names from query results
 * @returns Promise that resolves when all migrations are complete
 * @throws {Error} If any migration fails to apply
 *
 * @example
 * ```typescript
 * // Platform-specific implementation
 * const sqlExec = async (sql: string, params?: unknown[]) => {
 *   return await db.run(sql, params);
 * };
 *
 * const sqlQuery = async (sql: string, params?: unknown[]) => {
 *   return await db.query(sql, params);
 * };
 *
 * const extractNames = (result: DBResult) => {
 *   return new Set(result.values.map(row => row[0]));
 * };
 *
 * await runMigrations(sqlExec, sqlQuery, extractNames);
 * ```
 */
export async function runMigrations<T>(
  sqlExec: (sql: string, params?: unknown[]) => Promise<void>,
  sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
  extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
  try {
    logger.debug("📋 [Migration] Starting migration process...");

    // Create migrations table if it doesn't exist
    // Note: We use IF NOT EXISTS here because this is infrastructure, not a business migration
    await sqlExec(`
      CREATE TABLE IF NOT EXISTS migrations (
        name TEXT PRIMARY KEY,
        applied_at TEXT DEFAULT CURRENT_TIMESTAMP
      );
    `);

    // Step 2: Get list of already applied migrations
    const appliedMigrationsResult = await sqlQuery(
      "SELECT name FROM migrations",
    );

    const appliedMigrations = extractMigrationNames(appliedMigrationsResult);

    // Step 3: Get all registered migrations
    const migrations = migrationRegistry.getMigrations();

    if (migrations.length === 0) {
      logger.warn("⚠️  [Migration] No migrations registered");
      return;
    }

    // Only log migration counts in development
    logger.debug(
      `📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`,
    );

    let appliedCount = 0;
    let skippedCount = 0;

    // Step 4: Process each migration
    for (const migration of migrations) {
      // Check 1: Is it recorded as applied in migrations table?
      const isRecordedAsApplied = appliedMigrations.has(migration.name);

      // Skip if already recorded as applied (name-only check)
      if (isRecordedAsApplied) {
        skippedCount++;
        continue;
      }

      // Check 2: Does the schema already exist in the database?
      const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);

      // Handle case where schema exists but isn't recorded
      if (isSchemaPresent) {
        try {
          await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
            migration.name,
          ]);
          logger.debug(
            `✅ [Migration] Marked existing schema as applied: ${migration.name}`,
          );
          skippedCount++;
          continue;
        } catch (insertError) {
          logger.warn(
            `⚠️  [Migration] Could not record existing schema ${migration.name}:`,
            insertError,
          );
          // Continue with normal migration process as fallback
        }
      }

      // Apply the migration
      logger.debug(`🔄 [Migration] Applying migration: ${migration.name}`);

      try {
        // Execute the migration SQL as single atomic operation
        logger.debug(`🔧 [Migration] Executing SQL for: ${migration.name}`);
        logger.debug(`🔧 [Migration] SQL content: ${migration.sql}`);

        // Execute the migration SQL directly - it should be atomic
        // The SQL itself should handle any necessary transactions
        const execResult = await sqlExec(migration.sql);

        logger.debug(
          `🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
        );

        // Validate the migration was applied correctly
        const validation = await validateMigrationApplication(
          migration,
          sqlQuery,
        );
        if (!validation.isValid) {
          logger.warn(
            `⚠️  [Migration] Validation failed for ${migration.name}:`,
            validation.errors,
          );
        }

        // Record that the migration was applied
        await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
          migration.name,
        ]);

        logger.debug(`🎉 [Migration] Successfully applied: ${migration.name}`);
        appliedCount++;
      } catch (error) {
        logger.error(`❌ [Migration] Error applying ${migration.name}:`, error);

        // Provide explicit rollback instructions for migration failures
        logger.error(
          `🔄 [Migration] ROLLBACK INSTRUCTIONS for ${migration.name}:`,
        );
        logger.error(`   1. Stop the application immediately`);
        logger.error(
          `   2. Restore database from pre-migration backup/snapshot`,
        );
        logger.error(
          `   3. Remove migration entry: DELETE FROM migrations WHERE name = '${migration.name}'`,
        );
        logger.error(
          `   4. Verify database state matches pre-migration condition`,
        );
        logger.error(`   5. Restart application and investigate root cause`);
        logger.error(
          `   FAILURE CAUSE: ${error instanceof Error ? error.message : String(error)}`,
        );
        logger.error(
          `   REQUIRED OPERATOR ACTION: Manual database restoration required`,
        );

        // Handle specific cases where the migration might be partially applied
        const errorMessage = String(error).toLowerCase();

        // Check if it's a duplicate table/column error - this means the schema already exists
        if (
          errorMessage.includes("duplicate column") ||
          errorMessage.includes("column already exists") ||
          errorMessage.includes("already exists") ||
          (errorMessage.includes("table") &&
            errorMessage.includes("already exists"))
        ) {
          logger.debug(
            `⚠️  [Migration] ${migration.name} appears already applied (${errorMessage}). Validating and marking as complete.`,
          );

          // Validate the existing schema
          const validation = await validateMigrationApplication(
            migration,
            sqlQuery,
          );
          if (!validation.isValid) {
            logger.warn(
              `⚠️  [Migration] Schema validation failed for ${migration.name}:`,
              validation.errors,
            );
            // Don't mark as applied if validation fails
            continue;
          }

          // Mark the migration as applied since the schema change already exists
          try {
            await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
              migration.name,
            ]);
            logger.debug(`✅ [Migration] Marked as applied: ${migration.name}`);
            appliedCount++;
          } catch (insertError) {
            // If we can't insert the migration record, log it but don't fail
            logger.warn(
              `⚠️  [Migration] Could not record ${migration.name} as applied:`,
              insertError,
            );
          }
        } else {
          // For other types of errors, still fail the migration
          logger.error(
            `❌ [Migration] Failed to apply ${migration.name}:`,
            error,
          );
          throw new Error(`Migration ${migration.name} failed: ${error}`);
        }
      }
    }

    // Step 6: Final validation - verify all migrations are properly recorded
    const finalMigrationsResult = await sqlQuery("SELECT name FROM migrations");
    const finalAppliedMigrations = extractMigrationNames(finalMigrationsResult);

    // Check that all expected migrations are recorded
    const expectedMigrations = new Set(migrations.map((m) => m.name));
    const missingMigrations = [...expectedMigrations].filter(
      (name) => !finalAppliedMigrations.has(name),
    );

    if (missingMigrations.length > 0) {
      logger.warn(
        `⚠️  [Migration] Missing migration records: ${missingMigrations.join(", ")}`,
      );
    }

    // Only show completion message in development
    logger.log(
      `🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`,
    );
  } catch (error) {
    logger.error("\n💥 [Migration] Migration process failed:", error);
    logger.error("[MigrationService] Migration process failed:", error);
    throw error;
  }
}
