import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
import { DataSource, QueryRunner } from 'typeorm';
import { uuidV7 } from '../common/uuid-v7';

@Injectable()
export class DatabaseMigrationsService implements OnApplicationBootstrap {
  private readonly logger = new Logger(DatabaseMigrationsService.name);

  constructor(private readonly dataSource: DataSource) {}

  async onApplicationBootstrap(): Promise<void> {
    await this.ensureAiConfigTable();
    await this.ensureSyncStateLeaseColumns();
    await this.ensureStudentProfileCollegeColumns();
    await this.ensureCourseAttendanceSchema();
    await this.ensureAttendanceDevicesSchema();
    await this.ensureStudentWalletSchema();
    await this.backfillOrganizations();
    await this.normalizeClassDates();
    await this.protectAttendanceHistory();
    await this.removeUnusedClassroomColumns();
    await this.removeUnusedRoomColumns();
    await this.cleanupDepositRefundColumns();
    await this.removeUnusedClassStudentColumns();
    await this.normalizeClassroomStatuses();
  }

  private async ensureSyncStateLeaseColumns(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const table = await runner.getTable('sync_state');
      if (!table) return;
      const columns = new Set(table.columns.map((column) => column.name));
      if (!columns.has('run_id')) {
        await runner.query('ALTER TABLE sync_state ADD COLUMN run_id VARCHAR(64)');
      }
      if (!columns.has('running_since')) {
        await runner.query('ALTER TABLE sync_state ADD COLUMN running_since DATETIME');
      }
    } finally {
      await runner.release();
    }
  }

  private async ensureStudentProfileCollegeColumns(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const table = await runner.getTable('student_profiles');
      if (!table) return;
      const columns = new Set(table.columns.map((column) => column.name));
      const additions: Array<[string, string]> = [
        ['college_school', 'VARCHAR(100)'],
        ['college_major', 'VARCHAR(100)'],
      ];
      for (const [name, definition] of additions) {
        if (!columns.has(name)) await runner.query(`ALTER TABLE student_profiles ADD COLUMN ${name} ${definition}`);
      }
    } finally {
      await runner.release();
    }
  }

  private async ensureAttendanceDevicesSchema(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const isMySQL = this.dataSource.options.type === 'mysql';
      const pk = isMySQL ? 'INTEGER PRIMARY KEY AUTO_INCREMENT' : 'INTEGER PRIMARY KEY AUTOINCREMENT';
      await runner.query(`CREATE TABLE IF NOT EXISTS attendance_devices (
        id ${pk},
        device_sn VARCHAR(100) NOT NULL,
        device_name VARCHAR(100) NOT NULL,
        classroom_id INTEGER NOT NULL,
        status VARCHAR(20) NOT NULL DEFAULT 'active',
        location VARCHAR(200),
        notes TEXT,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
      )`);
      const table = await runner.getTable('attendance_devices');
      const columnNames = new Set(table?.columns.map((column) => column.name) ?? []);
      const additions: Array<[string, string]> = [
        ['device_sn', 'VARCHAR(100) NOT NULL DEFAULT \'\''],
        ['device_name', 'VARCHAR(100) NOT NULL DEFAULT \'\''],
        ['classroom_id', 'INTEGER NOT NULL DEFAULT 0'],
        ['status', "VARCHAR(20) NOT NULL DEFAULT 'active'"],
        ['location', 'VARCHAR(200)'],
        ['notes', 'TEXT'],
        ['created_at', 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'],
        ['updated_at', 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP'],
      ];
      for (const [name, definition] of additions) {
        if (!columnNames.has(name)) await runner.query(`ALTER TABLE attendance_devices ADD COLUMN ${name} ${definition}`);
      }
      const refreshed = await runner.getTable('attendance_devices');
      const createIndex = async (sql: string) => {
        try {
          await runner.query(sql);
        } catch {
          // Existing MySQL indexes cannot use IF NOT EXISTS; startup must stay idempotent.
        }
      };
      const uniqueSn = refreshed?.indices.some((index) => index.columnNames.length === 1 && index.columnNames[0] === 'device_sn' && index.isUnique);
      if (!uniqueSn) {
        await createIndex(
          isMySQL
            ? 'CREATE UNIQUE INDEX idx_attendance_devices_device_sn ON attendance_devices (device_sn)'
            : 'CREATE UNIQUE INDEX IF NOT EXISTS idx_attendance_devices_device_sn ON attendance_devices (device_sn)',
        );
      }
      await createIndex(
        isMySQL
          ? 'CREATE INDEX idx_attendance_devices_classroom_id ON attendance_devices (classroom_id)'
          : 'CREATE INDEX IF NOT EXISTS idx_attendance_devices_classroom_id ON attendance_devices (classroom_id)',
      );
    } finally {
      await runner.release();
    }
  }

  private async ensureStudentWalletSchema(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const isMySQL = this.dataSource.options.type === 'mysql';
      const pk = isMySQL ? 'INTEGER PRIMARY KEY AUTO_INCREMENT' : 'INTEGER PRIMARY KEY AUTOINCREMENT';
      await runner.query(`CREATE TABLE IF NOT EXISTS student_wallets (
        id ${pk}, student_id INTEGER NOT NULL UNIQUE, balance DECIMAL(12,2) NOT NULL DEFAULT 0,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
      )`);
      await runner.query(`CREATE TABLE IF NOT EXISTS wallet_transactions (
        id ${pk}, student_id INTEGER NOT NULL, bill_id INTEGER, type VARCHAR(30) NOT NULL,
        amount DECIMAL(12,2) NOT NULL, balance_after DECIMAL(12,2) NOT NULL,
        description VARCHAR(300), recorded_by INTEGER,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
      )`);
      await runner.query(`CREATE TABLE IF NOT EXISTS financial_operations (
        id ${pk}, operation_id VARCHAR(64) NOT NULL UNIQUE, type VARCHAR(64) NOT NULL,
        status VARCHAR(20) NOT NULL DEFAULT 'running', result_json TEXT, error_message VARCHAR(500),
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
      )`);
      const walletTransactions = await runner.getTable('wallet_transactions');
      if (walletTransactions) {
        const columns = new Set(walletTransactions.columns.map((column) => column.name));
        if (!columns.has('operation_id')) {
          await runner.query('ALTER TABLE wallet_transactions ADD COLUMN operation_id VARCHAR(64)');
        }
      }
      const billItems = await runner.getTable('bill_items');
      if (billItems) {
        const columns = new Set(billItems.columns.map((column) => column.name));
        for (const [name, definition] of [
          ['room_expense_id', 'INTEGER'],
          ['personal_expense_id', 'INTEGER'],
        ]) {
          if (!columns.has(name)) await runner.query(`ALTER TABLE bill_items ADD COLUMN ${name} ${definition}`);
        }
      }
      const roomExpenses = await runner.getTable('room_expenses');
      if (roomExpenses) {
        const columns = new Set(roomExpenses.columns.map((column) => column.name));
        if (!columns.has('import_key')) {
          await runner.query('ALTER TABLE room_expenses ADD COLUMN import_key VARCHAR(120)');
        }
        const refreshedRoomExpenses = await runner.getTable('room_expenses');
        const hasImportKey = refreshedRoomExpenses?.indices.some((index) =>
          index.isUnique && index.columnNames.length === 1 && index.columnNames[0] === 'import_key');
        if (!hasImportKey) {
          await runner.query(isMySQL
            ? 'CREATE UNIQUE INDEX idx_room_expenses_import_key ON room_expenses (import_key)'
            : 'CREATE UNIQUE INDEX IF NOT EXISTS idx_room_expenses_import_key ON room_expenses (import_key)');
        }
      }
      const bills = await runner.getTable('bills');
      if (bills) {
        const columns = new Set(bills.columns.map((column) => column.name));
        const additions = [
          ['source', "VARCHAR(30) NOT NULL DEFAULT 'batch'"],
          ['paid_amount', 'DECIMAL(10,2) NOT NULL DEFAULT 0'],
          ['outstanding_amount', 'DECIMAL(10,2) NOT NULL DEFAULT 0'],
          ['cancelled_at', 'DATETIME'],
          ['cancel_reason', 'VARCHAR(300)'],
        ];
        for (const [name, definition] of additions) {
          if (!columns.has(name)) await runner.query(`ALTER TABLE bills ADD COLUMN ${name} ${definition}`);
        }
        await runner.query("UPDATE bills SET outstanding_amount = total_amount WHERE outstanding_amount = 0 AND status <> 'paid'");
        await runner.query("UPDATE bills SET paid_amount = total_amount, outstanding_amount = 0 WHERE status = 'paid'");
        await runner.query("UPDATE bills SET status = 'unpaid' WHERE status IN ('draft', 'confirmed')");
      }
      const personalExpenses = await runner.getTable('personal_expenses');
      if (personalExpenses && !personalExpenses.columns.some((column) => column.name === 'bill_id')) {
        await runner.query('ALTER TABLE personal_expenses ADD COLUMN bill_id INTEGER');
      }
    } finally {
      await runner.release();
    }
  }

  private async removeUnusedClassroomColumns(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const tables = await runner.getTables(['classrooms']);
      if (tables.length === 0) return;

      const table = await runner.getTable('classrooms');
      const columnNames = new Set(table?.columns.map((column) => column.name) ?? []);
      for (const columnName of ['course_type', 'supervisor']) {
        if (columnNames.has(columnName)) {
          await runner.query(`ALTER TABLE classrooms DROP COLUMN ${columnName}`);
        }
      }
    } finally {
      await runner.release();
    }
  }

  private async removeUnusedRoomColumns(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const tables = await runner.getTables(['rooms']);
      if (tables.length === 0) return;

      const table = await runner.getTable('rooms');
      if (table?.columns.some((column) => column.name === 'gender')) {
        await runner.dropColumn('rooms', 'gender');
      }
    } finally {
      await runner.release();
    }
  }

  private async cleanupDepositRefundColumns(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const tables = await runner.getTables(['deposits']);
      if (tables.length === 0) return;

      const table = await runner.getTable('deposits');
      const columnNames = new Set(table?.columns.map((column) => column.name) ?? []);
      for (const [legacyName, currentName] of [
        ['refund_approved_by', 'refunded_by'],
        ['refund_approved_at', 'refunded_at'],
      ] as const) {
        if (!columnNames.has(legacyName)) continue;

        if (columnNames.has(currentName)) {
          await runner.query(
            `UPDATE deposits SET ${currentName} = COALESCE(${currentName}, ${legacyName})`,
          );
          await runner.dropColumn('deposits', legacyName);
        } else {
          await runner.renameColumn('deposits', legacyName, currentName);
          columnNames.add(currentName);
        }
        columnNames.delete(legacyName);
      }

      for (const columnName of ['refund_status', 'refund_requested_at', 'refund_rejected_reason']) {
        if (columnNames.has(columnName)) {
          await runner.dropColumn('deposits', columnName);
          columnNames.delete(columnName);
        }
      }
    } finally {
      await runner.release();
    }
  }

  private async removeUnusedClassStudentColumns(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const tables = await runner.getTables(['class_student']);
      if (tables.length === 0) return;

      const table = await runner.getTable('class_student');
      if (table?.columns.some((column) => column.name === 'enrollment_id')) {
        await runner.dropColumn('class_student', 'enrollment_id');
      }
    } finally {
      await runner.release();
    }
  }

  private async normalizeClassroomStatuses(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const tables = await runner.getTables(['classrooms']);
      if (tables.length === 0) return;
      await runner.query(`
        UPDATE classrooms
        SET status = 'available'
        WHERE status IS NULL OR status NOT IN ('available', 'maintenance', 'archived')
      `);
    } finally {
      await runner.release();
    }
  }

  private async ensureAiConfigTable(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const tables = await runner.getTables(['ai_config']);
      const isMySQL = this.dataSource.options.type === 'mysql';

      if (tables.length === 0) {
        const pkDef = isMySQL
          ? 'id INTEGER PRIMARY KEY AUTO_INCREMENT'
          : 'id INTEGER PRIMARY KEY AUTOINCREMENT';
        const boolType = isMySQL ? 'TINYINT(1)' : 'BOOLEAN';
        const datetimeFn = isMySQL ? 'CURRENT_TIMESTAMP' : 'CURRENT_TIMESTAMP';

        await runner.query(`
          CREATE TABLE ai_config (
            ${pkDef},
            singleton_key VARCHAR(20) NOT NULL DEFAULT 'GLOBAL',
            provider VARCHAR(50) NOT NULL DEFAULT 'OPENAI',
            base_url VARCHAR(500),
            encrypted_api_key TEXT,
            api_key_iv VARCHAR(50),
            api_key_auth_tag VARCHAR(50),
            key_last4 VARCHAR(4),
            default_model VARCHAR(100),
            enabled ${boolType} DEFAULT 0,
            timeout_ms INT DEFAULT 30000,
            verified ${boolType} DEFAULT 0,
            last_tested_at DATETIME,
            last_test_latency_ms INT,
            created_at DATETIME NOT NULL DEFAULT ${datetimeFn},
            updated_at DATETIME NOT NULL DEFAULT ${datetimeFn}
          )
        `);

        if (isMySQL) {
          try {
            await runner.query(
              'CREATE UNIQUE INDEX uq_ai_config_singleton ON ai_config(singleton_key)',
            );
          } catch {
            // Index may already exist; MySQL has no IF NOT EXISTS for indexes
          }
        } else {
          await runner.query(
            'CREATE UNIQUE INDEX IF NOT EXISTS uq_ai_config_singleton ON ai_config(singleton_key)',
          );
        }

        this.logger.log('已创建 ai_config 表');
      } else {
        // Check for missing columns
        const table = await runner.getTable('ai_config');
        const columnNames = new Set(table?.columns.map((c) => c.name) ?? []);

        const desiredColumns: Array<{ name: string; def: string }> = [
          { name: 'id', def: '' }, // skip — primary key
          { name: 'singleton_key', def: "VARCHAR(20) NOT NULL DEFAULT 'GLOBAL'" },
          { name: 'provider', def: "VARCHAR(50) NOT NULL DEFAULT 'OPENAI'" },
          { name: 'base_url', def: 'VARCHAR(500)' },
          { name: 'encrypted_api_key', def: 'TEXT' },
          { name: 'api_key_iv', def: 'VARCHAR(50)' },
          { name: 'api_key_auth_tag', def: 'VARCHAR(50)' },
          { name: 'key_last4', def: 'VARCHAR(4)' },
          { name: 'default_model', def: 'VARCHAR(100)' },
          { name: 'enabled', def: isMySQL ? 'TINYINT(1) DEFAULT 0' : 'BOOLEAN DEFAULT 0' },
          { name: 'timeout_ms', def: 'INT DEFAULT 30000' },
          { name: 'verified', def: isMySQL ? 'TINYINT(1) DEFAULT 0' : 'BOOLEAN DEFAULT 0' },
          { name: 'last_tested_at', def: 'DATETIME' },
          { name: 'last_test_latency_ms', def: 'INT' },
          { name: 'created_at', def: 'DATETIME' },
          { name: 'updated_at', def: 'DATETIME' },
        ];

        for (const col of desiredColumns) {
          if (col.def && !columnNames.has(col.name)) {
            await runner.query(`ALTER TABLE ai_config ADD COLUMN ${col.name} ${col.def}`);
            this.logger.log(`已为 ai_config 表添加列: ${col.name}`);
          }
        }
      }
    } finally {
      await runner.release();
    }
  }

  private async ensureCourseAttendanceSchema(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const tables = await runner.getTables([
        'class_schedule',
        'attendance_records',
        'attendance_sessions',
      ]);
      const tableNames = new Set(tables.map((table) => table.name));
      const isMySQL = this.dataSource.options.type === 'mysql';

      if (!tableNames.has('attendance_sessions')) {
        const pkDef = isMySQL
          ? 'id INTEGER PRIMARY KEY AUTO_INCREMENT'
          : 'id INTEGER PRIMARY KEY AUTOINCREMENT';
        await runner.query(`
          CREATE TABLE attendance_sessions (
            ${pkDef},
            schedule_id INTEGER NOT NULL,
            class_id INTEGER NOT NULL,
            lesson_date DATE NOT NULL,
            status VARCHAR(20) NOT NULL DEFAULT 'in_progress',
            started_by INTEGER,
            started_at DATETIME,
            completed_by INTEGER,
            completed_at DATETIME,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (schedule_id) REFERENCES class_schedule(id) ON DELETE RESTRICT,
            FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE RESTRICT
          )
        `);
      }

      if (tableNames.has('class_schedule')) {
        const scheduleTable = await runner.getTable('class_schedule');
        const scheduleColumns = new Set(scheduleTable?.columns.map((column) => column.name) ?? []);
        if (!scheduleColumns.has('attendance_advance_minutes')) {
          await runner.query(
            'ALTER TABLE class_schedule ADD COLUMN attendance_advance_minutes INTEGER NOT NULL DEFAULT 30',
          );
          this.logger.log('已为排课添加课前签到分钟配置');
        }
      }

      const attendanceTable = await runner.getTable('attendance_records');
      const columnNames = new Set(attendanceTable?.columns.map((column) => column.name) ?? []);
      if (!columnNames.has('schedule_id')) {
        await runner.query('ALTER TABLE attendance_records ADD COLUMN schedule_id INTEGER');
      }
      if (!columnNames.has('attendance_session_id')) {
        await runner.query(
          'ALTER TABLE attendance_records ADD COLUMN attendance_session_id INTEGER',
        );
      }

      const createIndex = async (sql: string) => {
        try {
          await runner.query(sql);
        } catch {
          // Existing MySQL indexes cannot use IF NOT EXISTS; startup must stay idempotent.
        }
      };
      await createIndex(
        isMySQL
          ? 'CREATE UNIQUE INDEX uq_attendance_session_schedule_date ON attendance_sessions(schedule_id, lesson_date)'
          : 'CREATE UNIQUE INDEX IF NOT EXISTS uq_attendance_session_schedule_date ON attendance_sessions(schedule_id, lesson_date)',
      );
      await createIndex(
        isMySQL
          ? 'CREATE UNIQUE INDEX uq_attendance_session_student ON attendance_records(attendance_session_id, student_id)'
          : 'CREATE UNIQUE INDEX IF NOT EXISTS uq_attendance_session_student ON attendance_records(attendance_session_id, student_id)',
      );
    } finally {
      await runner.release();
    }
  }

  private async backfillOrganizations(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const tables = await runner.getTables([
        'tenants',
        'organizations',
        'students',
        'occupancies',
        'classroom_rentals',
      ]);
      const tableNames = new Set(tables.map((table) => table.name));
      if (!tableNames.has('organizations')) return;

      const organizationRows = () =>
        runner.query('SELECT * FROM organizations WHERE is_host = 1 LIMIT 1');
      let host = (await organizationRows())[0];
      if (!host) {
        await runner.query(
          `INSERT INTO organizations (public_id, code, name, is_host, color, notes, status, created_at, updated_at)
           VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
          [
            uuidV7(),
            'HOST',
            process.env.HOST_ORGANIZATION_NAME || '本机构',
            1,
            '#1677ff',
            '系统默认运营主体',
            'active',
          ],
        );
        host = (await organizationRows())[0];
      }
      if (!host) return;

      if (tableNames.has('tenants')) {
        const legacyTenants: Array<Record<string, unknown>> =
          await runner.query('SELECT * FROM tenants');
        for (const legacy of legacyTenants) {
          const name = String(legacy.name || '').trim();
          if (!name) continue;
          let external = (
            await runner.query('SELECT * FROM organizations WHERE name = ? LIMIT 1', [name])
          )[0];
          if (!external) {
            await runner.query(
              `INSERT INTO organizations (public_id, code, name, is_host, contact_name, phone, color, notes, status, created_at, updated_at)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
              [
                uuidV7(),
                `ORG_${legacy.id}`,
                name,
                0,
                legacy.contact || null,
                legacy.phone || null,
                legacy.color || null,
                legacy.notes || null,
                legacy.status || 'active',
              ],
            );
            external = (
              await runner.query('SELECT * FROM organizations WHERE name = ? LIMIT 1', [name])
            )[0];
          }
          if (!external) continue;
          if (tableNames.has('students')) {
            await runner
              .query(
                'UPDATE students SET organization_id = ? WHERE organization_id IS NULL AND tenant_id = ?',
                [external.id, legacy.id],
              )
              .catch(() => undefined);
          }
          if (tableNames.has('occupancies')) {
            await runner
              .query(
                'UPDATE occupancies SET responsible_organization_id = ? WHERE responsible_organization_id IS NULL AND tenant_id = ?',
                [external.id, legacy.id],
              )
              .catch(() => undefined);
          }
          if (tableNames.has('classroom_rentals')) {
            await runner
              .query(
                'UPDATE classroom_rentals SET lessee_organization_id = ?, lessor_organization_id = ? WHERE lessee_organization_id IS NULL AND tenant_id = ?',
                [external.id, host.id, legacy.id],
              )
              .catch(() => undefined);
          }
        }
      }

      if (tableNames.has('students')) {
        await runner.query(
          'UPDATE students SET organization_id = ? WHERE organization_id IS NULL',
          [host.id],
        );
      }
      if (tableNames.has('occupancies')) {
        await runner.query(
          `UPDATE occupancies
           SET responsible_organization_id = COALESCE(
             (SELECT organization_id FROM students WHERE students.id = occupancies.student_id), ?
           )
           WHERE responsible_organization_id IS NULL`,
          [host.id],
        );
      }
      if (tableNames.has('classroom_rentals')) {
        await runner.query(
          'UPDATE classroom_rentals SET lessor_organization_id = ? WHERE lessor_organization_id IS NULL',
          [host.id],
        );
      }
    } finally {
      await runner.release();
    }
  }

  private async normalizeClassDates(): Promise<void> {
    const driver = this.dataSource.options.type;
    let columns: Array<'start_date' | 'end_date'> = ['start_date', 'end_date'];

    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const table = await runner.getTable('classes');
      if (!table) return;

      // Fresh MySQL schemas created by TypeORM already use native DATE columns.
      // This cleanup is only for legacy schemas that stored dates as strings;
      // comparing a native DATE column with '' raises ER_TRUNCATED_WRONG_VALUE
      // in strict SQL mode.
      if (driver === 'mysql') {
        columns = columns.filter((columnName) => {
          const column = table.columns.find((item) => item.name === columnName);
          const type = String(column?.type ?? '').toLowerCase();
          return !['date', 'datetime', 'timestamp'].includes(type);
        });
        if (columns.length === 0) return;
      }
    } finally {
      await runner.release();
    }

    const columnText = (column: string) =>
      driver === 'mysql' ? `CAST(${column} AS CHAR)` : column;
    const firstTenChars = (column: string) =>
      driver === 'mysql'
        ? `NULLIF(LEFT(${columnText(column)}, 10), '')`
        : `NULLIF(substr(${column}, 1, 10), '')`;
    const normalizedDate = (column: string) => `CASE
            WHEN ${column} IS NULL THEN NULL
            ELSE ${firstTenChars(column)}
          END`;
    const lengthFunction = driver === 'mysql' ? 'CHAR_LENGTH' : 'length';
    const needsNormalization = (column: string) => `(
            ${column} IS NOT NULL
            AND (${columnText(column)} = '' OR ${lengthFunction}(${columnText(column)}) > 10)
          )`;

    const assignments = columns
      .map((column) => `${column} = ${normalizedDate(column)}`)
      .join(',\n          ');
    const predicates = columns.map((column) => needsNormalization(column)).join('\n          OR ');
    const result = await this.dataSource.transaction((manager) =>
      manager.query(`
        UPDATE classes
        SET
          ${assignments}
        WHERE
          ${predicates}
      `),
    );

    const affected = typeof result?.changes === 'number' ? result.changes : result?.affectedRows;
    if (affected) this.logger.log(`已规范化 ${affected} 条班级日期数据`);
  }
  private async protectAttendanceHistory(): Promise<void> {
    const runner = this.dataSource.createQueryRunner();
    await runner.connect();
    try {
      const tables = await runner.getTables(['attendance_sessions']);
      if (tables.length === 0) return;

      const isMySQL = this.dataSource.options.type === 'mysql';
      if (isMySQL) {
        await this.migrateMySQLAttendanceFKs(runner);
      } else {
        await this.migrateSQLiteAttendanceFKs(runner);
      }
    } finally {
      await runner.release();
    }
  }

  private async migrateMySQLAttendanceFKs(runner: QueryRunner): Promise<void> {
    // Drop any existing FK constraint on schedule_id or class_id
    const fkColumns = ['schedule_id', 'class_id'];
    for (const col of fkColumns) {
      const fkRows: { CONSTRAINT_NAME: string }[] = await runner.query(
        `
        SELECT CONSTRAINT_NAME
        FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = 'attendance_sessions'
          AND COLUMN_NAME = ?
          AND REFERENCED_TABLE_NAME IS NOT NULL
      `,
        [col],
      );

      for (const row of fkRows) {
        try {
          await runner.query(
            `ALTER TABLE attendance_sessions DROP FOREIGN KEY \`${row.CONSTRAINT_NAME}\``,
          );
          this.logger.log(`已移除考勤场次 FK 约束: ${row.CONSTRAINT_NAME}`);
        } catch {
          // constraint may have already been dropped
        }
      }
    }

    const constraints: Array<{ name: string; col: string; ref: string }> = [
      { name: 'fk_as_schedule_protect', col: 'schedule_id', ref: 'class_schedule(id)' },
      { name: 'fk_as_class_protect', col: 'class_id', ref: 'classes(id)' },
    ];
    for (const c of constraints) {
      // Only skip if RESTRICT constraint is already confirmed via information_schema
      const existing: Array<{ DELETE_RULE: string }> = await runner.query(
        `
        SELECT DELETE_RULE
        FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
        WHERE CONSTRAINT_SCHEMA = DATABASE()
          AND TABLE_NAME = 'attendance_sessions'
          AND CONSTRAINT_NAME = ?
      `,
        [c.name],
      );

      if (existing.length > 0 && existing[0].DELETE_RULE === 'RESTRICT') {
        this.logger.log(`考勤场次删除保护约束已存在: ${c.name}`);
        continue;
      }

      // ADD RESTRICT must throw on failure — no catch
      await runner.query(`
        ALTER TABLE attendance_sessions
        ADD CONSTRAINT ${c.name}
        FOREIGN KEY (${c.col}) REFERENCES ${c.ref}
        ON DELETE RESTRICT
      `);
      this.logger.log(`已添加考勤场次删除保护约束: ${c.name}`);
    }
  }

  private async migrateSQLiteAttendanceFKs(runner: QueryRunner): Promise<void> {
    // SQLite cannot ALTER TABLE to add foreign keys.
    // Rebuild the table inside a transaction: create a new table with FK constraints,
    // copy all rows, drop old, rename new, then recreate indexes.
    const fkRows: Array<{ id: number }> = await runner.query(
      "PRAGMA foreign_key_list('attendance_sessions')",
    );
    if (fkRows.length > 0) return; // FKs already present

    this.logger.log('正在重建 attendance_sessions 表以添加外键保护…');

    // PRAGMA foreign_keys=OFF must be issued outside the transaction
    await runner.query('PRAGMA foreign_keys = OFF');
    try {
      await runner.query('BEGIN');
      try {
        await runner.query(`
          CREATE TABLE attendance_sessions_new (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            schedule_id INTEGER NOT NULL,
            class_id INTEGER NOT NULL,
            lesson_date DATE NOT NULL,
            status VARCHAR(20) NOT NULL DEFAULT 'in_progress',
            started_by INTEGER,
            started_at DATETIME,
            completed_by INTEGER,
            completed_at DATETIME,
            created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (schedule_id) REFERENCES class_schedule(id) ON DELETE RESTRICT,
            FOREIGN KEY (class_id) REFERENCES classes(id) ON DELETE RESTRICT
          )
        `);
        await runner.query(`
          INSERT INTO attendance_sessions_new (
            id, schedule_id, class_id, lesson_date, status,
            started_by, started_at, completed_by, completed_at, created_at, updated_at
          )
          SELECT
            id, schedule_id, class_id, lesson_date, status,
            started_by, started_at, completed_by, completed_at, created_at, updated_at
          FROM attendance_sessions
        `);
        await runner.query('DROP TABLE attendance_sessions');
        await runner.query('ALTER TABLE attendance_sessions_new RENAME TO attendance_sessions');
        await runner.query(
          'CREATE UNIQUE INDEX IF NOT EXISTS uq_attendance_session_schedule_date ON attendance_sessions(schedule_id, lesson_date)',
        );

        // Rebuild attendance_records to add/protect FK on attendance_session_id
        const recordsFk = await runner.query("PRAGMA foreign_key_list('attendance_records')");
        const hasSessionFk = recordsFk.some(
          (r: { from: string }) => r.from === 'attendance_session_id',
        );
        if (!hasSessionFk) {
          await runner.query(`
            CREATE TABLE attendance_records_new (
              id INTEGER PRIMARY KEY AUTOINCREMENT,
              student_id INTEGER NOT NULL,
              class_id INTEGER,
              schedule_id INTEGER,
              attendance_session_id INTEGER,
              attendance_date DATE NOT NULL,
              session VARCHAR(20) NOT NULL,
              status VARCHAR(20) NOT NULL,
              remark VARCHAR(200),
              source VARCHAR(20) DEFAULT 'manual',
              created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
              updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
              FOREIGN KEY (attendance_session_id) REFERENCES attendance_sessions(id) ON DELETE SET NULL
            )
          `);
          await runner.query(`
            INSERT INTO attendance_records_new (
              id, student_id, class_id, schedule_id, attendance_session_id,
              attendance_date, session, status, remark, source, created_at, updated_at
            )
            SELECT
              id, student_id, class_id, schedule_id, attendance_session_id,
              attendance_date, session, status, remark, source, created_at, updated_at
            FROM attendance_records
          `);
          await runner.query('DROP TABLE attendance_records');
          await runner.query('ALTER TABLE attendance_records_new RENAME TO attendance_records');
          await runner.query(
            'CREATE UNIQUE INDEX IF NOT EXISTS uq_attendance_session_student ON attendance_records(attendance_session_id, student_id)',
          );
        }

        // Verify foreign key integrity BEFORE committing the transaction.
        // If violations exist, the transaction rolls back and old tables are preserved.
        const checkRows = await runner.query('PRAGMA foreign_key_check');
        if (checkRows.length > 0) {
          throw new Error(`外键一致性检查失败: ${checkRows.length} 行违反外键约束`);
        }

        await runner.query('COMMIT');
        this.logger.log('attendance_sessions 表外键保护重建完成');
      } catch (err) {
        await runner.query('ROLLBACK');
        throw err;
      }
    } finally {
      await runner.query('PRAGMA foreign_keys = ON');
    }
  }
}
