const path = require('path');
const fs = require('fs/promises');
const { randomUUID } = require('crypto');

function shellQuote(value) {
  return `'${String(value).replace(/'/g, `"'"'`)}'`;
}

function slugify(value) {
  return value.replace(/[^a-zA-Z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase() || 'item';
}

function formatStamp(date = new Date()) {
  return date.toISOString().replace(/[:.]/g, '-');
}

function normalizeDockerHostPath(inputPath) {
  const value = String(inputPath || '').trim();
  if (/^[A-Za-z]:[\\/]/.test(value)) {
    const drive = value[0].toLowerCase();
    const suffix = value.slice(2).replace(/\\/g, '/').replace(/^\/+/, '');
    return `/run/desktop/mnt/host/${drive}/${suffix}`;
  }

  return value.replace(/\\/g, '/');
}

function normalizeContainerPath(inputPath) {
  const value = String(inputPath || '').trim().replace(/\\/g, '/');
  if (!value.startsWith('/')) {
    throw new Error('Em execucao via Docker, o diretorio de backup deve ser absoluto dentro do container (ex.: /app/data/backups).');
  }

  return value;
}

function normalizeMounts(containerInspect) {
  return (containerInspect.Mounts || [])
    .filter((mount) => mount.Type === 'bind' || mount.Type === 'volume')
    .map((mount) => ({
      type: mount.Type,
      name: mount.Name || null,
      source: mount.Source,
      destination: mount.Destination,
      rw: mount.RW,
    }))
    .sort((left, right) => {
      const leftKey = `${left.destination}|${left.type}|${left.name || left.source}`;
      const rightKey = `${right.destination}|${right.type}|${right.name || right.source}`;
      return leftKey.localeCompare(rightKey);
    });
}

function sameMountSignature(leftMounts, rightMounts) {
  if (leftMounts.length !== rightMounts.length) {
    return false;
  }

  return leftMounts.every((left, index) => {
    const right = rightMounts[index];
    return (
      left.type === right.type
      && left.name === right.name
      && left.source === right.source
      && left.destination === right.destination
    );
  });
}

function getMountBindingSource(mount) {
  return mount.type === 'volume' ? mount.name : mount.source;
}

function normalizeBackupScope(value) {
  return value === 'container' ? 'container' : 'volumes';
}

function containerSnapshotPath(profileId, containerId, scope) {
  return `/tmp/dockerbackup-${slugify(profileId)}-${containerId.slice(0, 12)}-${scope}.snar`;
}

function toContainerRelPath(absPath) {
  return String(absPath || '').replace(/^\/+/, '') || '.';
}

function parseManifestLines(rawOutput) {
  const map = new Map();
  for (const line of String(rawOutput || '').split(/\r?\n/)) {
    const trimmed = line.trim();
    if (!trimmed) {
      continue;
    }

    const [relativePath, sizeRaw, mtimeRaw, scopeRaw] = trimmed.split('|');
    if (!relativePath) {
      continue;
    }

    const key = scopeRaw
      ? `${toContainerRelPath(scopeRaw)}/${relativePath}`.replace(/\/\//g, '/')
      : relativePath;

    map.set(key, {
      size: Number(sizeRaw || 0),
      mtime: Number(mtimeRaw || 0),
    });
  }

  return map;
}

function calculateManifestDiff(beforeMap, afterMap) {
  let deleted = 0;
  let created = 0;
  let modified = 0;

  for (const [filePath, beforeEntry] of beforeMap.entries()) {
    const afterEntry = afterMap.get(filePath);
    if (!afterEntry) {
      deleted += 1;
      continue;
    }

    if (beforeEntry.size !== afterEntry.size || beforeEntry.mtime !== afterEntry.mtime) {
      modified += 1;
    }
  }

  for (const filePath of afterMap.keys()) {
    if (!beforeMap.has(filePath)) {
      created += 1;
    }
  }

  return { deleted, created, modified };
}

class BackupService {
  constructor({ dockerService, store }) {
    this.dockerService = dockerService;
    this.store = store;
  }

  async runProfile(profileId, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
    const profile = await this.store.getProfile(profileId);
    if (!profile) {
      throw new Error('Profile nao encontrado.');
    }

    if (!profile.containerIds.length) {
      throw new Error('Selecione ao menos um container no profile.');
    }

    const effectiveMode = ['full', 'incremental'].includes(options.mode)
      ? options.mode
      : (['full', 'incremental'].includes(profile.mode) ? profile.mode : 'full');

    const backupScope = normalizeBackupScope(profile.backupScope);

    const backupRun = {
      id: randomUUID(),
      profileId: profile.id,
      profileName: profile.name,
      createdAt: new Date().toISOString(),
      mode: effectiveMode,
      backupScope,
      backupDir: profile.backupDir,
      basedOnFullBackupId: options.basedOnFullBackupId || null,
      status: 'ok',
      containers: [],
    };

    const progress = {
      profileId: profile.id,
      profileName: profile.name,
      startedAt: backupRun.createdAt,
      status: 'running',
      overall: {
        total: profile.containerIds.length,
        completed: 0,
        pending: profile.containerIds.length,
        percent: 0,
      },
      currentContainer: null,
    };

    const emitProgress = () => {
      onProgress(JSON.parse(JSON.stringify(progress)));
    };

    emitProgress();

    for (const [index, containerId] of profile.containerIds.entries()) {
      progress.currentContainer = {
        containerId,
        index: index + 1,
        total: profile.containerIds.length,
        containerName: null,
        status: 'running',
        step: 'iniciando',
        message: 'Preparando backup do container.',
        logs: [],
        percent: 0,
        file: {
          current: 0,
          total: 0,
          currentFile: null,
          percent: 0,
        },
      };
      emitProgress();

      const containerBackup = await this.backupContainer(profile, containerId, backupRun.createdAt, {
        mode: effectiveMode,
        backupScope,
        onProgress: (containerProgress) => {
          progress.currentContainer = {
            ...progress.currentContainer,
            ...containerProgress,
          };
          emitProgress();
        },
      });

      backupRun.containers.push(containerBackup);
      if (containerBackup.status !== 'ok') {
        backupRun.status = 'partial';
      }

      progress.overall.completed += 1;
      progress.overall.pending = Math.max(0, progress.overall.total - progress.overall.completed);
      progress.overall.percent = progress.overall.total
        ? Math.round((progress.overall.completed / progress.overall.total) * 100)
        : 100;
      progress.currentContainer = {
        ...(progress.currentContainer || {}),
        status: containerBackup.status,
        percent: 100,
      };
      emitProgress();
    }

    await this.store.addBackup(backupRun);

    progress.status = backupRun.status === 'ok' ? 'completed' : 'completed-with-errors';
    progress.finishedAt = new Date().toISOString();
    progress.currentContainer = null;
    progress.overall.percent = 100;
    progress.overall.pending = 0;
    emitProgress();

    return backupRun;
  }

  async backupContainer(profile, containerId, runDateIso, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
    const runMode = ['full', 'incremental'].includes(options.mode)
      ? options.mode
      : (['full', 'incremental'].includes(profile.mode) ? profile.mode : 'full');
    const backupScope = normalizeBackupScope(options.backupScope || profile.backupScope);

    let inspect;
    let mounts = [];
    let containerName = containerId.slice(0, 12);

    try {
      inspect = await this.dockerService.inspectContainer(containerId);
      mounts = normalizeMounts(inspect);
      containerName = inspect.Name.replace(/^\//, '');
    } catch (error) {
      onProgress({ containerName, status: 'error', percent: 100, step: 'erro', message: error.message });
      return {
        containerId,
        containerName,
        status: 'error',
        mode: runMode,
        error: `Falha ao inspecionar container: ${error.message}`,
      };
    }

    if (backupScope === 'volumes' && !mounts.length) {
      onProgress({
        containerName,
        status: 'skipped',
        step: 'concluido',
        message: 'Container sem volumes elegiveis.',
        percent: 100,
        file: { current: 0, total: 0, currentFile: null, percent: 100 },
      });
      return {
        containerId: inspect.Id,
        containerName,
        status: 'skipped',
        mode: runMode,
        message: 'Container sem volumes ou bind mounts elegiveis.',
      };
    }

    const runInDocker = this.dockerService.isRunningInDocker();
    const backupRoot = runInDocker
      ? normalizeContainerPath(profile.backupDir)
      : normalizeDockerHostPath(profile.backupDir);
    const safeContainerName = slugify(containerName);
    const safeProfileName = slugify(profile.name);
    const stamp = formatStamp(new Date(runDateIso));
    const archiveRelativePath = path.posix.join(safeProfileName, safeContainerName, `${stamp}-${runMode}.tar.gz`);
    const snapshotRelativePath = path.posix.join(safeProfileName, safeContainerName, 'latest.snar');

    // Filter mounts to only those the user selected (if volumeSelections is defined for this container)
    const selectedPaths = profile.volumeSelections?.[containerId] || profile.volumeSelections?.[inspect.Id];
    const activeMounts = (backupScope === 'volumes' && selectedPaths?.length)
      ? mounts.filter((m) => selectedPaths.includes(m.destination))
      : mounts;

    if (backupScope === 'volumes' && !activeMounts.length) {
      onProgress({
        containerName,
        status: 'skipped',
        step: 'concluido',
        message: 'Nenhum volume selecionado para backup.',
        percent: 100,
        file: { current: 0, total: 0, currentFile: null, percent: 100 },
      });
      return {
        containerId: inspect.Id,
        containerName,
        status: 'skipped',
        mode: runMode,
        message: 'Nenhum volume selecionado para backup.',
      };
    }

    const containerBackup = {
      containerId: inspect.Id,
      containerName,
      backupScope,
      backupPaths: backupScope === 'container' ? ['/'] : activeMounts.map((mount) => mount.destination),
      mountSignature: mounts,
      archiveRelativePath,
      snapshotRelativePath,
      wasRunning: inspect.State?.Running === true,
      mode: runMode,
      status: 'ok',
    };

    const logs = [];
    const pushLog = (message, step = 'processando') => {
      const line = `[${new Date().toLocaleTimeString('pt-BR')}] ${message}`;
      logs.push(line);
      while (logs.length > 40) {
        logs.shift();
      }

      onProgress({
        containerName,
        step,
        message,
        logs: [...logs],
      });
    };

    let fileTotal = 0;
    let fileCurrent = 0;
    const updateFileProgress = (currentFile = null) => {
      const filePercent = fileTotal > 0 ? Math.min(100, Math.round((fileCurrent / fileTotal) * 100)) : 0;
      onProgress({
        containerName,
        status: 'running',
        step: 'processando',
        percent: filePercent,
        file: {
          current: fileCurrent,
          total: fileTotal,
          currentFile,
          percent: filePercent,
        },
      });
    };

    try {
      if (backupScope === 'container' && !runInDocker) {
        throw new Error('Backup do container inteiro requer app executando via Docker.');
      }

      pushLog(`Escopo selecionado: ${backupScope === 'container' ? 'container inteiro' : 'somente volumes'}.`, 'preparando');

      if (runInDocker) {
        await this.dockerService.ensureLocalDirectory(backupRoot);
        pushLog(`Diretorio de backup pronto em ${backupRoot}.`, 'preparando');

        const originalRunning = inspect.State?.Running === true;
        let tempStarted = false;
        try {
          if (originalRunning) {
            pushLog('Container ativo detectado. Parando antes do backup.', 'preparando');
            await this.dockerService.stopContainer(containerId);
          }

          pushLog('Iniciando container temporariamente para snapshot.', 'preparando');
          await this.dockerService.repairAndStartContainer(containerId);
          tempStarted = true;

          const sourcePaths = backupScope === 'container'
            ? ['/']
            : activeMounts.map((mount) => mount.destination);
          const relSourcePaths = sourcePaths.map((item) => toContainerRelPath(item));

          if (backupScope === 'volumes') {
            pushLog('Contando arquivos para barra de progresso.', 'contando');
            const countCmd = `set -eu; TOTAL=0; for p in ${relSourcePaths.map((item) => shellQuote(item)).join(' ')}; do if [ -e \"/$p\" ]; then C=$(find \"/$p\" 2>/dev/null | wc -l | tr -d \" \" ); TOTAL=$((TOTAL + C)); fi; done; echo \"$TOTAL\"`;
            const output = await this.dockerService.runContainerCommand(containerId, countCmd);
            const parsed = Number(output.split(/\r?\n/).pop());
            fileTotal = Number.isFinite(parsed) ? parsed : 0;
            pushLog(`Total de arquivos identificado: ${fileTotal}.`, 'contando');
          }

          const absoluteArchivePath = path.posix.join(backupRoot, archiveRelativePath);
          const snarInContainer = containerSnapshotPath(profile.id, containerId, backupScope);
          const absoluteSnapshotPath = path.posix.join(backupRoot, snapshotRelativePath);

          // Detecta se o container tem GNU tar (--listed-incremental é extensão GNU).
          // Containers Alpine/BusyBox usam o fallback --newer-mtime.
          const hasGnuTar = await this.dockerService.containerHasGnuTar(containerId);
          pushLog(`GNU tar detectado no container: ${hasGnuTar ? 'sim' : 'nao (usando --newer-mtime como fallback)'}.`, 'preparando');

          let tarIncrementalFlag = '';

          if (hasGnuTar) {
            // Gerencia o .snar assim como o script shell usa --listed-incremental=$dirbackup/backup.snar:
            // - Full: remove o .snar anterior do container para forçar snapshot limpo.
            // - Incremental: injeta o .snar salvo no diretório de backup de volta no container.
            if (runMode === 'full') {
              await this.dockerService.runContainerCommand(containerId, `rm -f ${shellQuote(snarInContainer)}`).catch(() => null);
              pushLog('Backup full: snapshot incremental anterior removido.', 'preparando');
            } else {
              try {
                await fs.access(absoluteSnapshotPath);
                await this.dockerService.putSnarToContainer(containerId, absoluteSnapshotPath, snarInContainer);
                pushLog('Backup incremental: snapshot anterior restaurado no container.', 'preparando');
              } catch {
                pushLog('Aviso: snapshot anterior nao encontrado, gerando backup completo.', 'preparando');
              }
            }
            tarIncrementalFlag = `--listed-incremental=${shellQuote(snarInContainer)}`;
          } else {
            // Fallback para containers sem GNU tar: usa helper container com GNU tar
            // montando os volumes diretamente — produz .snar como qualquer outro container.
            if (runMode === 'full') {
              // Full: remove .snar anterior para forçar snapshot limpo no helper.
              await fs.rm(absoluteSnapshotPath, { force: true }).catch(() => null);
            }
            // O .snar é gerenciado pelo helper usando helperSnarPath calculado abaixo.
          }

          // Containers BusyBox sem GNU tar: roda o tar num helper container que TEM GNU tar,
          // montando os volumes do container alvo diretamente.
          if (!hasGnuTar && backupScope === 'volumes' && activeMounts.length) {
            pushLog('Container sem GNU tar: usando helper com GNU tar para gerar archive.', 'gerando-tar');
            updateFileProgress();

            // Quando rodando dentro do Docker, backupRoot é um path interno do container da app.
            // Precisamos do source real (host path ou volume name) para passar ao helper container.
            const selfBind = await this.dockerService.getSelfBindSource(backupRoot);
            if (!selfBind) {
              throw new Error(
                `Nao foi possivel determinar o source do diretorio de backup (${backupRoot}) para o helper. ` +
                'Verifique se o diretorio de backup esta montado via volume ou bind no container da app.'
              );
            }

            // suffix é o subpath dentro do volume (ex: /backups quando mount=/app/data e backupRoot=/app/data/backups)
            const helperBackupRoot = `/backuproot_base${selfBind.suffix}`;
            const helperBinds = [`${selfBind.source}:/backuproot_base`];
            const helperRelPaths = [];
            for (const mount of activeMounts) {
              const src = mount.type === 'volume' ? mount.name : mount.source;
              // Monta no path real do container (ex: /var/lib/gitea) para que o archive
              // gerado tenha entradas com os paths corretos (ex: var/lib/gitea/...).
              // Isso garante compatibilidade com o restore via helper.
              helperBinds.push(`${src}:${mount.destination}:ro`);
              helperRelPaths.push(toContainerRelPath(mount.destination));
            }
            const helperArchivePath = `${helperBackupRoot}/${archiveRelativePath}`;
            const helperSnarPath = `${helperBackupRoot}/${snapshotRelativePath}`;
            const helperSnarDir = path.posix.dirname(helperSnarPath);
            const helperCmd = [
              'set -u',
              `mkdir -p ${shellQuote(path.posix.dirname(helperArchivePath))} ${shellQuote(helperSnarDir)}`,
              `echo "__DBKP_TAR_BEGIN__" 1>&2`,
              `tar --ignore-failed-read --listed-incremental=${shellQuote(helperSnarPath)} -czvf ${shellQuote(helperArchivePath)} -C / ${helperRelPaths.map((p) => shellQuote(p)).join(' ')}; TAR_RC=$?; [ $TAR_RC -le 1 ] || exit $TAR_RC`,
            ].join('; ');

            await this.dockerService.runHelper({
              binds: helperBinds,
              cmd: helperCmd,
              maxOkExitCode: 1,
              onOutput: (line, stream) => {
                const normalizedLine = String(line || '').trim();
                if (!normalizedLine || normalizedLine.startsWith('__DBKP_TAR_BEGIN__')) {
                  return;
                }
                // No helper (tar escreve em arquivo): lista de arquivos vai para stdout;
                // avisos do tar vão para stderr.
                if (stream === 'stdout' && !normalizedLine.startsWith('tar:')) {
                  fileCurrent += 1;
                  updateFileProgress(normalizedLine);
                } else if (stream === 'stderr') {
                  pushLog(`Aviso do tar: ${normalizedLine}`, 'gerando-tar');
                }
              },
            });

            pushLog(`Arquivo gerado via helper: ${absoluteArchivePath}`, 'finalizando');
            pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando');

            containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
            try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {}

            onProgress({
              containerName,
              status: 'ok',
              step: 'concluido',
              message: 'Backup concluido com sucesso.',
              percent: 100,
              file: { current: Math.max(fileCurrent, fileTotal), total: fileTotal, currentFile: null, percent: 100 },
            });
            return containerBackup;
          }

          const tarParts = [
            'set -u',
            'umask 077',
            'echo "__DBKP_TAR_BEGIN__" 1>&2',
          ];

          // --ignore-failed-read é extensão GNU tar — não existe no BusyBox tar (Alpine).
          // Usar condicionalmente para evitar aborto silencioso com 0 bytes no arquivo.
          const gnuFlags = hasGnuTar ? '--ignore-failed-read' : '';

          // GNU tar: exit 0 = ok, exit 1 = avisos (arquivos mudaram, permissão negada), exit 2 = erro fatal.
          // Aceitamos exit 1 como sucesso para não descartar archives válidos com avisos menores.
          if (backupScope === 'container') {
            tarParts.push(
              `tar ${gnuFlags} ${tarIncrementalFlag} -czvf - -C / --exclude=proc --exclude=sys --exclude=dev --exclude=run --exclude=tmp .; TAR_RC=$?; [ $TAR_RC -le 1 ] || exit $TAR_RC`
            );
          } else {
            tarParts.push(
              `tar ${gnuFlags} ${tarIncrementalFlag} -czvf - -C / ${relSourcePaths.map((item) => shellQuote(item)).join(' ')}; TAR_RC=$?; [ $TAR_RC -le 1 ] || exit $TAR_RC`
            );
          }

          updateFileProgress();
          pushLog('Iniciando compactacao tar do container.', 'gerando-tar');

          await this.dockerService.streamContainerCommandToFile(containerId, tarParts.join('; '), absoluteArchivePath, {
            maxOkExitCode: 1,
            onOutput: (line, stream) => {
              const normalizedLine = String(line || '').trim();
              if (!normalizedLine || stream !== 'stderr' || normalizedLine.startsWith('__DBKP_TAR_BEGIN__')) {
                return;
              }

              if (!normalizedLine.startsWith('tar:')) {
                fileCurrent += 1;
                updateFileProgress(normalizedLine);
              } else {
                pushLog(`Aviso do tar: ${normalizedLine}`, 'gerando-tar');
              }
            },
          });

          pushLog(`Arquivo gerado: ${absoluteArchivePath}`, 'finalizando');

          // Persiste o .snar atualizado no diretório de backup (como o script shell faz com $dirbackup/backup.snar)
          // para que a cadeia incremental sobreviva a recriações do container.
          if (hasGnuTar) {
            const snarSaved = await this.dockerService.getSnarFromContainer(containerId, snarInContainer, absoluteSnapshotPath).catch(() => false);
            if (snarSaved) {
              pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando');
            }
          }
        } finally {
          if (tempStarted) {
            pushLog('Encerrando container apos backup.', 'finalizando');
            await this.dockerService.stopContainer(containerId).catch(() => null);
          }

          if (originalRunning) {
            pushLog('Reiniciando container (estava ativo antes do backup).', 'finalizando');
            await this.dockerService.startContainer(containerId).catch(() => null);
          }
        }

        containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
        try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {}

        onProgress({
          containerName,
          status: 'ok',
          step: 'concluido',
          message: 'Backup concluido com sucesso.',
          percent: 100,
          file: {
            current: Math.max(fileCurrent, fileTotal),
            total: fileTotal,
            currentFile: null,
            percent: 100,
          },
        });

        return containerBackup;
      }

      if (backupScope === 'container') {
        throw new Error('Backup de container inteiro sem Docker nativo nao e suportado.');
      }

      await this.dockerService.ensureHostDirectory(backupRoot);
      const wasRunning = inspect.State?.Running === true;
      if (wasRunning) {
        await this.dockerService.stopContainer(containerId);
      }

      const binds = [`${backupRoot}:/backuproot`];
      for (const [index, mount] of activeMounts.entries()) {
        binds.push(`${getMountBindingSource(mount)}:/payload/m${index}:ro`);
      }

      const archivePath = `/backuproot/${archiveRelativePath}`;
      const snapshotPath = `/backuproot/${snapshotRelativePath}`;
      const parentDir = path.posix.dirname(archivePath);
      const cmdParts = ['set -eu', `mkdir -p ${shellQuote(parentDir)}`];
      if (runMode === 'full') {
        cmdParts.push(`rm -f ${shellQuote(snapshotPath)}`);
      }
      cmdParts.push('TOTAL_FILES=$(find /payload -type f | wc -l | tr -d " ")');
      cmdParts.push('echo "__DBKP_TOTAL_FILES__=${TOTAL_FILES}"');
      cmdParts.push(`tar --listed-incremental=${shellQuote(snapshotPath)} -czvf ${shellQuote(archivePath)} -C /payload .`);

      await this.dockerService.runHelper({
        binds,
        cmd: cmdParts.join(' && '),
        onOutput: (line, stream) => {
          const normalizedLine = String(line || '').trim();
          if (!normalizedLine) {
            return;
          }

          if (normalizedLine.startsWith('__DBKP_TOTAL_FILES__=')) {
            const parsed = Number(normalizedLine.split('=')[1]);
            fileTotal = Number.isFinite(parsed) ? parsed : 0;
            updateFileProgress();
            return;
          }

          if (stream === 'stdout' && !normalizedLine.startsWith('tar:')) {
            fileCurrent += 1;
            updateFileProgress(normalizedLine);
          }
        },
      });

      if (wasRunning) {
        await this.dockerService.startContainer(containerId).catch(() => null);
      }

      containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
      try {
        const hostArchivePath = path.join(profile.backupDir, ...archiveRelativePath.split('/'));
        containerBackup.archiveSize = (await fs.stat(hostArchivePath)).size;
      } catch {}

      onProgress({
        containerName,
        status: 'ok',
        step: 'concluido',
        message: 'Backup concluido com sucesso.',
        percent: 100,
        file: {
          current: Math.max(fileCurrent, fileTotal),
          total: fileTotal,
          currentFile: null,
          percent: 100,
        },
      });

      return containerBackup;
    } catch (error) {
      onProgress({
        containerName,
        status: 'error',
        step: 'erro',
        message: error.message,
        percent: 100,
      });
      return {
        ...containerBackup,
        status: 'error',
        error: error.message,
      };
    }
  }

  async restoreBackup(profileId, backupId, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
    const selectedContainerIds = options.selectedContainerIds;

    const profile = await this.store.getProfile(profileId);
    if (!profile) {
      throw new Error('Profile nao encontrado.');
    }

    const backupRun = await this.store.getBackup(backupId);
    if (!backupRun || backupRun.profileId !== profileId) {
      throw new Error('Backup nao encontrado para este profile.');
    }

    const allowedContainerIds = new Set(Array.isArray(selectedContainerIds) && selectedContainerIds.length
      ? selectedContainerIds
      : backupRun.containers.map((item) => item.containerId));

    const targets = backupRun.containers.filter((item) => item.status === 'ok' && allowedContainerIds.has(item.containerId));
    if (!targets.length) {
      throw new Error('Nenhum container valido foi selecionado para restore.');
    }

    const progress = {
      profileId: profile.id,
      profileName: profile.name,
      operation: 'restore',
      startedAt: new Date().toISOString(),
      status: 'running',
      overall: {
        total: targets.length,
        completed: 0,
        pending: targets.length,
        percent: 0,
      },
      currentContainer: null,
    };

    const emitProgress = () => {
      onProgress(JSON.parse(JSON.stringify(progress)));
    };

    emitProgress();

    const results = [];

    for (const containerEntry of targets) {
      const logs = [];
      const pushLog = (message, step = 'restaurando') => {
        const line = `[${new Date().toLocaleTimeString('pt-BR')}] ${message}`;
        logs.push(line);
        while (logs.length > 40) {
          logs.shift();
        }

        progress.currentContainer = {
          ...(progress.currentContainer || {}),
          message,
          step,
          logs: [...logs],
        };
        emitProgress();
      };

      progress.currentContainer = {
        containerId: containerEntry.containerId,
        containerName: containerEntry.containerName,
        status: 'running',
        step: 'preparando',
        message: 'Preparando restauracao do container.',
        logs: [],
        percent: 0,
        file: {
          current: 0,
          total: 0,
          currentFile: null,
          percent: 0,
        },
      };
      emitProgress();

      try {
        const chain = await this.store.getBackupsForContainer(profileId, containerEntry.containerId, backupId);
        if (!chain.length || chain[0].mode !== 'full') {
          throw new Error(`Nao existe cadeia full + incremental valida para ${containerEntry.containerName}.`);
        }

        pushLog(`Cadeia de restore encontrada com ${chain.length} arquivo(s).`, 'preparando');

        const restoreInfo = await this.restoreContainer(profile, containerEntry, chain, {
          onProgress: (snapshot) => {
            progress.currentContainer = {
              ...progress.currentContainer,
              ...snapshot,
            };
            emitProgress();
          },
          pushLog,
        });

        progress.currentContainer = {
          ...progress.currentContainer,
          status: 'ok',
          step: 'concluido',
          message: 'Restore concluido com sucesso.',
          percent: 100,
          file: {
            ...(progress.currentContainer?.file || {}),
            percent: 100,
          },
        };
        emitProgress();

        results.push({
          containerId: containerEntry.containerId,
          containerName: containerEntry.containerName,
          status: 'ok',
          stats: restoreInfo?.stats || null,
        });
      } catch (error) {
        pushLog(`Falha no restore: ${error.message}`, 'erro');
        progress.currentContainer = {
          ...progress.currentContainer,
          status: 'error',
          step: 'erro',
          message: error.message,
          percent: 100,
        };
        emitProgress();

        results.push({
          containerId: containerEntry.containerId,
          containerName: containerEntry.containerName,
          status: 'error',
          error: error.message,
        });
      }

      progress.overall.completed += 1;
      progress.overall.pending = Math.max(0, progress.overall.total - progress.overall.completed);
      progress.overall.percent = progress.overall.total
        ? Math.round((progress.overall.completed / progress.overall.total) * 100)
        : 100;
      emitProgress();
    }

    progress.status = results.every((item) => item.status === 'ok') ? 'completed' : 'completed-with-errors';
    progress.finishedAt = new Date().toISOString();
    progress.currentContainer = null;
    progress.overall.percent = 100;
    progress.overall.pending = 0;
    emitProgress();

    return {
      backupId,
      status: results.every((item) => item.status === 'ok') ? 'ok' : 'partial',
      containers: results,
    };
  }

  async restoreContainer(profile, targetEntry, chain, options = {}) {
    const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
    const pushLog = typeof options.pushLog === 'function' ? options.pushLog : () => {};

    const inspect = await this.dockerService.inspectContainer(targetEntry.containerId);
    const backupScope = normalizeBackupScope(targetEntry.backupScope || profile.backupScope);
    const currentMounts = normalizeMounts(inspect);

    if (backupScope === 'volumes' && !sameMountSignature(targetEntry.mountSignature, currentMounts)) {
      throw new Error(`Os mounts atuais do container ${targetEntry.containerName} nao batem com o backup selecionado.`);
    }

    const runInDocker = this.dockerService.isRunningInDocker();
    const useNativeRestore = runInDocker;

    if (useNativeRestore) {
      const backupRoot = normalizeContainerPath(profile.backupDir);
      const originalWasRunning = inspect.State?.Running === true;
      const restoreStats = { deleted: 0, created: 0, modified: 0 };

      try {
        if (originalWasRunning) {
          pushLog('Container ativo detectado. Parando antes do restore.', 'preparando');
          await this.dockerService.stopContainer(targetEntry.containerId);
        }

        if (backupScope === 'volumes') {
          const restorePaths = (chain[0]?.backupPaths && chain[0].backupPaths.length)
            ? chain[0].backupPaths
            : currentMounts.map((mount) => mount.destination);

          // Validar que todos os archives existem antes de tocar nos dados.
          for (const entry of chain) {
            await fs.access(path.posix.join(backupRoot, entry.archiveRelativePath));
          }

          // Para restore de volumes no modo Docker-nativo, NÃO usamos putArchive.
          // putArchive num container parado escreve na camada overlay do container,
          // não nos volumes nomeados. Quando o container inicia, o volume montado
          // (agora vazio após a limpeza) sobrepõe a camada → arquivos invisíveis.
          //
          // Solução: helper container que monta os volumes nos seus paths reais
          // (ex: gitea_data:/var/lib/gitea) e extrai o archive com -C /.
          // O archive já tem as entradas nos paths reais (ex: var/lib/gitea/...).
          const selfBind = await this.dockerService.getSelfBindSource(backupRoot);
          if (!selfBind) {
            throw new Error(
              `Nao foi possivel determinar o source do diretorio de backup (${backupRoot}) para o helper de restore. ` +
              'Verifique se o diretorio de backup esta montado via volume ou bind no container da app.'
            );
          }

          const helperBackupRoot = `/backuproot_base${selfBind.suffix}`;
          const helperBinds = [`${selfBind.source}:/backuproot_base:ro`];
          const helperCmds = [];

          // Montar cada volume no seu path real e preparar limpeza.
          for (const mount of currentMounts) {
            if (!restorePaths.includes(mount.destination)) continue;
            const src = mount.type === 'volume' ? mount.name : mount.source;
            helperBinds.push(`${src}:${mount.destination}`);
            helperCmds.push(
              `find ${shellQuote(mount.destination)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true`
            );
          }

          pushLog('Limpando volumes e restaurando archives via helper.', 'preparando');

          // Adicionar extração de cada archive.
          // IMPORTANTE: não usar 'set -e' nem '&&' — tar retorna código 1 (avisos de
          // permissão/ownership) ao extrair em volumes. Com set -e/&&, o incremental
          // nunca seria aplicado porque o full aborta a cadeia. Tratar código 1 como
          // sucesso e apenas falhar com código >= 2.
          //
          // Para archives incrementais: --listed-incremental=/dev/null instrui o tar a
          // ler o snapshot embutido no archive e APAGAR arquivos que foram removidos
          // entre o backup anterior e este incremental (comportamento delta correto).
          // Para o archive full: extração simples, sem remoção de arquivos existentes.
          for (const [index, entry] of chain.entries()) {
            const archivePath = `${helperBackupRoot}/${entry.archiveRelativePath}`;
            const tarIncrFlag = entry.mode === 'incremental' ? '--listed-incremental=/dev/null ' : '';
            helperCmds.push(`echo "[${index + 1}/${chain.length}] Aplicando: ${entry.archiveRelativePath}" 1>&2`);
            helperCmds.push(`tar ${tarIncrFlag}-xzvf ${shellQuote(archivePath)} -C /; RC=$?; [ $RC -le 1 ] || exit $RC`);
          }

          // Emite progresso inicial (o helper roda tudo de uma vez, sem granularidade por arquivo).
          onProgress({
            step: 'restaurando',
            file: { current: 0, total: chain.length, currentFile: null, percent: 0 },
            percent: 10,
          });

          await this.dockerService.runHelper({
            binds: helperBinds,
            cmd: helperCmds.join('\n'),
            onOutput: (line, streamName) => {
              const normalized = String(line || '').trim();
              if (!normalized) return;
              if (streamName === 'stdout') {
                // Linha do tar -v: cada arquivo extraído (não logar — podem ser muitos)
                restoreStats.created++;
              } else {
                // Linha do tar (warnings) ou echo de progresso (stderr)
                pushLog(normalized, 'restaurando');
              }
            },
          });

          onProgress({
            step: 'finalizando',
            file: { current: chain.length, total: chain.length, currentFile: null, percent: 100 },
            percent: 100,
          });

          pushLog('Restore de volumes concluido.', 'finalizando');
        } else {
          // Escopo container inteiro.
          for (const entry of chain) {
            await fs.access(path.posix.join(backupRoot, entry.archiveRelativePath));
          }

          // Para escopo container, restaura via putArchive (escreve na camada do container).
          // Nota: paths cobertos por volumes nomeados não serão restaurados via esta rota,
          // pois o volume sobrepõe a camada após o container iniciar. Para esses cases,
          // use o escopo 'volumes' em vez de 'container'.
          for (const [index, entry] of chain.entries()) {
            const absoluteArchivePath = path.posix.join(backupRoot, entry.archiveRelativePath);

            onProgress({
              step: 'restaurando',
              file: {
                current: index + 1,
                total: chain.length,
                currentFile: entry.archiveRelativePath,
                percent: Math.round(((index + 1) / chain.length) * 100),
              },
              percent: Math.round(((index + 1) / chain.length) * 100),
            });
            pushLog(`Aplicando arquivo ${index + 1}/${chain.length}: ${entry.archiveRelativePath}`, 'restaurando');

            await this.dockerService.putCompressedArchiveFromFile(targetEntry.containerId, '/', absoluteArchivePath);
          }

          pushLog('Restore do container concluido.', 'finalizando');
        }
      } finally {
        if (originalWasRunning) {
          pushLog('Reiniciando container (estava ativo antes do restore).', 'finalizando');
          await this.dockerService.startContainer(targetEntry.containerId).catch(() => null);
        }
      }

      return { stats: restoreStats };
    }

    if (backupScope === 'container') {
      throw new Error('Restore do container inteiro requer app executando via Docker.');
    }

    const backupRoot = normalizeDockerHostPath(profile.backupDir);
    const wasRunning = inspect.State?.Running === true;
    const binds = [`${backupRoot}:/backuproot:ro`];
    const helperCmds = [];

    // Limitar restore aos volumes que estavam ativos no backup (mesma lógica do path nativo).
    const restorePathsNonNative = (chain[0]?.backupPaths && chain[0].backupPaths.length)
      ? chain[0].backupPaths
      : currentMounts.map((mount) => mount.destination);

    // Montar cada volume/bind no seu path real (igual ao path nativo).
    // Archives criados pelo helper já têm entries com paths reais (a0/..., var/lib/gitea/...),
    // então extrair com -C / coloca os arquivos nos volumes montados corretamente.
    for (const mount of currentMounts) {
      if (!restorePathsNonNative.includes(mount.destination)) continue;
      binds.push(`${getMountBindingSource(mount)}:${mount.destination}`);
      helperCmds.push(
        `find ${shellQuote(mount.destination)} -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true`
      );
    }

    // Extração de cada archive sem set -e/&&: tar frequentemente retorna código 1
    // (avisos de permissão/ownership) — aceitar código 1 como sucesso, falhar apenas em >= 2.
    // Para archives incrementais: --listed-incremental=/dev/null instrui o tar a ler o
    // snapshot embutido e apagar arquivos removidos entre backups (comportamento delta correto).
    const restoreStats = { deleted: 0, created: 0, modified: 0 };
    for (const [index, entry] of chain.entries()) {
      const tarIncrFlag = entry.mode === 'incremental' ? '--listed-incremental=/dev/null ' : '';
      helperCmds.push(`echo "[${index + 1}/${chain.length}] Aplicando: ${entry.archiveRelativePath}" 1>&2`);
      helperCmds.push(`tar ${tarIncrFlag}-xzvf ${shellQuote(`/backuproot/${entry.archiveRelativePath}`)} -C /; RC=$?; [ $RC -le 1 ] || exit $RC`);
    }

    try {
      if (wasRunning) {
        await this.dockerService.stopContainer(targetEntry.containerId);
      }

      await this.dockerService.runHelper({
        binds,
        cmd: helperCmds.join('\n'),
        onOutput: (line, streamName) => {
          const normalized = String(line || '').trim();
          if (!normalized) return;
          if (streamName === 'stdout') {
            restoreStats.created++;
          } else {
            pushLog(normalized, 'restaurando');
          }
        },
      });
    } finally {
      if (wasRunning) {
        await this.dockerService.startContainer(targetEntry.containerId).catch(() => null);
      }
    }

    return { stats: restoreStats };
  }
}

module.exports = BackupService;
