"use strict";

const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

const { CHECKSUM_DISPLAY_LENGTH } = require("./constants");
const { logError, log } = require("./logging");

/**
 * Atomic write for settings.json: write to .tmp, then rename.
 * Prevents corruption if the process is interrupted mid-write.
 */
function writeSettingsAtomic(settingsPath, data) {
  const content =
    typeof data === "string" ? data : JSON.stringify(data, null, 2);
  const tmp = settingsPath + ".tmp";
  fs.writeFileSync(tmp, content, "utf-8");
  fs.renameSync(tmp, settingsPath);
}

function computeFileChecksum(filePath) {
  const raw = fs.readFileSync(filePath);
  const buf = Buffer.alloc(raw.length);
  let j = 0;
  for (let i = 0; i < raw.length; i++) {
    if (raw[i] === 0x0d && raw[i + 1] === 0x0a) continue;
    buf[j++] = raw[i];
  }
  return crypto
    .createHash("sha256")
    .update(buf.subarray(0, j))
    .digest("hex")
    .slice(0, CHECKSUM_DISPLAY_LENGTH);
}

function computeDirChecksums(dir, prefix = "") {
  const checksums = new Map();
  if (!fs.existsSync(dir)) return checksums;
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name);
    const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
    // Skip symlinks to avoid circular references
    const stat = fs.lstatSync(fullPath);
    if (stat.isSymbolicLink()) continue;
    if (entry.isDirectory()) {
      const sub = computeDirChecksums(fullPath, relPath);
      for (const [k, v] of sub) {
        checksums.set(k, v);
      }
    } else {
      checksums.set(relPath, computeFileChecksum(fullPath));
    }
  }
  return checksums;
}

function isBinaryFile(filePath) {
  const buf = Buffer.alloc(512);
  let fd;
  try {
    fd = fs.openSync(filePath, "r");
    const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
    for (let i = 0; i < bytesRead; i++) {
      if (buf[i] === 0) return true;
    }
    return false;
  } finally {
    if (fd !== undefined) fs.closeSync(fd);
  }
}

/**
 * Recursively copy a directory, preserving structure.
 * Returns { copied: number, updated: number, skipped: number, failed: number, modified: string[] }
 *
 * Options:
 *   overwrite          — unconditionally overwrite existing files (counted as `copied`)
 *   overwriteIfChanged — compare checksums; overwrite only when content differs (counted as `updated`)
 *   detectModified     — track modified files without overwriting (legacy)
 *   dryRun             — count operations without writing
 */
function copyDir(src, dest, options = {}) {
  const {
    overwrite = false,
    overwriteIfChanged = false,
    dryRun = false,
    detectModified = false,
  } = options;
  let copied = 0;
  let updated = 0;
  let skipped = 0;
  let failed = 0;
  const modified = [];

  if (!fs.existsSync(src)) {
    throw new Error(`Source directory not found: ${src}`);
  }

  if (!dryRun) {
    fs.mkdirSync(dest, { recursive: true });
  }

  const entries = fs.readdirSync(src, { withFileTypes: true });

  for (const entry of entries) {
    const srcPath = path.join(src, entry.name);
    const destPath = path.join(dest, entry.name);

    // Skip symlinks to prevent following links outside the source tree
    const srcStat = fs.lstatSync(srcPath);
    if (srcStat.isSymbolicLink()) {
      skipped++;
      continue;
    }

    if (entry.isDirectory()) {
      const result = copyDir(srcPath, destPath, options);
      copied += result.copied;
      updated += result.updated;
      skipped += result.skipped;
      failed += result.failed;
      if (detectModified) modified.push(...result.modified);
    } else {
      if (fs.existsSync(destPath) && !overwrite) {
        if (overwriteIfChanged) {
          const srcChecksum = computeFileChecksum(srcPath);
          const destChecksum = computeFileChecksum(destPath);
          if (srcChecksum !== destChecksum) {
            if (!dryRun) {
              fs.mkdirSync(path.dirname(destPath), { recursive: true });
              try {
                fs.copyFileSync(srcPath, destPath);
                // Preserve executable bits from source
                const srcMode = srcStat.mode;
                if (srcMode & 0o111) {
                  fs.chmodSync(destPath, srcMode);
                }
              } catch (err) {
                if (err.code === "EACCES") {
                  logError(`Permission denied: ${destPath}`);
                  log(
                    "  Check file permissions or run with appropriate access.",
                  );
                  failed++;
                  continue;
                }
                throw err;
              }
            }
            updated++;
          } else {
            skipped++;
          }
        } else {
          if (detectModified) {
            const srcChecksum = computeFileChecksum(srcPath);
            const destChecksum = computeFileChecksum(destPath);
            if (srcChecksum !== destChecksum) {
              modified.push(destPath);
            }
          }
          skipped++;
        }
      } else {
        if (!dryRun) {
          fs.mkdirSync(path.dirname(destPath), { recursive: true });
          try {
            fs.copyFileSync(srcPath, destPath);
            // Preserve executable bits from source
            const srcMode = srcStat.mode;
            if (srcMode & 0o111) {
              fs.chmodSync(destPath, srcMode);
            }
          } catch (err) {
            if (err.code === "EACCES") {
              logError(`Permission denied: ${destPath}`);
              log("  Check file permissions or run with appropriate access.");
              failed++;
              continue;
            }
            throw err;
          }
        }
        copied++;
      }
    }
  }

  return { copied, updated, skipped, failed, modified };
}

/**
 * Remove evolv-* skill directories from destDir that no longer exist in srcDir.
 * Only touches directories matching the given prefix — won't affect user-installed skills.
 * Returns { removed: number, removedDirs: string[] }
 */
function cleanupDeprecatedSkills(srcDir, destDir, options = {}) {
  const { prefix = "evolv-", dryRun = false } = options;
  let removed = 0;
  const removedDirs = [];

  if (!fs.existsSync(destDir)) {
    return { removed, removedDirs };
  }

  // Build set of valid skill names from source (e.g. 'evolv-new-project')
  const validNames = new Set();
  if (fs.existsSync(srcDir)) {
    const srcEntries = fs.readdirSync(srcDir, { withFileTypes: true });
    for (const entry of srcEntries) {
      if (entry.isDirectory()) {
        validNames.add(prefix + entry.name);
      }
    }
  }

  // Scan dest for prefix-matching dirs not in valid set
  const destEntries = fs.readdirSync(destDir, { withFileTypes: true });
  for (const entry of destEntries) {
    if (
      entry.isDirectory() &&
      entry.name.startsWith(prefix) &&
      !validNames.has(entry.name)
    ) {
      const staleDir = path.join(destDir, entry.name);
      if (!dryRun) {
        fs.rmSync(staleDir, { recursive: true, force: true });
      }
      removedDirs.push(entry.name);
      removed++;
    }
  }

  return { removed, removedDirs };
}

/**
 * Count files recursively in a directory.
 */
function countFiles(dir) {
  if (!fs.existsSync(dir)) return 0;
  let count = 0;
  const entries = fs.readdirSync(dir, { withFileTypes: true });
  for (const entry of entries) {
    if (entry.isDirectory()) {
      count += countFiles(path.join(dir, entry.name));
    } else {
      count++;
    }
  }
  return count;
}

module.exports = {
  writeSettingsAtomic,
  computeFileChecksum,
  computeDirChecksums,
  isBinaryFile,
  copyDir,
  cleanupDeprecatedSkills,
  countFiles,
};
