#!/usr/bin/env node
"use strict";

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

const BINARY_NAME = "opencodereview";

const packageRoot = path.join(__dirname, "..");
const binDir = path.join(packageRoot, "bin");
const binaryDest = path.join(binDir, BINARY_NAME);

function info(msg) {
  console.log(`[INFO]  ${msg}`);
}

function warn(msg) {
  console.warn(`[WARN]  ${msg}`);
}

function error(msg) {
  console.error(`[ERROR] ${msg}`);
}

function detectPlatform() {
  const os = process.platform;
  let arch = process.arch;

  switch (arch) {
    case "x64":
      arch = "amd64";
      break;
    case "arm64":
      arch = "arm64";
      break;
    default:
      throw new Error(
        `Unsupported architecture: ${arch}. Supported: amd64 (x64), arm64`
      );
  }

  if (os !== "linux" && os !== "darwin") {
    throw new Error(
      `Unsupported operating system: ${os}. Supported: linux, darwin`
    );
  }

  return { os, arch };
}

function loadPackageJson() {
  const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
  if (!pkg.version) {
    throw new Error("Missing version field in package.json");
  }
  if (!pkg.ocrConfig || !pkg.ocrConfig.urlPattern) {
    throw new Error("Missing ocrConfig.urlPattern in package.json");
  }
  return pkg;
}

function resolveVersion(pkg) {
  const envVersion = process.env.OCR_VERSION;
  if (envVersion) {
    const v = envVersion.startsWith("v") ? envVersion.slice(1) : envVersion;
    info(`Using pinned version from OCR_VERSION: ${v}`);
    return v;
  }

  info(`Using version from package.json: ${pkg.version}`);
  return pkg.version;
}

function buildUrl(pattern, vars) {
  return pattern
    .replace(/\{version\}/g, vars.version)
    .replace(/\{os\}/g, vars.os)
    .replace(/\{arch\}/g, vars.arch);
}

function download(url, maxRedirects = 10) {
  if (!url.startsWith("https")) {
    return Promise.reject(new Error(`Refusing non-HTTPS download: ${url}`));
  }
  if (maxRedirects <= 0) {
    return Promise.reject(new Error(`Too many redirects fetching ${url}`));
  }
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
        res.resume();
        download(res.headers.location, maxRedirects - 1).then(resolve).catch(reject);
        return;
      }
      if (res.statusCode !== 200) {
        res.resume();
        reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
        return;
      }
      resolve(res);
    }).on("error", reject);
  });
}

async function downloadText(url) {
  const res = await download(url);
  return new Promise((resolve, reject) => {
    let data = "";
    res.on("data", (chunk) => (data += chunk));
    res.on("end", () => resolve(data));
    res.on("error", reject);
  });
}

async function downloadBinary(url, destPath) {
  const res = await download(url);
  return new Promise((resolve, reject) => {
    const fileStream = fs.createWriteStream(destPath);
    res.on("error", (err) => {
      fileStream.destroy();
      fs.unlink(destPath, () => {});
      reject(err);
    });
    res.pipe(fileStream);
    fileStream.on("finish", () => fileStream.close(() => resolve()));
    fileStream.on("error", (err) => {
      fs.unlink(destPath, () => {});
      reject(err);
    });
  });
}

function computeChecksum(filePath) {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash("sha256");
    const stream = fs.createReadStream(filePath);
    stream.on("data", (chunk) => hash.update(chunk));
    stream.on("end", () => resolve(hash.digest("hex")));
    stream.on("error", reject);
  });
}

async function main() {
  info("OpenCodeReview Installer");
  info("=========================");

  const { os, arch } = detectPlatform();
  info(`Detected platform: ${os}/${arch}`);

  const pkg = loadPackageJson();
  const version = resolveVersion(pkg);
  const config = pkg.ocrConfig;

  if (!fs.existsSync(binDir)) {
    fs.mkdirSync(binDir, { recursive: true });
  }

  const jsWrapper = path.join(binDir, "ocr.js");
  if (fs.existsSync(jsWrapper)) {
    try {
      fs.chmodSync(jsWrapper, 0o755);
    } catch (e) {
      warn(`Could not make ocr.js executable: ${e.message}`);
    }
  }

  const vars = { version, os, arch };
  const downloadUrl = buildUrl(config.urlPattern, vars);
  info(`Downloading ${downloadUrl} ...`);

  await downloadBinary(downloadUrl, binaryDest);
  fs.chmodSync(binaryDest, 0o755);

  if (config.checksumPattern) {
    try {
      const checksumUrl = buildUrl(config.checksumPattern, vars);
      info("Verifying checksum...");
      const shaContent = await downloadText(checksumUrl);
      const actualSha = await computeChecksum(binaryDest);

      let verified = false;
      for (const line of shaContent.split("\n")) {
        const trimmed = line.trim();
        if (trimmed.includes(`-${os}-${arch}`)) {
          const expectedSha = trimmed.split(/\s+/)[0].toLowerCase();
          if (expectedSha) {
            if (actualSha !== expectedSha) {
              throw new Error(
                `Checksum mismatch! Expected: ${expectedSha}, Got: ${actualSha}`
              );
            }
            info("Checksum verified.");
            verified = true;
            break;
          }
        }
      }
      if (!verified) {
        warn("No matching checksum entry found; skipping verification.");
      }
    } catch (e) {
      if (e.message.includes("mismatch")) {
        throw e;
      }
      warn(`Could not verify checksum: ${e.message}`);
    }
  }

  info(`Installed: ${binaryDest}`);
  info("");
  info("OpenCodeReview is ready!");
  info("");
  info("Quick start:");
  info("  ocr version             Show version info");
  info("  ocr config set          Configure your LLM provider");
  info("  ocr review              Start a code review");
}

if (require.main === module) {
  main().catch((err) => {
    error(err.message);
    process.exit(1);
  });
} else {
  module.exports = {
    detectPlatform,
    loadPackageJson,
    buildUrl,
    download,
    downloadText,
    downloadBinary,
    computeChecksum,
  };
}
