import { execFileSync, spawnSync } from "node:child_process";
import { createPrivateKey, createSign } from "node:crypto";
import { readFileSync } from "node:fs";
import { pathToFileURL } from "node:url";

const APP_ID_ENV = "OPENCLAW_GH_READ_APP_ID";
const KEY_FILE_ENV = "OPENCLAW_GH_READ_PRIVATE_KEY_FILE";
const INSTALLATION_ID_ENV = "OPENCLAW_GH_READ_INSTALLATION_ID";
const PERMISSIONS_ENV = "OPENCLAW_GH_READ_PERMISSIONS";
const API_VERSION = "2022-11-28";
const DEFAULT_READ_PERMISSION_KEYS = [
  "actions",
  "checks",
  "contents",
  "issues",
  "metadata",
  "pull_requests",
  "statuses",
] as const;

type GrantedPermissionLevel = "read" | "write" | "admin" | null | undefined;
type RequestedPermissionLevel = "read" | "write";
type GrantedPermissions = Record<string, GrantedPermissionLevel>;
type RequestedPermissions = Record<string, RequestedPermissionLevel>;

type InstallationResponse = {
  id: number;
  permissions?: GrantedPermissions;
};

type AccessTokenResponse = {
  token: string;
};

export function parseRepoArg(args: string[]): string | null {
  for (let i = 0; i < args.length; i += 1) {
    const arg = args[i];
    if (arg === "-R" || arg === "--repo") {
      return normalizeRepo(args[i + 1] ?? null);
    }
    if (arg.startsWith("--repo=")) {
      return normalizeRepo(arg.slice("--repo=".length));
    }
    if (arg.startsWith("-R") && arg.length > 2) {
      return normalizeRepo(arg.slice(2));
    }
  }
  return null;
}

export function normalizeRepo(value: string | null | undefined): string | null {
  const trimmed = value?.trim();
  if (!trimmed) {
    return null;
  }

  const withoutProtocol = trimmed.replace(/^[a-z]+:\/\//i, "");
  const withoutHost = withoutProtocol.replace(/^(?:[^@/]+@)?github\.com[:/]/i, "");
  const normalized = withoutHost.replace(/\.git$/i, "").replace(/^\/+|\/+$/g, "");
  const parts = normalized.split("/").filter(Boolean);
  if (parts.length < 2) {
    return null;
  }

  return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
}

export function parsePermissionKeys(raw: string | null | undefined): string[] {
  const trimmed = raw?.trim();
  if (!trimmed) {
    return [...DEFAULT_READ_PERMISSION_KEYS];
  }

  return trimmed
    .split(",")
    .map((value) => value.trim())
    .filter(Boolean);
}

export function buildReadPermissions(
  grantedPermissions: GrantedPermissions | null | undefined,
  requestedKeys: readonly string[],
): RequestedPermissions {
  const permissions: RequestedPermissions = {};
  for (const key of requestedKeys) {
    const granted = grantedPermissions?.[key];
    if (granted === "read" || granted === "write") {
      permissions[key] = "read";
    }
  }
  return permissions;
}

function isMainModule() {
  const entry = process.argv[1];
  return entry ? import.meta.url === pathToFileURL(entry).href : false;
}

function fail(message: string): never {
  console.error(`gh-read: ${message}`);
  process.exit(1);
}

function readRequiredEnv(name: string): string {
  const value = process.env[name]?.trim();
  if (!value) {
    fail(`missing ${name}`);
  }
  return value;
}

function resolveRepo(args: string[]): string | null {
  const fromArgs = parseRepoArg(args);
  if (fromArgs) {
    return fromArgs;
  }

  const fromEnv = normalizeRepo(process.env.GH_REPO);
  if (fromEnv) {
    return fromEnv;
  }

  try {
    const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    }).trim();
    return normalizeRepo(remote);
  } catch {
    return null;
  }
}

function base64UrlEncode(value: string | Uint8Array) {
  return Buffer.from(value)
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/g, "");
}

