import { createWriteStream, promises as fs } from "node:fs";
import http from "node:http";
import https from "node:https";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { Utils } from "electrobun/bun";
import { launchMacInstaller } from "./macappupdate.js";
import { launchWindowsInstaller } from "./winappupdate.js";
import { scheduleAppRestart } from "./updater-runner.js";

const SUPPORTED_TARGETS = {
  darwin: {
    extension: ".pkg",
    osMatchers: ["darwin", "mac", "macos", "osx"],
  },
  win32: {
    extension: ".msi",
    osMatchers: ["win32", "win", "windows"],
  },
};

let runningUpdate = null;

const getArtifactName = (artifact) =>
  String(artifact?.fileName || artifact?.relativePath || artifact?.url || "");

const normalizeArch = (arch) => {
  if (arch === "x64" || arch === "amd64") return "x64";
  if (arch === "arm64" || arch === "aarch64") return "arm64";
  return arch;
};

const artifactMatchesPlatform = (artifact, target, platform, arch) => {
  const name = getArtifactName(artifact).toLowerCase();
  const normalizedArch = normalizeArch(arch);
  const artifactArch = normalizeArch(String(artifact?.arch || "").toLowerCase());
  const artifactPlatform = String(
    artifact?.platform || artifact?.os || artifact?.target || "",
  ).toLowerCase();

  if (!name.endsWith(target.extension)) return false;
  if (!artifact?.url) return false;

  const matchesArch =
    artifactArch === normalizedArch ||
    name.includes(`-${normalizedArch}`) ||
    name.includes(`_${normalizedArch}`) ||
    name.includes(`.${normalizedArch}.`) ||
    name.includes(normalizedArch);

  const matchesOs =
    !artifactPlatform ||
    target.osMatchers.includes(artifactPlatform) ||
    target.osMatchers.some((matcher) => name.includes(matcher)) ||
    (platform === "darwin" && name.includes("mac")) ||
    (platform === "win32" && name.includes("win"));

  return matchesArch && matchesOs;
};

const selectUpdateArtifact = (
  update,
  platform = process.platform,
  arch = process.arch,
) => {
  const target = SUPPORTED_TARGETS[platform];
  if (!target) {
    throw new Error(`App updates are not supported on ${platform}.`);
  }

  const artifacts = Array.isArray(update?.artifacts) ? update.artifacts : [];
  const matchingArtifact = artifacts.find((artifact) =>
    artifactMatchesPlatform(artifact, target, platform, arch),
  );
  const fallbackArtifact = artifacts.find((artifact) => {
    const name = getArtifactName(artifact).toLowerCase();
    return artifact?.url && name.endsWith(target.extension);
  });

  if (!matchingArtifact && !fallbackArtifact) {
    throw new Error(
      `No ${target.extension} update artifact found for ${platform}/${arch}.`,
    );
  }

  return matchingArtifact || fallbackArtifact;
};

const getInstallErrorMessage = (error, output = "") => {
  const combined = `${output}\n${error?.message || ""}`.trim();

  if (
    /cancel/i.test(combined) ||
    /did not grant permission/i.test(combined) ||
    /user canceled/i.test(combined)
  ) {
    return "Update installation was cancelled.";
  }

  if (/incorrect/i.test(combined)) {
    return "The administrator password was incorrect.";
  }

  if (
    /1625/.test(combined) ||
    /forbidden by system policy/i.test(combined) ||
    /Non-assigned apps are disabled/i.test(combined)
  ) {
    return "Update installation was blocked by Windows Installer policy.";
  }

  return combined || "Failed to install update.";
};

const getDownloadUrl = (url, redirectCount = 0) =>
  new Promise((resolve, reject) => {
    if (redirectCount > 5) {
      reject(new Error("Too many redirects while downloading update."));
      return;
    }

    const parsedUrl = new URL(url);
    const client = parsedUrl.protocol === "https:" ? https : http;
    const request = client.get(parsedUrl, (response) => {
      const location = response.headers.location;

      if (response.statusCode >= 300 && response.statusCode < 400 && location) {
        response.resume();
        resolve(
          getDownloadUrl(
            new URL(location, parsedUrl).toString(),
            redirectCount + 1,
          ),
        );
        return;
      }

      resolve({ response, url: parsedUrl.toString() });
    });

    request.on("error", reject);
  });

const downloadArtifact = async (artifact, destinationPath, sendProgress) => {
  const { response } = await getDownloadUrl(artifact.url);

  if (response.statusCode < 200 || response.statusCode >= 300) {
    response.resume();
    throw new Error(`Update download failed with HTTP ${response.statusCode}.`);
  }

  const totalBytes =
    Number.parseInt(response.headers["content-length"], 10) || 0;
  let downloadedBytes = 0;

  await new Promise((resolve, reject) => {
    const output = createWriteStream(destinationPath);

    response.on("data", (chunk) => {
      downloadedBytes += chunk.length;
      const percent = totalBytes
        ? Math.round((downloadedBytes / totalBytes) * 100)
        : null;

      sendProgress({
        phase: "downloading",
        percent,
        downloadedBytes,
        totalBytes,
        message: totalBytes
          ? `Downloading update (${percent}%)`
          : "Downloading update",
      });
    });

    response.on("error", reject);
    output.on("error", reject);
    output.on("finish", resolve);
    response.pipe(output);
  });
};

const launchInstallerAndRestart = async (
  mainWindow,
  installerPath,
  sendProgress,
) => {
  const installerHelpers = { sendProgress, getInstallErrorMessage };

  if (process.platform === "darwin") {
    await launchMacInstaller(
      mainWindow,
      installerPath,
      sendProgress,
      installerHelpers,
    );
  } else if (process.platform === "win32") {
    await launchWindowsInstaller(
      mainWindow,
      installerPath,
      sendProgress,
      installerHelpers,
    );
  } else {
    throw new Error(`App updates are not supported on ${process.platform}.`);
  }

  scheduleAppRestart();
  Utils.quit();
};

const runAppUpdate = async (mainWindow, update, sendProgress) => {
  const artifact = selectUpdateArtifact(update);
  const tempDirectory = await fs.mkdtemp(
    path.join(os.tmpdir(), "farmcontrol-update-"),
  );
  const artifactName = path.basename(getArtifactName(artifact));
  const installerPath = path.join(tempDirectory, artifactName);

  sendProgress({
    phase: "preparing",
    percent: 0,
    artifact,
    message: "Preparing update download",
  });

  await downloadArtifact(artifact, installerPath, sendProgress);

  sendProgress({
    phase: "downloaded",
    percent: 100,
    downloadedBytes: null,
    totalBytes: null,
    artifact,
    message: "Update downloaded",
  });

  await launchInstallerAndRestart(mainWindow, installerPath, sendProgress);
};

export function startAppUpdate(mainWindow, update, sendProgress) {
  if (runningUpdate) return runningUpdate;

  runningUpdate = runAppUpdate(mainWindow, update, sendProgress)
    .then(() => ({ ok: true }))
    .catch((error) => {
      sendProgress({
        phase: "error",
        percent: null,
        message: error?.message || "Failed to update app.",
      });
      throw error;
    })
    .finally(() => {
      runningUpdate = null;
    });

  return runningUpdate;
}
