#!/usr/bin/env node

import { cp, mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { basename, dirname, extname, join, resolve } from "node:path";
import { tmpdir } from "node:os";
import { spawn } from "node:child_process";

const BUILD_SERVICE_BASE_URL = "https://hbuilder.a.yunfeiyun.com";
const DEFAULT_UPLOAD_BASE_URL = "https://s3.hnyfwlw.com/test-ivan";
const DEFAULT_UPLOAD_PREFIX = "hbuilder-cloud/android-build-input";
const ARTIFACT_SCENARIO = "release";
const DEFAULT_POLL_INTERVAL_SECONDS = 3;
const CLIENT_ENV_FILENAME = ".android-build-client.env";
const SUPPORTED_KEYSTORE_SUFFIXES = new Set([".keystore", ".jks"]);

function printUsage() {
  console.log(`用法:
  1. 在项目根目录创建 ${CLIENT_ENV_FILENAME}
  2. 执行 node android-build-client.mjs

默认行为:
  1. 自动检测 unpackage/resources 下最新导出的 App 资源
  2. 自动读取项目根目录 manifest.json 中的版本号
  3. 从 ${CLIENT_ENV_FILENAME} 读取 DCloud AppKey、包名和签名参数
  4. 自动将项目根目录下唯一的 .keystore/.jks 签名文件一起打进标准包
  5. 打出标准构建包并匿名上传到 ${DEFAULT_UPLOAD_BASE_URL}
  6. 请求远程构建服务 ${BUILD_SERVICE_BASE_URL}
  7. 默认持续轮询直到完成
`);
}

async function loadClientEnv(projectRoot) {
  const envPath = join(projectRoot, CLIENT_ENV_FILENAME);
  if (!(await fileExists(envPath))) {
    return {};
  }
  const content = await readFile(envPath, "utf8");
  const env = {};
  for (const rawLine of content.split(/\r?\n/)) {
    const line = rawLine.trim();
    if (!line || line.startsWith("#")) {
      continue;
    }
    const index = line.indexOf("=");
    if (index <= 0) {
      continue;
    }
    const key = line.slice(0, index).trim();
    let value = line.slice(index + 1).trim();
    if (
      (value.startsWith("\"") && value.endsWith("\"")) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      value = value.slice(1, -1);
    }
    env[key] = value;
  }
  return env;
}

function stripJsonComments(text) {
  let result = "";
  let inString = false;
  let stringQuote = "";
  let escaped = false;
  let inLineComment = false;
  let inBlockComment = false;

  for (let index = 0; index < text.length; index += 1) {
    const char = text[index];
    const next = text[index + 1];

    if (inLineComment) {
      if (char === "\n") {
        inLineComment = false;
        result += char;
      }
      continue;
    }

    if (inBlockComment) {
      if (char === "*" && next === "/") {
        inBlockComment = false;
        index += 1;
      }
      continue;
    }

    if (inString) {
      result += char;
      if (escaped) {
        escaped = false;
      } else if (char === "\\") {
        escaped = true;
      } else if (char === stringQuote) {
        inString = false;
        stringQuote = "";
      }
      continue;
    }

    if ((char === "\"" || char === "'") && !inString) {
      inString = true;
      stringQuote = char;
      result += char;
      continue;
    }

    if (char === "/" && next === "/") {
      inLineComment = true;
      index += 1;
      continue;
    }

    if (char === "/" && next === "*") {
      inBlockComment = true;
      index += 1;
      continue;
    }

    result += char;
  }

  return result;
}

function normalizeJsonText(text) {
  return stripJsonComments(text.replace(/^\uFEFF/, ""));
}

async function parseJsonFile(path) {
  return JSON.parse(normalizeJsonText(await readFile(path, "utf8")));
}

async function fileExists(path) {
  try {
    await stat(path);
    return true;
  } catch {
    return false;
  }
}

async function findLatestResourceDir(projectRoot) {
  const resourcesRoot = join(projectRoot, "unpackage", "resources");
  const candidates = await readdir(resourcesRoot, { withFileTypes: true });
  let latest = null;
  for (const entry of candidates) {
    if (!entry.isDirectory()) {
      continue;
    }
    const dirPath = join(resourcesRoot, entry.name);
    const manifestPath = join(dirPath, "www", "manifest.json");
    if (!(await fileExists(manifestPath))) {
      continue;
    }
    const manifestStat = await stat(manifestPath);
    if (!latest || manifestStat.mtimeMs > latest.mtimeMs) {
      latest = { path: dirPath, mtimeMs: manifestStat.mtimeMs };
    }
  }
  if (!latest) {
    throw new Error("未找到导出资源: unpackage/resources/*/www/manifest.json");
  }
  return latest.path;
}

async function loadProjectManifest(projectRoot) {
  const manifestPath = join(projectRoot, "manifest.json");
  if (!(await fileExists(manifestPath))) {
    throw new Error("缺少项目根目录 manifest.json");
  }
  const data = await parseJsonFile(manifestPath);
  return {
    appName: data.name,
    uniAppId: data.appid,
    versionName: data.versionName,
    versionCode: Number(data.versionCode),
  };
}

async function loadResourceMetadata(resourceDir) {
  const manifestPath = join(resourceDir, "www", "manifest.json");
  const data = await parseJsonFile(manifestPath);
  return {
    resourceDir,
    uniAppId: data.id,
  };
}

async function deriveAppPackage(projectRoot) {
  const environmentPath = join(projectRoot, "config", "environment.js");
  if (await fileExists(environmentPath)) {
    const content = await readFile(environmentPath, "utf8");
    const match = content.match(/https:\/\/([a-z0-9-]+)\.agmp\.yunfeiyun\.com/i);
    if (match?.[1]) {
      return `com.agmp.${match[1].toLowerCase()}`;
    }
  }
  throw new Error("无法自动推导 Android 包名，请提供环境变量 BUILD_APP_PACKAGE");
}

async function resolveAppPackage(projectRoot, clientEnv) {
  const appPackage = (process.env.BUILD_APP_PACKAGE || clientEnv.BUILD_APP_PACKAGE || "").trim();
  if (appPackage) {
    return appPackage;
  }
  return deriveAppPackage(projectRoot);
}

async function collectNativeResources(projectRoot) {
  const manifestPath = join(projectRoot, "manifest.json");
  if (!(await fileExists(manifestPath))) {
    return [];
  }
  const manifest = await parseJsonFile(manifestPath);
  const appPlus = manifest["app-plus"] || {};
  const distribute = appPlus.distribute || {};
  const androidIcons = (distribute.icons || {}).android || {};
  const androidSplash = (distribute.splashscreen || {}).android || {};
  const densityMap = {
    hdpi: "drawable-hdpi",
    xhdpi: "drawable-xhdpi",
    xxhdpi: "drawable-xxhdpi",
    xxxhdpi: "drawable-xxxhdpi",
  };

  const mappings = [];
  const missing = [];
  for (const [density, relPath] of Object.entries(androidIcons)) {
    const targetDir = densityMap[density];
    if (!targetDir) continue;
    const absPath = resolve(projectRoot, relPath);
    if (await fileExists(absPath)) {
      mappings.push({ source: absPath, target: join(targetDir, "icon.png") });
    } else {
      missing.push(relPath);
    }
  }
  for (const [density, relPath] of Object.entries(androidSplash)) {
    const targetDir = densityMap[density];
    if (!targetDir) continue;
    const absPath = resolve(projectRoot, relPath);
    if (await fileExists(absPath)) {
      mappings.push({ source: absPath, target: join(targetDir, "splash.png") });
    } else {
      missing.push(relPath);
    }
  }
  if (missing.length > 0) {
    throw new Error(`manifest.json 中声明的 Android 图标或启动图不存在: ${missing.join(", ")}`);
  }
  return mappings;
}

async function findProjectKeystore(projectRoot) {
  const entries = await readdir(projectRoot, { withFileTypes: true });
  const keystoreFiles = entries
    .filter((entry) => entry.isFile())
    .map((entry) => join(projectRoot, entry.name))
    .filter((path) => SUPPORTED_KEYSTORE_SUFFIXES.has(extname(path).toLowerCase()));
  if (keystoreFiles.length === 0) {
    throw new Error("项目根目录缺少签名文件，请放置一个 .keystore 或 .jks 文件");
  }
  if (keystoreFiles.length > 1) {
    throw new Error("项目根目录存在多个签名文件，请只保留一个 .keystore 或 .jks 文件");
  }
  return keystoreFiles[0];
}

function runCommand(command, args) {
  return new Promise((resolvePromise, rejectPromise) => {
    const child = spawn(command, args, {
      stdio: ["ignore", "pipe", "pipe"],
    });
    let stdout = "";
    let stderr = "";
    child.stdout.on("data", (chunk) => {
      stdout += String(chunk);
    });
    child.stderr.on("data", (chunk) => {
      stderr += String(chunk);
    });
    child.on("error", (error) => {
      rejectPromise(error);
    });
    child.on("close", (code) => {
      if (code === 0) {
        resolvePromise({ stdout, stderr });
        return;
      }
      rejectPromise(new Error(stderr.trim() || stdout.trim() || `${command} 退出码: ${code}`));
    });
  });
}

function parseAliasNames(text) {
  const aliases = new Set();
  for (const line of text.split(/\r?\n/)) {
    const trimmed = line.trim();
    const aliasMatch = trimmed.match(/^Alias name:\s*(.+)$/i);
    if (aliasMatch?.[1]) {
      aliases.add(aliasMatch[1].trim());
      continue;
    }
    const chineseMatch = trimmed.match(/^别名(?:名称)?[：:]\s*(.+)$/);
    if (chineseMatch?.[1]) {
      aliases.add(chineseMatch[1].trim());
    }
  }
  return [...aliases];
}

async function detectKeyAlias(keystorePath, keystorePassword) {
  let output;
  try {
    output = await runCommand("keytool", [
      "-J-Duser.language=en",
      "-J-Duser.country=US",
      "-list",
      "-v",
      "-keystore",
      keystorePath,
      "-storepass",
      keystorePassword,
    ]);
  } catch (error) {
    throw new Error(`无法自动解析证书 alias，请安装 keytool 或在 ${CLIENT_ENV_FILENAME} 中填写 BUILD_KEY_ALIAS。原始错误: ${error.message}`);
  }
  const aliases = parseAliasNames(`${output.stdout}\n${output.stderr}`);
  if (aliases.length === 0) {
    throw new Error(`未能从证书中解析到 alias，请在 ${CLIENT_ENV_FILENAME} 中填写 BUILD_KEY_ALIAS`);
  }
  if (aliases.length > 1) {
    throw new Error(`证书中存在多个 alias: ${aliases.join(", ")}，请在 ${CLIENT_ENV_FILENAME} 中显式指定 BUILD_KEY_ALIAS`);
  }
  return aliases[0];
}

async function createBuildInputArchive(config, resourceMetadata) {
  const stageRoot = await mkdtemp(join(tmpdir(), "android-build-client-"));
  const distRoot = config.outputDir;
  const safeVersion = String(resourceMetadata.versionName).replaceAll("/", "_");
  const timestamp = new Date().toISOString().replaceAll(":", "").replaceAll("-", "").replace(/\..+/, "");
  const archivePath = join(
    distRoot,
    `android-build-input_${resourceMetadata.uniAppId}_v${safeVersion}_${timestamp}.tar.gz`,
  );

  try {
    await mkdir(join(stageRoot, "www"), { recursive: true });
    await mkdir(join(stageRoot, "native-res"), { recursive: true });
    await mkdir(join(stageRoot, "meta"), { recursive: true });
    await mkdir(join(stageRoot, "signing"), { recursive: true });
    await mkdir(distRoot, { recursive: true });

    await cp(join(resourceMetadata.resourceDir, "www"), join(stageRoot, "www"), { recursive: true });

    const nativeResources = await collectNativeResources(config.projectRoot);
    for (const mapping of nativeResources) {
      const destination = join(stageRoot, "native-res", mapping.target);
      await mkdir(dirname(destination), { recursive: true });
      await cp(mapping.source, destination);
    }

    await writeFile(
      join(stageRoot, "meta", "build.json"),
      JSON.stringify(
        {
          uni_appid: resourceMetadata.uniAppId,
          app_name: resourceMetadata.appName,
          version_name: resourceMetadata.versionName,
          version_code: resourceMetadata.versionCode,
        },
        null,
        2,
      ) + "\n",
      "utf8",
    );

    await cp(config.keystorePath, join(stageRoot, "signing", basename(config.keystorePath)));

    await runTar(stageRoot, archivePath);
    return { archivePath, cleanup: () => rm(stageRoot, { recursive: true, force: true }) };
  } catch (error) {
    await rm(stageRoot, { recursive: true, force: true });
    throw error;
  }
}

function runTar(stageRoot, archivePath) {
  return new Promise((resolvePromise, rejectPromise) => {
    const child = spawn("tar", ["-C", stageRoot, "-czf", archivePath, "."], {
      env: {
        ...process.env,
        COPYFILE_DISABLE: "1",
      },
      stdio: "inherit",
    });
    child.on("error", (error) => {
      rejectPromise(new Error(`执行 tar 失败: ${error.message}`));
    });
    child.on("close", (code) => {
      if (code === 0) {
        resolvePromise();
        return;
      }
      rejectPromise(new Error(`tar 打包失败，退出码: ${code}`));
    });
  });
}

function encodeS3Path(key) {
  return key
    .split("/")
    .map((segment) => encodeURIComponent(segment).replace(/[!*'()]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`))
    .join("/");
}

async function uploadArchive(config, archivePath, resourceMetadata) {
  const body = await readFile(archivePath);
  const version = resourceMetadata.versionName;
  const datePath = new Date().toISOString().slice(0, 10).replaceAll("-", "");
  const timestamp = new Date().toISOString().replaceAll(":", "").replaceAll("-", "").replace(/\..+/, "");
  const objectKey = [
    config.uploadPrefix,
    ARTIFACT_SCENARIO,
    config.appPackage,
    version,
    datePath,
    `${basename(archivePath, ".tar.gz")}_${timestamp}.tar.gz`,
  ].join("/");
  const uploadUrl = `${config.uploadBaseUrl}/${encodeS3Path(objectKey)}`;

  const response = await fetch(uploadUrl, {
    method: "PUT",
    headers: {
      "Content-Type": "application/gzip",
    },
    body,
  });

  if (!response.ok) {
    throw new Error(`上传构建包失败: ${response.status} ${await response.text()}`);
  }

  return {
    resourceUrl: `${config.uploadBaseUrl}/${objectKey}`,
    objectKey,
  };
}

async function requestRemoteBuild(config, resourceMetadata, resourceUrl) {
  const payload = {
    resource_url: resourceUrl,
    app_package: config.appPackage,
    dcloud_appkey: config.dcloudAppKey,
    keystore_password: config.keystorePassword,
    key_alias: config.keyAlias,
    key_password: config.keyPassword,
  };
  payload.version_name = resourceMetadata.versionName;
  payload.version_code = resourceMetadata.versionCode;

  let response;
  try {
    response = await fetch(`${config.serviceBaseUrl}/v1/android/builds`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(15000),
    });
  } catch (error) {
    throw new Error(`请求构建服务失败: ${config.serviceBaseUrl} 不可达。原始错误: ${error.message}`);
  }
  if (!response.ok) {
    throw new Error(`请求构建服务失败: ${response.status} ${await response.text()}`);
  }
  const data = await response.json();
  return {
    ...data,
    requestPayload: payload,
    metadata: resourceMetadata,
  };
}

async function waitForTask(config, taskId) {
  while (true) {
    let response;
    try {
      response = await fetch(`${config.serviceBaseUrl}/v1/android/builds/${taskId}`, {
        signal: AbortSignal.timeout(15000),
      });
    } catch (error) {
      throw new Error(
        `查询任务状态失败: ${config.serviceBaseUrl} 不可达。原始错误: ${error.message}`,
      );
    }
    if (!response.ok) {
      throw new Error(`查询任务状态失败: ${response.status} ${await response.text()}`);
    }
    const task = await response.json();
    console.log(`[build] 任务状态: ${task.status}`);
    if (task.status === "succeeded") {
      return task;
    }
    if (task.status === "failed") {
      throw new Error(task.error_message || "远程构建失败");
    }
    await new Promise((resolvePromise) =>
      setTimeout(resolvePromise, config.pollIntervalSeconds * 1000),
    );
  }
}

async function main() {
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
    printUsage();
    return;
  }
  const projectRoot = process.cwd();
  const clientEnv = await loadClientEnv(projectRoot);
  const dcloudAppKey = (process.env.BUILD_DCLOUD_APPKEY || clientEnv.BUILD_DCLOUD_APPKEY || "").trim();
  if (!dcloudAppKey) {
    throw new Error(`缺少 BUILD_DCLOUD_APPKEY，请在 ${CLIENT_ENV_FILENAME} 或环境变量中提供`);
  }
  const keystorePassword = (
    process.env.BUILD_KEYSTORE_PASSWORD || clientEnv.BUILD_KEYSTORE_PASSWORD || ""
  ).trim();
  if (!keystorePassword) {
    throw new Error(`缺少 BUILD_KEYSTORE_PASSWORD，请在 ${CLIENT_ENV_FILENAME} 或环境变量中提供`);
  }
  const keyPassword = (
    process.env.BUILD_KEY_PASSWORD || clientEnv.BUILD_KEY_PASSWORD || keystorePassword
  ).trim();

  const projectManifest = await loadProjectManifest(projectRoot);
  const appPackage = await resolveAppPackage(projectRoot, clientEnv);
  const keystorePath = await findProjectKeystore(projectRoot);
  const configuredAlias = (process.env.BUILD_KEY_ALIAS || clientEnv.BUILD_KEY_ALIAS || "").trim();
  const keyAlias = configuredAlias || (await detectKeyAlias(keystorePath, keystorePassword));
  const config = {
    projectRoot,
    serviceBaseUrl: BUILD_SERVICE_BASE_URL,
    uploadBaseUrl: DEFAULT_UPLOAD_BASE_URL,
    uploadPrefix: DEFAULT_UPLOAD_PREFIX,
    appPackage,
    dcloudAppKey,
    keystorePath,
    keystorePassword,
    keyAlias,
    keyPassword,
    outputDir: resolve(projectRoot, "dist"),
    pollIntervalSeconds: DEFAULT_POLL_INTERVAL_SECONDS,
  };
  const resourceDir = await findLatestResourceDir(projectRoot);
  const exportMetadata = await loadResourceMetadata(resourceDir);
  const resourceMetadata = {
    resourceDir,
    uniAppId: projectManifest.uniAppId || exportMetadata.uniAppId,
    appName: projectManifest.appName,
    versionName: projectManifest.versionName,
    versionCode: projectManifest.versionCode,
  };

  console.log(`[build] 构建服务: ${config.serviceBaseUrl}`);
  console.log(`[build] 资源目录: ${resourceDir}`);
  console.log(`[build] 资源 AppID: ${resourceMetadata.uniAppId}`);
  console.log(`[build] 包名: ${config.appPackage}`);
  console.log(`[build] 版本: ${resourceMetadata.versionName} (${resourceMetadata.versionCode})`);
  console.log(`[build] 签名文件: ${config.keystorePath}`);
  console.log("[build] 抓包支持: 已固定允许用户证书与明文 HTTP");

  const { archivePath, cleanup } = await createBuildInputArchive(config, resourceMetadata);
  try {
    console.log(`[build] 标准包已生成: ${archivePath}`);
    const uploadResult = await uploadArchive(config, archivePath, resourceMetadata);
    console.log(`[build] 标准包已上传: ${uploadResult.resourceUrl}`);
    const buildRequest = await requestRemoteBuild(config, resourceMetadata, uploadResult.resourceUrl);
    console.log(`[build] 已提交构建任务: ${buildRequest.task_id}`);
    const finalTask = await waitForTask(config, buildRequest.task_id);
    console.log(`[build] 构建完成: ${finalTask.artifact_url || "-"}`);
  } finally {
    await cleanup();
  }
}

main().catch((error) => {
  console.error(`[build] ${error.message}`);
  process.exitCode = 1;
});