function createAppJwt(appId: string, privateKeyPem: string) {
  const now = Math.floor(Date.now() / 1000);
  const header = base64UrlEncode(JSON.stringify({ alg: "RS256", typ: "JWT" }));
  const payload = base64UrlEncode(JSON.stringify({ iat: now - 60, exp: now + 9 * 60, iss: appId }));
  const signingInput = `${header}.${payload}`;
  const signer = createSign("RSA-SHA256");
  signer.update(signingInput);
  signer.end();
  const signature = signer.sign(createPrivateKey(privateKeyPem));
  return `${signingInput}.${base64UrlEncode(signature)}`;
}

async function githubJson<T>(
  path: string,
  bearerToken: string,
  init?: {
    method?: "GET" | "POST";
    body?: unknown;
  },
): Promise<T> {
  const response = await fetch(`https://api.github.com${path}`, {
    method: init?.method ?? "GET",
    headers: {
      Accept: "application/vnd.github+json",
      Authorization: `Bearer ${bearerToken}`,
      "Content-Type": "application/json",
      "User-Agent": "openclaw-gh-read",
      "X-GitHub-Api-Version": API_VERSION,
    },
    body: init?.body === undefined ? undefined : JSON.stringify(init.body),
  });

  if (!response.ok) {
    const text = await response.text();
    fail(`${init?.method ?? "GET"} ${path} failed (${response.status}): ${text}`);
  }

  return (await response.json()) as T;
}

async function resolveInstallation(
  appJwt: string,
  repo: string | null,
): Promise<InstallationResponse> {
  const installationId = process.env[INSTALLATION_ID_ENV]?.trim();
  if (repo) {
    return githubJson<InstallationResponse>(`/repos/${repo}/installation`, appJwt);
  }
  if (installationId) {
    return githubJson<InstallationResponse>(`/app/installations/${installationId}`, appJwt);
  }
  fail(
    `missing repo context; pass -R owner/repo, set GH_REPO, or set ${INSTALLATION_ID_ENV} for a direct installation lookup`,
  );
  throw new Error("unreachable");
}

async function createInstallationToken(
  appJwt: string,
  installation: InstallationResponse,
  repo: string | null,
): Promise<string> {
  const repoName = repo?.split("/")[1] ?? null;
  const requestedPermissionKeys = parsePermissionKeys(process.env[PERMISSIONS_ENV]);
  const permissions = buildReadPermissions(installation.permissions, requestedPermissionKeys);
  const body: {
    repositories?: string[];
    permissions?: RequestedPermissions;
  } = {};

  if (repoName) {
    body.repositories = [repoName];
  }
  if (Object.keys(permissions).length > 0) {
    body.permissions = permissions;
  }

  const tokenResponse = await githubJson<AccessTokenResponse>(
    `/app/installations/${installation.id}/access_tokens`,
    appJwt,
    { method: "POST", body },
  );
  return tokenResponse.token;
}

async function main() {
  if (process.argv.length <= 2) {
    fail(
      "usage: scripts/gh-read <gh args...>\nset OPENCLAW_GH_READ_APP_ID and OPENCLAW_GH_READ_PRIVATE_KEY_FILE first",
    );
  }

  const ghArgs = process.argv.slice(2);
  const appId = readRequiredEnv(APP_ID_ENV);
  const privateKeyPath = readRequiredEnv(KEY_FILE_ENV);
  const privateKeyPem = readFileSync(privateKeyPath, "utf8");
  const repo = resolveRepo(ghArgs);
  const appJwt = createAppJwt(appId, privateKeyPem);
  const installation = await resolveInstallation(appJwt, repo);
  const token = await createInstallationToken(appJwt, installation, repo);
  const child = spawnSync("gh", ghArgs, {
    stdio: "inherit",
    env: {
      ...process.env,
      GH_TOKEN: token,
      GITHUB_TOKEN: token,
    },
  });

  if (child.error) {
    fail(child.error.message);
  }

  process.exit(child.status ?? 1);
}

if (isMainModule()) {
  await main();
}
