import { createHash } from "node:crypto";
import { copyFile, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
import { dirname, join, posix } from "node:path";
import { fileURLToPath } from "node:url";

const FILES_DIRECTORY = dirname(fileURLToPath(import.meta.url));
const ROOT_DIRECTORY = dirname(FILES_DIRECTORY);
const UPDATE_DIRECTORY = join(FILES_DIRECTORY, "tmp", "aktualizacja");
const PENDING_DIRECTORY = join(UPDATE_DIRECTORY, "oczekujaca");
const STATE_FILE = join(PENDING_DIRECTORY, "aktualizacja.json");
const STAGED_FILES_DIRECTORY = join(PENDING_DIRECTORY, "pliki");
const VERSION_FILE = join(FILES_DIRECTORY, "wersja-aplikacji.json");
const PACKAGE_FILE = join(FILES_DIRECTORY, "package.json");
const PACKAGE_LOCK_FILE = join(FILES_DIRECTORY, "package-lock.json");
const SHA256_PATTERN = /^[a-f0-9]{64}$/i;
const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
const ALLOWED_FILES = new Set([
  "start.htm",
  "pliki/mrmuaiqdpzjowpihiz.js",
  "pliki/kkpybpmhonxogbwetgp.js",
  "pliki/wwpfromllxxaibsmcggeilaesdhsxnfpjbkdhvfzn.js",
  "pliki/rsoboqwptvhomquiafjt.js",
  "pliki/vrjeksrdppugqlwzhfab.js",
  "pliki/xrhckdwhwcdxzqgbexgi.js",
  "pliki/pbvcrbtheayxdibsiziopspjvliemamswmymzletxoiriwob.js",
  "pliki/vhwsorwqtxdyzietldhegbsptp.css",
  "pliki/ycsqctiwsleqkdjoimum.js",
  "pliki/wypowiedzenieocdruk.pdf",
  "pliki/wypowiedzenieautomat.pdf",
  "pliki/wypowiedzenienabywcy.pdf",
  "pliki/serwer.mjs",
  "pliki/INSTRUKCJA-AUTOMATYZACJI.txt",
  "pliki/INSTRUKCJA-AKTUALIZACJI.txt",
  "pliki/Uruchom Baza Polis.cmd",
  "pliki/aktualizator.mjs",
]);




const ALLOWED_DIRECTORIES = [
  "pliki/assets/",
  "pliki/pwdjhucdpxshwlujddqk/",
  "pliki/fonts/",
  "pliki/narzedzia/",
  "pliki/vzhgqyjhppnpobjprzyl/",
  "pliki/llopfcklkccjjlikfqjj/",
  "pliki/tests/",
  "pliki/tools/",
  "pliki/vendor/",
  "trasy-towarzystw/",
];


const INSTALLED_PATH_OVERRIDES = new Map([
  ["start.htm", "pliki/start.htm"],
  ["pliki/Uruchom Baza Polis.cmd", "Uruchom Baza Polis.cmd"],
]);
const INSTALLED_DIRECTORY_OVERRIDES = new Map([
  ["trasy-towarzystw/", "pliki/llopfcklkccjjlikfqjj/"],
]);

function resolveInstalledPath(updatePath) {
  const exactOverride = INSTALLED_PATH_OVERRIDES.get(updatePath);
  if (exactOverride) return exactOverride;
  for (const [prefix, replacement] of INSTALLED_DIRECTORY_OVERRIDES) {
    if (updatePath.startsWith(prefix)) return replacement + updatePath.slice(prefix.length);
  }
  return updatePath;
}

function normalizeUpdatePath(value) {
  const rawPath = String(value || "").trim().replaceAll("\\", "/");
  const normalizedPath = posix.normalize(rawPath);
  if (
    !rawPath
    || rawPath.includes("\0")
    || rawPath.startsWith("/")
    || /^[a-z]:/i.test(rawPath)
    || normalizedPath === "."
    || normalizedPath === ".."
    || normalizedPath.startsWith("../")
    || normalizedPath !== rawPath
  ) {
    throw new Error(`Nieprawidłowa ścieżka w oczekującej aktualizacji: ${rawPath || "(pusta)"}`);
  }
  if (!ALLOWED_FILES.has(normalizedPath) && !ALLOWED_DIRECTORIES.some((directory) => normalizedPath.startsWith(directory))) {
    throw new Error(`Aktualizacja próbuje zastąpić chroniony plik: ${normalizedPath}`);
  }
  return normalizedPath;
}

async function fileExists(filePath) {
  try {
    return (await stat(filePath)).isFile();
  } catch {
    return false;
  }
}

async function readPendingState() {
  if (!(await fileExists(STATE_FILE))) return null;
  let state;
  try {
    state = JSON.parse(await readFile(STATE_FILE, "utf8"));
  } catch {
    throw new Error("Nie udało się odczytać przygotowanej aktualizacji.");
  }
  if (state?.formatVersion !== 1 || !VERSION_PATTERN.test(String(state?.version || "")) || !Array.isArray(state?.files) || state.files.length === 0) {
    throw new Error("Przygotowana aktualizacja ma nieprawidłowy format.");
  }
  const paths = new Set();
  const files = state.files.map((entry) => {
    const path = normalizeUpdatePath(entry?.path);
    const pathKey = path.toLocaleLowerCase("pl");
    if (paths.has(pathKey)) throw new Error(`Plik występuje w aktualizacji więcej niż raz: ${path}`);
    paths.add(pathKey);
    const sha256 = String(entry?.sha256 || "").toLowerCase();
    if (!SHA256_PATTERN.test(sha256)) throw new Error(`Nieprawidłowa suma SHA-256 pliku: ${path}`);
    return { path, sha256 };
  });
  return { version: state.version, files };
}

async function verifyStagedFiles(state) {
  for (const file of state.files) {
    const stagedPath = join(STAGED_FILES_DIRECTORY, ...file.path.split("/"));
    const contents = await readFile(stagedPath);
    const checksum = createHash("sha256").update(contents).digest("hex");
    if (checksum !== file.sha256) throw new Error(`Plik ${file.path} został uszkodzony po pobraniu.`);
  }
}

async function writeFileAtomically(filePath, contents) {
  const temporaryPath = `${filePath}.${process.pid}-${Date.now()}.tmp`;
  try {
    await writeFile(temporaryPath, contents, "utf8");
    await rename(temporaryPath, filePath);
  } catch (error) {
    await rm(temporaryPath, { force: true }).catch(() => {});
    throw error;
  }
}

function updatePackageVersion(contents, version, { lockFile = false } = {}) {
  let document;
  try {
    document = JSON.parse(contents);
  } catch {
    throw new Error(lockFile
      ? "Plik package-lock.json jest uszkodzony."
      : "Plik package.json jest uszkodzony.");
  }
  if (!document || typeof document !== "object" || Array.isArray(document)) {
    throw new Error(lockFile
      ? "Plik package-lock.json ma nieprawidłowy format."
      : "Plik package.json ma nieprawidłowy format.");
  }
  document.version = version;
  if (lockFile) {
    if (!document.packages?.[""] || typeof document.packages[""] !== "object") {
      throw new Error("Plik package-lock.json nie zawiera metadanych głównego pakietu.");
    }
    document.packages[""].version = version;
  }
  return `${JSON.stringify(document, null, 2)}\n`;
}

async function synchronizePackageVersions(version) {
  const packageContents = await readFile(PACKAGE_FILE, "utf8").catch(() => null);
  const packageLockContents = await readFile(PACKAGE_LOCK_FILE, "utf8").catch(() => null);
  if (packageContents !== null) {
    const nextPackageContents = updatePackageVersion(packageContents, version);
    if (nextPackageContents !== packageContents) {
      await writeFileAtomically(PACKAGE_FILE, nextPackageContents);
    }
  }
  if (packageLockContents !== null) {
    const nextPackageLockContents = updatePackageVersion(packageLockContents, version, { lockFile: true });
    if (nextPackageLockContents !== packageLockContents) {
      await writeFileAtomically(PACKAGE_LOCK_FILE, nextPackageLockContents);
    }
  }
}

async function applyPendingUpdate(state) {
  const backupName = `${state.version}-${new Date().toISOString().replace(/[:.]/g, "-")}`;
  const backupDirectory = join(UPDATE_DIRECTORY, "kopie", backupName);
  const appliedFiles = [];
  const previousVersionContents = await readFile(VERSION_FILE).catch(() => null);
  const previousPackageContents = await readFile(PACKAGE_FILE, "utf8").catch(() => null);
  const previousPackageLockContents = await readFile(PACKAGE_LOCK_FILE, "utf8").catch(() => null);
  await mkdir(backupDirectory, { recursive: true });
  try {
    for (const file of state.files) {
      const pathParts = file.path.split("/");
      const installedParts = resolveInstalledPath(file.path).split("/");
      const stagedPath = join(STAGED_FILES_DIRECTORY, ...pathParts);
      const targetPath = join(ROOT_DIRECTORY, ...installedParts);
      const backupPath = join(backupDirectory, ...pathParts);
      const targetExisted = await fileExists(targetPath);
      if (targetExisted) {
        await mkdir(dirname(backupPath), { recursive: true });
        await copyFile(targetPath, backupPath);
      }
      await mkdir(dirname(targetPath), { recursive: true });
      appliedFiles.push({ targetPath, backupPath, targetExisted });
      await copyFile(stagedPath, targetPath);
    }
    if (previousPackageContents !== null) {
      await writeFileAtomically(PACKAGE_FILE, updatePackageVersion(previousPackageContents, state.version));
    }
    if (previousPackageLockContents !== null) {
      await writeFileAtomically(
        PACKAGE_LOCK_FILE,
        updatePackageVersion(previousPackageLockContents, state.version, { lockFile: true }),
      );
    }
    await writeFileAtomically(VERSION_FILE, `${JSON.stringify({ version: state.version }, null, 2)}\n`);
    await writeFile(
      join(UPDATE_DIRECTORY, "ostatnia-aktualizacja.json"),
      `${JSON.stringify({ version: state.version, installedAt: new Date().toISOString(), backupDirectory }, null, 2)}\n`,
      "utf8",
    );
    await rm(PENDING_DIRECTORY, { recursive: true, force: true });
    console.log(`Zainstalowano aktualizację Bazy Polis ${state.version}.`);
  } catch (error) {
    for (const applied of appliedFiles.reverse()) {
      try {
        if (applied.targetExisted) await copyFile(applied.backupPath, applied.targetPath);
        else await rm(applied.targetPath, { force: true });
      } catch {
      }
    }
    try {
      if (previousVersionContents === null) await rm(VERSION_FILE, { force: true });
      else await writeFileAtomically(VERSION_FILE, previousVersionContents);
    } catch {
    }
    try {
      if (previousPackageContents !== null) await writeFileAtomically(PACKAGE_FILE, previousPackageContents);
    } catch {
    }
    try {
      if (previousPackageLockContents !== null) {
        await writeFileAtomically(PACKAGE_LOCK_FILE, previousPackageLockContents);
      }
    } catch {
    }
    throw error;
  }
}

try {
  const pendingState = await readPendingState();
  if (pendingState) {
    console.log(`Instalowanie aktualizacji Bazy Polis ${pendingState.version}...`);
    await verifyStagedFiles(pendingState);
    await applyPendingUpdate(pendingState);
  } else {
    
    
    try {
      const installedVersion = String(JSON.parse(await readFile(VERSION_FILE, "utf8"))?.version || "");
      if (VERSION_PATTERN.test(installedVersion)) await synchronizePackageVersions(installedVersion);
    } catch (error) {
      
      
      console.warn(`Nie udało się zsynchronizować metadanych wersji: ${error?.message || error}`);
    }
  }
} catch (error) {
  console.error(`Nie udało się zainstalować aktualizacji: ${error?.message || error}`);
  
  
  
  
  
  try {
    await rm(PENDING_DIRECTORY, { recursive: true, force: true });
    console.error("Niekompletna aktualizacja została usunięta. Program uruchomi się w dotychczasowej wersji, a nowa wersja pobierze się ponownie.");
  } catch (cleanupError) {
    console.error(`Nie udało się usunąć niekompletnej aktualizacji: ${cleanupError?.message || cleanupError}`);
  }
}
