import { execFile } from 'node:child_process';
import { existsSync, mkdirSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { promisify } from 'node:util';
import { homedir } from 'node:os';
import type { BackupConfig } from '../config/index.js';
import { auditLogger } from '../audit/index.js';

const execFileAsync = promisify(execFile);

export interface BackupTargetFiles {
  sessionsDb: string;
  vectorsDb?: string;
  memoryDir?: string;
}

export interface BackupRunOptions {
  dataDir: string;
  backupConfig: BackupConfig;
  now?: Date;
}

export interface BackupResult {
  archivePath: string;
  fileName: string;
  uploaded: boolean;
  remotePath?: string;
}

function expandHomePath(pathValue: string): string {
  if (!pathValue.startsWith('~/')) {
    return pathValue;
  }
  return resolve(homedir(), pathValue.slice(2));
}

function fileTimestamp(now: Date): string {
  const year = now.getUTCFullYear();
  const month = String(now.getUTCMonth() + 1).padStart(2, '0');
  const day = String(now.getUTCDate()).padStart(2, '0');
  const hour = String(now.getUTCHours()).padStart(2, '0');
  const minute = String(now.getUTCMinutes()).padStart(2, '0');
  const second = String(now.getUTCSeconds()).padStart(2, '0');
  return `${year}${month}${day}_${hour}${minute}${second}`;
}

function buildMinioHost(opts: {
  endpoint: string;
  accessKey: string;
  secretKey: string;
  secure: boolean;
}): string {
  const protocol = opts.secure ? 'https' : 'http';
  const encodedAccess = encodeURIComponent(opts.accessKey);
  const encodedSecret = encodeURIComponent(opts.secretKey);
  return `${protocol}://${encodedAccess}:${encodedSecret}@${opts.endpoint}`;
}

function buildObjectKey(prefix: string, fileName: string): string {
  const trimmed = prefix.replace(/^\/+|\/+$/g, '');
  return trimmed.length > 0 ? `${trimmed}/${fileName}` : fileName;
}

function resolveMcPath(pathValue: string | undefined): string {
  const trimmed = pathValue?.trim();
  return trimmed && trimmed.length > 0 ? trimmed : 'mc';
}

function formatMinioCliError(error: unknown, mcPath: string): string {
  if (error && typeof error === 'object' && 'code' in error && (error as { code?: unknown }).code === 'ENOENT') {
    return `MinIO client binary not found: ${mcPath}. Install MinIO Client (mc) or set \`backup.minio.mc_path\` to the full binary path.`;
  }
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}

function collectExistingEntries(opts: {
  dataDir: string;
  backupConfig: BackupConfig;
}): string[] {
  const entries: string[] = [];
  const sessionsPath = join(opts.dataDir, 'sessions.db');
  if (existsSync(sessionsPath)) {
    entries.push('sessions.db');
  }

  if (opts.backupConfig.include_vectors) {
    const vectorsPath = join(opts.dataDir, 'vectors.db');
    if (existsSync(vectorsPath)) {
      entries.push('vectors.db');
    }
  }

  const memoryPath = join(opts.dataDir, 'memory');
  if (existsSync(memoryPath)) {
    entries.push('memory');
  }

  return entries;
}

export async function runBackupSnapshot(opts: BackupRunOptions): Promise<BackupResult> {
  const now = opts.now ?? new Date();
  const localDir = expandHomePath(opts.backupConfig.local_dir);
  mkdirSync(localDir, { recursive: true });

  const entries = collectExistingEntries({
    dataDir: opts.dataDir,
    backupConfig: opts.backupConfig,
  });

  if (entries.length === 0) {
    throw new Error(`No backup inputs found under ${opts.dataDir}`);
  }

  const fileName = `flynn_${fileTimestamp(now)}.tar.gz`;
  const archivePath = join(localDir, fileName);
  await execFileAsync('tar', ['-czf', archivePath, '-C', opts.dataDir, ...entries]);

  auditLogger?.systemConfig('backup', 'snapshot', {
    archive_path: archivePath,
    entry_count: entries.length,
  });

  if (!opts.backupConfig.minio.enabled) {
    return {
      archivePath,
      fileName,
      uploaded: false,
    };
  }

  const endpoint = opts.backupConfig.minio.endpoint;
  const accessKey = opts.backupConfig.minio.access_key;
  const secretKey = opts.backupConfig.minio.secret_key;
  const bucket = opts.backupConfig.minio.bucket;

  if (!endpoint || !accessKey || !secretKey || !bucket) {
    throw new Error('backup.minio.enabled=true requires endpoint, access_key, secret_key, and bucket');
  }

  const alias = 'flynnbackup';
  const host = buildMinioHost({
    endpoint,
    accessKey,
    secretKey,
    secure: opts.backupConfig.minio.secure,
  });
  const env = {
    ...process.env,
    [`MC_HOST_${alias}`]: host,
  };
  const mcPath = resolveMcPath(opts.backupConfig.minio.mc_path);

  let remotePath = '';
  try {
    await execFileAsync(mcPath, ['mb', '--ignore-existing', `${alias}/${bucket}`], { env });

    const objectKey = buildObjectKey(opts.backupConfig.minio.prefix, fileName);
    remotePath = `${alias}/${bucket}/${objectKey}`;
    await execFileAsync(mcPath, ['cp', archivePath, remotePath], { env });
  } catch (error) {
    throw new Error(formatMinioCliError(error, mcPath));
  }

  auditLogger?.systemConfig('backup', 'upload', {
    archive_path: archivePath,
    remote_path: remotePath,
    endpoint,
    bucket,
  });

  return {
    archivePath,
    fileName,
    uploaded: true,
    remotePath,
  };
}

export const backupInternals = {
  buildMinioHost,
  buildObjectKey,
  collectExistingEntries,
  expandHomePath,
  formatMinioCliError,
  fileTimestamp,
  resolveMcPath,
};
