import { createHash } from "node:crypto";
import { lstat, mkdir, readdir, stat } from "node:fs/promises";
import { createReadStream, readFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);
const TRANSACTION_ID = /^hatch_[0-9a-f-]{36}$/u;
const ARTIFACT_NAME = /^artifact-[0-9a-f-]{36}$/u;
const RELEASE_CONFIG_SCHEMA = "talons.hatch.release-config.v1";
const PRODUCT_ID = "talons";
const BUNDLE_ID = "com.owlkestrel.talons";
const INSTALLER_ADAPTER_ID = "talons.bundle-swap.macos.v1";
const EXECUTABLE_VERIFIER = "codesign-designated-requirement";
const EXECUTABLE_KEY_ID = "V8RCCB6627";

/**
 * Returns only a configuration derived from files sealed into Talons.app. No
 * environment variable, bridge response, model output, manifest value, or CLI
 * argument participates in product policy, publisher selection, or adapter
 * selection.
 */
export function createTalonsHatchProductConfig() {
  const runtimeRoot = runtimeRootForThisBundle();
  const releaseConfig = loadReleaseConfig(path.join(runtimeRoot, "talons", "release", "hatch-release-config.json"));
  const installed = loadInstalledIdentity(path.join(runtimeRoot, "talons", "release", "installed-identity.json"));
  const appBundlePath = path.resolve(runtimeRoot, "../../..");
  const helperPath = path.join(appBundlePath, "Contents", "Helpers", "TalonsHatchHelper");
  const stateDirectory = path.join(os.homedir(), "Library", "Application Support", "Talons", "Hatch");

  return {
    manifestUrl: releaseConfig.manifestUrl,
    trustedPublishers: releaseConfig.trustedPublishers,
    policy: releaseConfig.policy,
    installed,
    stateDirectory,
    adapter: createTalonsBundleAdapter({ stateDirectory, helperPath }),
  };
}

function runtimeRootForThisBundle() {
  const sourceDirectory = path.dirname(fileURLToPath(import.meta.url));
  const runtimeRoot = path.resolve(sourceDirectory, "../..");
  const expectedModule = path.join(runtimeRoot, "talons", "src", "talons-hatch-product.mjs");
  if (path.resolve(fileURLToPath(import.meta.url)) !== expectedModule) {
    throw new Error("Talons Hatch must run only from its fixed embedded runtime path.");
  }
  return runtimeRoot;
}

function loadReleaseConfig(configPath) {
  const value = readFixedJson(configPath, "Talons Hatch release configuration");
  if (value.schema !== RELEASE_CONFIG_SCHEMA || typeof value.status !== "string") {
    throw new Error("Talons Hatch release configuration is invalid.");
  }
  if (value.status !== "configured") {
    // This is a fail-closed seam for alpha bundles before O+K publishes the
    // real fixed manifest URL and public release key. Do not make a network
    // request or accept runtime configuration in this state.
    throw new Error("Talons Hatch release authority is not embedded in this alpha build.");
  }
  assertExactKeys(value, ["schema", "status", "manifestUrl", "trustedPublishers", "policy"], "Talons Hatch release configuration");
  if (!Array.isArray(value.trustedPublishers) || value.trustedPublishers.length === 0 || value.trustedPublishers.length > 16) {
    throw new Error("Talons Hatch release configuration has no bounded trusted publisher set.");
  }
  if (!value.policy || typeof value.policy !== "object" || Array.isArray(value.policy)) {
    throw new Error("Talons Hatch release configuration has no product policy.");
  }
  const policy = value.policy;
  if (policy.productId !== PRODUCT_ID
    || policy.appId !== BUNDLE_ID
    || policy.packageId !== BUNDLE_ID
    || policy.installer?.adapterId !== INSTALLER_ADAPTER_ID
    || policy.installer?.kind !== "native-updater"
    || policy.executableTrust?.authority !== "product-native"
    || policy.executableTrust?.verifier !== EXECUTABLE_VERIFIER
    || policy.executableTrust?.keyId !== EXECUTABLE_KEY_ID) {
    throw new Error("Talons Hatch release configuration is not pinned to the Talons native adapter.");
  }
  return {
    manifestUrl: value.manifestUrl,
    trustedPublishers: value.trustedPublishers,
    policy,
  };
}

function loadInstalledIdentity(identityPath) {
  const value = readFixedJson(identityPath, "Talons installed identity");
  assertExactKeys(value, [
    "productId", "appId", "packageId", "version", "build", "channel", "platform", "architecture", "updaterVersion",
  ], "Talons installed identity");
  if (value.productId !== PRODUCT_ID || value.appId !== BUNDLE_ID || value.packageId !== BUNDLE_ID
    || value.platform !== "macos" || value.architecture !== "arm64"
    || !Number.isSafeInteger(value.build) || value.build <= 0) {
    throw new Error("The bundled Talons installed identity is invalid.");
  }
  return value;
}

