import fs from 'fs';
import path from 'path';

import config from '@blocklet/sdk/lib/config';
import { Sequelize, DataTypes } from 'sequelize';

import logger from '../logger';

export function getArchiveDir(): string {
  const dataDir = config.env?.dataDir || '/tmp';
  const archiveDir = path.join(dataDir, 'archive');
  if (!fs.existsSync(archiveDir)) {
    fs.mkdirSync(archiveDir, { recursive: true });
  }
  return archiveDir;
}

export function getArchiveFilePath(fileName: string): string {
  return path.join(getArchiveDir(), fileName);
}

/**
 * Get archive file path for a specific year based on record's created_at.
 * Archive files are organized by data year (e.g., archive-2024.db contains 2024 data).
 */
export function getArchiveFilePathForYear(year: number): string {
  return path.join(getArchiveDir(), `archive-${year}.db`);
}

/**
 * Extract year from a record's created_at (fallback to updated_at, then current year).
 */
export function getRecordYear(record: any): number {
  const createdAt = record?.created_at;
  const updatedAt = record?.updated_at;
  const dateValue = createdAt || updatedAt;

  if (dateValue) {
    const date = dateValue instanceof Date ? dateValue : new Date(dateValue);
    if (!Number.isNaN(date.getTime())) {
      return date.getFullYear();
    }
  }

  return new Date().getFullYear();
}

export function openArchiveSequelize(filePath: string): Sequelize {
  return new Sequelize({
    dialect: 'sqlite',
    storage: filePath,
    logging: false,
  });
}

function normalizeCreateTableSql(sql: string): string {
  return sql.replace(/^CREATE TABLE\s+/i, 'CREATE TABLE IF NOT EXISTS ');
}

function normalizeCreateIndexSql(sql: string): string {
  return sql
    .replace(/^CREATE INDEX\s+/i, 'CREATE INDEX IF NOT EXISTS ')
    .replace(/^CREATE UNIQUE INDEX\s+/i, 'CREATE UNIQUE INDEX IF NOT EXISTS ');
}

type ColumnInfo = { name: string; type: string; notnull: number; dflt_value: any; pk: number };

export async function ensureArchiveTable(
  tableName: string,
  mainSequelize: Sequelize,
  archiveSequelize: Sequelize
): Promise<void> {
  // 1. Copy table schema (CREATE TABLE IF NOT EXISTS)
  const [tableRows] = await mainSequelize.query('SELECT sql FROM sqlite_master WHERE type = :type AND name = :name', {
    replacements: { type: 'table', name: tableName },
  });
  const tableRow = (Array.isArray(tableRows) ? tableRows[0] : null) as { sql?: string } | null;
  const createSql = tableRow?.sql;
  if (!createSql) {
    throw new Error(`table schema not found: ${tableName}`);
  }

  const normalizedSql = normalizeCreateTableSql(createSql);
  await archiveSequelize.query(normalizedSql);

  // 2. Copy indexes (PRIMARY KEY auto-indexes have sql=NULL, skip them)
  const [indexRows] = await mainSequelize.query(
    'SELECT sql FROM sqlite_master WHERE type = :type AND tbl_name = :tableName AND sql IS NOT NULL',
    { replacements: { type: 'index', tableName } }
  );
  for (const indexRow of indexRows as { sql?: string }[]) {
    if (indexRow?.sql) {
      try {
        const indexSql = normalizeCreateIndexSql(indexRow.sql);
        // eslint-disable-next-line no-await-in-loop
        await archiveSequelize.query(indexSql);
      } catch (err: any) {
        // Ignore "index already exists" errors
        if (!err.message?.includes('already exists')) {
          logger.warn('failed to create archive index', { tableName, error: err.message });
        }
      }
    }
  }

  // 3. Sync missing columns from main DB to archive DB
  // This handles schema migrations: if main DB has new columns, add them to archive DB
  const [mainColumns] = await mainSequelize.query(`PRAGMA table_info("${tableName}")`);
  const [archiveColumns] = await archiveSequelize.query(`PRAGMA table_info("${tableName}")`);

  const mainColumnNames = new Set((mainColumns as ColumnInfo[]).map((c) => c.name));
  const archiveColumnNames = new Set((archiveColumns as ColumnInfo[]).map((c) => c.name));

  // Find columns in main DB but not in archive DB
  const missingColumns = (mainColumns as ColumnInfo[]).filter((c) => !archiveColumnNames.has(c.name));

  for (const col of missingColumns) {
    try {
      // SQLite ALTER TABLE ADD COLUMN only supports columns with default values or NULL
      // For NOT NULL columns without defaults, we need to add them as nullable
      const nullable = col.notnull === 0 || col.dflt_value !== null;
      const defaultClause = col.dflt_value !== null ? ` DEFAULT ${col.dflt_value}` : '';

      // eslint-disable-next-line no-await-in-loop
      await archiveSequelize.query(
        `ALTER TABLE "${tableName}" ADD COLUMN "${col.name}" ${col.type}${nullable ? '' : ' NULL'}${defaultClause}`
      );
      logger.info('added missing column to archive table', { tableName, column: col.name });
    } catch (err: any) {
      // Ignore "duplicate column" errors (race condition protection)
      if (!err.message?.includes('duplicate column')) {
        logger.warn('failed to add missing column to archive table', {
          tableName,
          column: col.name,
          error: err.message,
        });
      }
    }
  }

  // 4. Add archived_at column if not exists
  if (!archiveColumnNames.has('archived_at') && !mainColumnNames.has('archived_at')) {
    const queryInterface = archiveSequelize.getQueryInterface();
    await queryInterface.addColumn(tableName, 'archived_at', {
      type: DataTypes.DATE,
      allowNull: false,
    });
  }
}

export function listArchiveFiles(): string[] {
  const archiveDir = getArchiveDir();
  const files = fs.readdirSync(archiveDir).filter((name) => name.endsWith('.db'));
  files.sort();
  return files.map((name) => path.join(archiveDir, name));
}

export function getFileSize(filePath: string): number {
  try {
    return fs.statSync(filePath).size;
  } catch (error) {
    logger.warn('stat archive file failed', { filePath, error });
    return 0;
  }
}

/**
 * Remove oldest archive files when exceeding maxFiles limit.
 * Archive files are organized by year (archive-YYYY.db), so maxFiles effectively means
 * "keep N years of archived data" (default 10 years).
 *
 * This is part of the data retention policy: archive files older than maxFiles years
 * are considered disposable history. The archived data has already served its purpose
 * (query window, compliance retention period). If longer retention is needed,
 * external backup should be configured before enabling archive cleanup.
 *
 * Compliance note: this deletion is a deliberate product decision — archived data beyond
 * the retention window (default 10 years) is treated as expired. Operators requiring
 * longer audit trails should configure external backup before enabling data retention.
 */
export function cleanupOldArchiveFiles(maxFiles: number): string[] {
  const files = listArchiveFiles();
  if (files.length <= maxFiles) {
    return [];
  }

  // Files are sorted ascending by name (oldest first), remove the oldest ones
  const toRemove = files.slice(0, files.length - maxFiles);
  const removed: string[] = [];
  for (const filePath of toRemove) {
    try {
      fs.unlinkSync(filePath);
      removed.push(path.basename(filePath));
      logger.info('removed old archive file', { filePath });
    } catch (error) {
      logger.warn('failed to remove old archive file', { filePath, error });
    }
  }
  return removed;
}
