import {
  app,
  dialog,
  shell
} from 'electron';
import * as fsp from 'fs/promises';
import * as path from 'path';
import { destroyDatabase, initializeDatabase } from './db/database';
import {
  createZipArchive,
  extractZipEntries,
  readZipArchive,
  type ZipArchiveEntry
} from './data-archive';

export interface ExportUserDataResult {
  cancelled: boolean;
  exported: boolean;
  filePath?: string;
}

export interface ImportUserDataResult {
  backupPath?: string;
  cancelled: boolean;
  imported: boolean;
  restartRequired: boolean;
}

export interface EraseUserDataResult {
  erased: boolean;
  restartRequired: boolean;
}

const ARCHIVE_MANIFEST_PATH = 'metoyou-data-manifest.json';
const ARCHIVE_DATA_PREFIX = 'data/';
const BACKUP_DIRECTORY_NAME = 'metoyou-data-backups';

export async function openCurrentDataFolder(): Promise<boolean> {
  const error = await shell.openPath(app.getPath('userData'));

  return error.length === 0;
}

export async function exportUserData(): Promise<ExportUserDataResult> {
  const dataPath = app.getPath('userData');
  const defaultFileName = `metoyou-data-${new Date().toISOString()
    .slice(0, 10)}.dat`;
  const { canceled, filePath } = await dialog.showSaveDialog({
    defaultPath: path.join(app.getPath('documents'), defaultFileName),
    filters: [{ extensions: ['dat'], name: 'MetoYou data archive' }],
    title: 'Export MetoYou data'
  });

  if (canceled || !filePath) {
    return { cancelled: true, exported: false };
  }

  const entries: ZipArchiveEntry[] = [
    {
      data: Buffer.from(JSON.stringify({
        appVersion: app.getVersion(),
        exportedAt: new Date().toISOString(),
        format: 'metoyou-user-data',
        version: 1
      }, null, 2)),
      path: ARCHIVE_MANIFEST_PATH
    }
  ];

  for (const file of await collectDataFiles(dataPath)) {
    const relativePath = toArchivePath(path.relative(dataPath, file));

    entries.push({
      data: await fsp.readFile(file),
      path: `${ARCHIVE_DATA_PREFIX}${relativePath}`
    });
  }

  await fsp.writeFile(ensureDatExtension(filePath), createZipArchive(entries));

  return {
    cancelled: false,
    exported: true,
    filePath: ensureDatExtension(filePath)
  };
}

export async function importUserData(): Promise<ImportUserDataResult> {
  const { canceled, filePaths } = await dialog.showOpenDialog({
    filters: [{ extensions: ['dat', 'zip'], name: 'MetoYou data archive' }],
    properties: ['openFile'],
    title: 'Import MetoYou data'
  });

  if (canceled || filePaths.length === 0) {
    return {
      cancelled: true,
      imported: false,
      restartRequired: false
    };
  }

  const archiveEntries = readZipArchive(await fsp.readFile(filePaths[0]));

  validateArchiveManifest(archiveEntries);

  const importRoot = path.join(app.getPath('temp'), `metoyou-import-${Date.now()}`);
  const importDataPath = path.join(importRoot, 'data');

  try {
    await extractZipEntries(
      archiveEntries
        .filter((entry) => entry.path.startsWith(ARCHIVE_DATA_PREFIX))
        .map((entry) => ({
          data: entry.data,
          path: entry.path.slice(ARCHIVE_DATA_PREFIX.length)
        })),
      importDataPath
    );

    await destroyDatabase();

    const backupPath = await moveCurrentDataAside();

    await copyDirectory(importDataPath, app.getPath('userData'));
    await initializeDatabase();

    return {
      backupPath,
      cancelled: false,
      imported: true,
      restartRequired: true
    };
  } catch (error) {
    await initializeDatabase().catch(() => {});
    throw error;
  } finally {
    await fsp.rm(importRoot, { force: true, recursive: true }).catch(() => {});
  }
}

export async function eraseUserData(): Promise<EraseUserDataResult> {
  const dataPath = app.getPath('userData');

  await destroyDatabase();

  for (const entry of await fsp.readdir(dataPath, { withFileTypes: true }).catch(() => [])) {
    await fsp.rm(path.join(dataPath, entry.name), { force: true, recursive: true });
  }

  await fsp.mkdir(dataPath, { recursive: true });
  await initializeDatabase();

  return {
    erased: true,
    restartRequired: true
  };
}

async function collectDataFiles(directoryPath: string): Promise<string[]> {
  const files: string[] = [];
  const entries = await fsp.readdir(directoryPath, { withFileTypes: true }).catch(() => []);

  for (const entry of entries) {
    if (entry.name === BACKUP_DIRECTORY_NAME) {
      continue;
    }

    const entryPath = path.join(directoryPath, entry.name);

    if (entry.isDirectory()) {
      files.push(...await collectDataFiles(entryPath));
    } else if (entry.isFile()) {
      files.push(entryPath);
    }
  }

  return files;
}

async function moveCurrentDataAside(): Promise<string | undefined> {
  const dataPath = app.getPath('userData');
  const backupRoot = path.join(dataPath, BACKUP_DIRECTORY_NAME);
  const backupPath = path.join(backupRoot, `before-import-${new Date().toISOString()
    .replace(/[:.]/g, '-')}`);
  const entries = await fsp.readdir(dataPath, { withFileTypes: true }).catch(() => []);

  await fsp.mkdir(backupPath, { recursive: true });

  let movedAny = false;

  for (const entry of entries) {
    if (entry.name === BACKUP_DIRECTORY_NAME) {
      continue;
    }

    const sourcePath = path.join(dataPath, entry.name);
    const targetPath = path.join(backupPath, entry.name);

    await fsp.mkdir(path.dirname(targetPath), { recursive: true });
    await fsp.rename(sourcePath, targetPath).catch(async () => {
      await copyPath(sourcePath, targetPath);
      await fsp.rm(sourcePath, { force: true, recursive: true });
    });

    movedAny = true;
  }

  return movedAny ? backupPath : undefined;
}

async function copyDirectory(sourcePath: string, targetPath: string): Promise<void> {
  await fsp.mkdir(targetPath, { recursive: true });

  for (const entry of await fsp.readdir(sourcePath, { withFileTypes: true }).catch(() => [])) {
    await copyPath(path.join(sourcePath, entry.name), path.join(targetPath, entry.name));
  }
}

async function copyPath(sourcePath: string, targetPath: string): Promise<void> {
  const stats = await fsp.stat(sourcePath);

  if (stats.isDirectory()) {
    await copyDirectory(sourcePath, targetPath);
    return;
  }

  if (stats.isFile()) {
    await fsp.mkdir(path.dirname(targetPath), { recursive: true });
    await fsp.copyFile(sourcePath, targetPath);
  }
}

function validateArchiveManifest(entries: ZipArchiveEntry[]): void {
  const manifest = entries.find((entry) => entry.path === ARCHIVE_MANIFEST_PATH);

  if (!manifest) {
    throw new Error('The selected file is missing a MetoYou data manifest.');
  }

  const parsed = JSON.parse(manifest.data.toString('utf8')) as { format?: string; version?: number };

  if (parsed.format !== 'metoyou-user-data' || parsed.version !== 1) {
    throw new Error('The selected file uses an unsupported data archive format.');
  }
}

function ensureDatExtension(filePath: string): string {
  return path.extname(filePath).toLowerCase() === '.dat'
    ? filePath
    : `${filePath}.dat`;
}

function toArchivePath(filePath: string): string {
  return filePath.split(path.sep).join('/');
}