function createTalonsBundleAdapter({ stateDirectory, helperPath }) {
  const candidatesDirectory = path.join(stateDirectory, "candidates");
  return {
    productId: PRODUCT_ID,
    adapterId: INSTALLER_ADAPTER_ID,
    executableVerifier: EXECUTABLE_VERIFIER,
    executableKeyId: EXECUTABLE_KEY_ID,

    async verifyArtifact({ artifactPath, expectedSha256, expectedSizeBytes }) {
      await assertPrivateRegularFile(artifactPath, "Hatch artifact");
      const actual = await sha256File(artifactPath);
      const details = await stat(artifactPath);
      if (actual !== expectedSha256 || details.size !== expectedSizeBytes) {
        throw new Error("Talons Hatch artifact bytes no longer match the checked plan.");
      }
    },

    async stageCandidate({ transactionId, artifactPath }) {
      assertTransactionID(transactionId);
      await assertExpectedArtifactPath(stateDirectory, transactionId, artifactPath);
      await ensurePrivateDirectory(candidatesDirectory, "Talons Hatch candidate directory");
      const candidateRoot = path.join(candidatesDirectory, transactionId);
      try {
        await mkdir(candidateRoot, { recursive: false, mode: 0o700 });
      } catch {
        throw new Error("Talons Hatch refuses to reuse a candidate staging directory.");
      }
      await assertPrivateDirectory(candidateRoot, "Talons Hatch candidate staging directory");
      await runFixed("/usr/bin/ditto", ["-x", "-k", artifactPath, candidateRoot]);
      const entries = (await readdir(candidateRoot)).sort();
      if (entries.length !== 1 || entries[0] !== "Talons.app") {
        throw new Error("The checked Talons archive must contain exactly Talons.app.");
      }
      const bundlePath = path.join(candidateRoot, "Talons.app");
      await runHelper(helperPath, ["verify", bundlePath]);
      return { opaqueId: `talons-candidate:${transactionId}` };
    },

    async activate({ candidate }) {
      await runHelper(helperPath, ["install", bundlePathForCandidate(candidatesDirectory, candidate.opaqueId)]);
    },

    async healthCheck() {
      try {
        await runHelper(helperPath, ["health"]);
        return true;
      } catch {
        return false;
      }
    },

    async rollback() {
      await runHelper(helperPath, ["rollback"]);
    },

    async reconcile({ candidate, receipt }) {
      if (!candidate || receipt.state === "staged") return "rolled-back";
      try {
        await runHelper(helperPath, ["health"]);
        return "active";
      } catch {
        try {
          await runHelper(helperPath, ["rollback"]);
          return "rolled-back";
        } catch {
          return "indeterminate";
        }
      }
    },
  };
}

function bundlePathForCandidate(candidatesDirectory, opaqueId) {
  const prefix = "talons-candidate:";
  if (!opaqueId.startsWith(prefix)) throw new Error("Talons Hatch candidate identity is invalid.");
  const transactionId = opaqueId.slice(prefix.length);
  assertTransactionID(transactionId);
  return path.join(candidatesDirectory, transactionId, "Talons.app");
}

async function runHelper(helperPath, arguments_) {
  await assertFixedExecutable(helperPath, "Talons Hatch helper");
  await runFixed(helperPath, arguments_);
}

async function runFixed(executable, arguments_) {
  await execFileAsync(executable, arguments_, {
    shell: false,
    timeout: 60_000,
    maxBuffer: 16 * 1024,
    windowsHide: true,
    env: { PATH: "/usr/bin:/bin", HOME: os.homedir() },
  });
}

async function assertExpectedArtifactPath(stateDirectory, transactionId, artifactPath) {
  const stagingDirectory = path.join(stateDirectory, "staging", transactionId);
  const expected = path.resolve(stagingDirectory);
  const actual = path.resolve(artifactPath);
  if (path.dirname(actual) !== expected || !ARTIFACT_NAME.test(path.basename(actual))) {
    throw new Error("Talons Hatch refuses an artifact outside this transaction's fixed staging directory.");
  }
  await assertPrivateRegularFile(actual, "Hatch artifact");
}

async function sha256File(filePath) {
  const digest = createHash("sha256");
  for await (const chunk of createReadStream(filePath, { highWaterMark: 64 * 1024 })) digest.update(chunk);
  return digest.digest("hex");
}

function readFixedJson(filePath, label) {
  let source;
  try {
    source = readFileSync(filePath, "utf8");
  } catch {
    throw new Error(`${label} is unavailable.`);
  }
  try {
    const value = JSON.parse(source);
    if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("not object");
    return value;
  } catch {
    throw new Error(`${label} is invalid.`);
  }
}

function assertExactKeys(value, expected, label) {
  const actual = Object.keys(value).sort();
  const wanted = [...expected].sort();
  if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
    throw new Error(`${label} has unsupported fields.`);
  }
}

function assertTransactionID(value) {
  if (!TRANSACTION_ID.test(value)) throw new Error("Talons Hatch transaction identity is invalid.");
}

async function ensurePrivateDirectory(directory, label) {
  await mkdir(directory, { recursive: true, mode: 0o700 });
  await assertPrivateDirectory(directory, label);
}

async function assertPrivateDirectory(directory, label) {
  const details = await lstat(directory);
  if (!details.isDirectory() || details.isSymbolicLink() || (details.mode & 0o077) !== 0 || details.uid !== process.getuid()) {
    throw new Error(`${label} must be an owner-private non-symlink directory.`);
  }
}

async function assertPrivateRegularFile(filePath, label) {
  const details = await lstat(filePath);
  if (!details.isFile() || details.isSymbolicLink() || details.nlink !== 1 || (details.mode & 0o777) !== 0o600 || details.uid !== process.getuid()) {
    throw new Error(`${label} must be an owner-private regular file.`);
  }
}

async function assertFixedExecutable(filePath, label) {
  const details = await lstat(filePath);
  if (!details.isFile() || details.isSymbolicLink() || (details.mode & 0o022) !== 0) {
    throw new Error(`${label} is not a fixed executable regular file.`);
  }
}
