/**
 * ZIP helpers for project import/export.
 * Prefer system zip/unzip for full-fidelity binary handling, with a lightweight
 * JS fallback for simple text-only imports.
 */
import fs from 'fs/promises';
import fsSync from 'fs';
import os from 'os';
import path from 'path';
import { execFile } from 'child_process';
import { promisify } from 'util';
import { inflateRawSync } from 'zlib';

const execFileAsync = promisify(execFile);
const VL_ROOT_DIRS = new Set(['Apps', 'Sections', 'ExtComponents', 'Services', 'Database', 'Theme', 'Process', 'Config', '.vl-code']);

/**
 * Extract files from a ZIP buffer.
 * Fallback parser for standard ZIP files (stored + deflate only).
 * Returns utf-8 text content, so this should only be used when system unzip is unavailable.
 */
export function extractZip(zipBuffer, allowedExts = []) {
  const files = [];
  let offset = 0;

  while (offset < zipBuffer.length - 4) {
    if (zipBuffer.readUInt32LE(offset) !== 0x04034b50) break;

    const compressionMethod = zipBuffer.readUInt16LE(offset + 8);
    const compressedSize = zipBuffer.readUInt32LE(offset + 18);
    const fileNameLen = zipBuffer.readUInt16LE(offset + 26);
    const extraLen = zipBuffer.readUInt16LE(offset + 28);

    const fileNameBuf = zipBuffer.subarray(offset + 30, offset + 30 + fileNameLen);
    const fileName = fileNameBuf.toString('utf-8');
    const dataStart = offset + 30 + fileNameLen + extraLen;
    const dataEnd = dataStart + compressedSize;

    const isDir = fileName.endsWith('/');
    const isMacJunk = fileName.startsWith('__MACOSX/') || fileName.includes('/.DS_Store');
    if (!isDir && !isMacJunk) {
      const ext = '.' + fileName.split('.').pop().toLowerCase();
      const passFilter = allowedExts.length === 0 || allowedExts.includes(ext);

      if (passFilter) {
        try {
          const compressedData = zipBuffer.subarray(dataStart, dataEnd);
          let content;
          if (compressionMethod === 0) {
            content = compressedData.toString('utf-8');
          } else if (compressionMethod === 8) {
            content = inflateRawSync(compressedData).toString('utf-8');
          } else {
            offset = dataEnd;
            continue;
          }

          const filePath = normalizeImportedPath(fileName);
          if (filePath) files.push({ path: filePath, content });
        } catch {
          // Skip entries that fail to decompress.
        }
      }
    }

    offset = dataEnd;
  }

  return files;
}

export async function extractZipToDir(zipPath, targetDir) {
  await fs.mkdir(targetDir, { recursive: true });

  try {
    const tmpExtractDir = await fs.mkdtemp(path.join(os.tmpdir(), 'vlcode-unzip-'));
    try {
      await execFileAsync('unzip', ['-oq', zipPath, '-d', tmpExtractDir]);
      const importRoot = await detectImportRoot(tmpExtractDir);
      await copyTree(importRoot, targetDir);
      return;
    } finally {
      await fs.rm(tmpExtractDir, { recursive: true, force: true }).catch(() => {});
    }
  } catch {
    const zipBuffer = await fs.readFile(zipPath);
    const files = extractZip(zipBuffer);
    for (const file of files) {
      const fullPath = path.join(targetDir, file.path);
      await fs.mkdir(path.dirname(fullPath), { recursive: true });
      await fs.writeFile(fullPath, file.content, 'utf-8');
    }
  }
}

export async function createZipFromDir(sourceDir) {
  const tmpZip = path.join(os.tmpdir(), `vlcode-export-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`);
  try {
    await execFileAsync(
      'zip',
      ['-qr', tmpZip, '.', '-x', 'node_modules/*', '.git/*', '__vl_compile.zip'],
      { cwd: sourceDir }
    );
    return await fs.readFile(tmpZip);
  } finally {
    await fs.rm(tmpZip, { force: true }).catch(() => {});
  }
}

function normalizeImportedPath(filePath) {
  let normalized = filePath.replace(/\\/g, '/');
  normalized = normalized.replace(/^\/+/, '');
  normalized = normalized.replace(/^\.\//, '');

  const parts = normalized.split('/').filter(Boolean);
  while (parts.length > 1 && !VL_ROOT_DIRS.has(parts[0]) && VL_ROOT_DIRS.has(parts[1])) {
    parts.shift();
  }

  const safePath = path.posix.normalize(parts.join('/'));
  if (!safePath || safePath === '.' || safePath.startsWith('../')) return null;
  return safePath;
}

async function detectImportRoot(extractDir) {
  let currentDir = extractDir;

  for (;;) {
    const entries = await fs.readdir(currentDir, { withFileTypes: true });
    const visible = entries.filter((entry) => entry.name !== '__MACOSX' && entry.name !== '.DS_Store');
    const names = visible.map((entry) => entry.name);

    if (names.some((name) => VL_ROOT_DIRS.has(name))) {
      return currentDir;
    }

    const subdirs = visible.filter((entry) => entry.isDirectory());
    if (subdirs.length === 1 && visible.length === 1) {
      currentDir = path.join(currentDir, subdirs[0].name);
      continue;
    }

    return currentDir;
  }
}

async function copyTree(sourceDir, targetDir) {
  const entries = await fs.readdir(sourceDir, { withFileTypes: true });
  for (const entry of entries) {
    if (entry.name === '__MACOSX' || entry.name === '.DS_Store') continue;

    const sourcePath = path.join(sourceDir, entry.name);
    const targetPath = path.join(targetDir, entry.name);
    if (entry.isDirectory()) {
      await fs.mkdir(targetPath, { recursive: true });
      await copyTree(sourcePath, targetPath);
      continue;
    }

    const parentDir = path.dirname(targetPath);
    if (!fsSync.existsSync(parentDir)) {
      await fs.mkdir(parentDir, { recursive: true });
    }
    await fs.copyFile(sourcePath, targetPath);
  }
}
