// SPEC: PJ2026-010606 GitHub入口 draft-2026-06-25-gh-split
// Moved mechanically from scripts/src/gh.ts:1557-1667.

import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { isAbsolute, join, normalize, resolve } from "node:path";
import { rootPath } from "../config";
import { parseEnvFile } from "../platform-infra-ops-library";
import { readIssueCommentBody } from "./body-input";
import { PREVIEW_CHARS, UNIDESK_CLI_CONFIG_PATH } from "./types";
import type { GitHubOptions, GitHubTokenProbe } from "./types";

const GITHUB_TOKEN_SOURCE_CONFIG_REF = `${UNIDESK_CLI_CONFIG_PATH}#github.auth.tokenSource`;
const GITHUB_REPOSITORY_OVERRIDES_CONFIG_REF = `${UNIDESK_CLI_CONFIG_PATH}#github.auth.repositoryOverrides`;

export interface GitHubYamlTokenSourceConfig {
  enabled?: boolean;
  priority: "before-env" | "after-env";
  root: string;
  sourceRef: string;
  sourceKey: string;
  format: "env" | "raw-token";
  projectedEnv?: string;
  scope: "repository-override" | "global";
  configRef: string;
  repository?: string;
}

export interface GitHubYamlAuthConfig {
  tokenSource: GitHubYamlTokenSourceConfig | null;
  repositoryOverrides: GitHubYamlTokenSourceConfig[];
}

function repositoryLookupKey(repository: string): string {
  return repository.toLowerCase();
}

export function readIssueLifecycleCommentBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
  if (options.comment === undefined && options.commentFile === undefined) return null;
  if (options.comment !== undefined) {
    throw new Error(`${command} inline --comment is unsupported; use --comment-stdin with a quoted heredoc`);
  }
  if (options.body !== undefined || options.bodyFile !== undefined) {
    throw new Error(`${command} --comment-stdin cannot be combined with --body/--body-file/--body-stdin`);
  }
  if (options.commentFile !== undefined) {
    if (options.commentFile !== "-") {
      throw new Error(`${command} --comment-file is unsupported for lifecycle comments; use --comment-stdin with a quoted heredoc`);
    }
    return readIssueCommentBody({ ...options, body: undefined, bodyFile: options.commentFile });
  }
  return null;
}

export function tokenFromEnvironment(): GitHubTokenProbe {
  if (process.env.GH_TOKEN && process.env.GH_TOKEN.length > 0) {
    return { present: true, source: "GH_TOKEN", ghFallbackAttempted: false };
  }
  if (process.env.GITHUB_TOKEN && process.env.GITHUB_TOKEN.length > 0) {
    return { present: true, source: "GITHUB_TOKEN", ghFallbackAttempted: false };
  }
  return { present: false, source: null, ghFallbackAttempted: false };
}

export function tokenFromYamlSource(repo: string, authConfig: GitHubYamlAuthConfig = readGitHubYamlAuthConfig()): { token: string | null; probe: GitHubTokenProbe } {
  const repoLookupKey = repositoryLookupKey(repo);
  const override = authConfig.repositoryOverrides.find((candidate) => (
    candidate.repository !== undefined && repositoryLookupKey(candidate.repository) === repoLookupKey
  )) ?? null;
  const config = override ?? authConfig.tokenSource;
  if (config === null || config.enabled === false) {
    return {
      token: null,
      probe: {
        present: false,
        source: null,
        scope: override === null ? "global" : "repository-override",
        ghFallbackAttempted: false,
        yamlSourceAttempted: true,
        yamlSourceEnabled: config?.enabled ?? false,
        yamlSourcePriority: config?.priority,
        configRef: config?.configRef ?? GITHUB_TOKEN_SOURCE_CONFIG_REF,
        valuesPrinted: false,
      },
    };
  }
  const sourceRoot = resolveYamlSourceRoot(config.root);
  const sourcePath = resolveYamlSourcePath(sourceRoot, config.sourceRef);
  const sourceExists = existsSync(sourcePath);
  const values = sourceExists ? parseYamlTokenSource(readFileSync(sourcePath, "utf8"), config) : {};
  const sourceToken = values[config.sourceKey] ?? null;
  const projectedToken = config.projectedEnv === undefined ? null : process.env[config.projectedEnv] ?? null;
  const token = sourceToken ?? projectedToken;
  const present = token !== null && token.length > 0;
  const projectedEnvPresent = projectedToken !== null && projectedToken.length > 0;
  return {
    token: present ? token : null,
    probe: {
      present,
      source: sourceToken !== null ? "yaml-token-source" : projectedEnvPresent ? "yaml-token-projection" : null,
      scope: config.scope,
      ghFallbackAttempted: false,
      yamlSourceAttempted: true,
      yamlSourceEnabled: true,
      yamlSourcePriority: config.priority,
      configRef: config.configRef,
      sourceRef: config.sourceRef,
      sourceKey: config.sourceKey,
      sourceExists,
      sourceKeyPresent: sourceToken !== null && sourceToken.length > 0,
      ...(config.projectedEnv === undefined ? {} : { projectedEnv: config.projectedEnv, projectedEnvPresent }),
      tokenFingerprint: present ? fingerprintToken(token) : null,
      valuesPrinted: false,
    },
  };
}

export function ghBinaryPath(): string | null {
  try {
    const output = execFileSync("sh", ["-lc", "command -v gh"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
    return output.length > 0 ? output : null;
  } catch {
    return null;
  }
}

export function ghAuthToken(): string | null {
  try {
    const output = execFileSync("gh", ["auth", "token"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5000 }).trim();
    return output.length > 0 ? output : null;
  } catch {
    return null;
  }
}

export function resolveToken(repo: string, allowGhFallback: boolean, authConfig?: GitHubYamlAuthConfig): { token: string | null; probe: GitHubTokenProbe } {
  const yamlToken = tokenFromYamlSource(repo, authConfig);
  if (yamlToken.probe.yamlSourcePriority === "before-env" && yamlToken.probe.present) return yamlToken;
  if (yamlToken.probe.scope === "repository-override" && !yamlToken.probe.present) return yamlToken;
  const envProbe = tokenFromEnvironment();
  if (envProbe.present) {
    const token = envProbe.source === "GH_TOKEN" ? process.env.GH_TOKEN ?? null : process.env.GITHUB_TOKEN ?? null;
    return { token, probe: envProbe };
  }
  if (yamlToken.probe.present) return yamlToken;
  if (!allowGhFallback) return { token: null, probe: yamlToken.probe };
  const ghPath = ghBinaryPath();
  if (ghPath === null) return { token: null, probe: { ...yamlToken.probe, ghFallbackAttempted: true, ghBinaryFound: false, ghAuthTokenAvailable: null } };
  const token = ghAuthToken();
  if (token !== null) return { token, probe: { present: true, source: "gh-auth-token", ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: true } };
  return { token: null, probe: { ...yamlToken.probe, ghFallbackAttempted: true, ghBinaryFound: true, ghAuthTokenAvailable: false } };
}

function readGitHubYamlAuthConfig(): GitHubYamlAuthConfig {
  const configPath = rootPath(UNIDESK_CLI_CONFIG_PATH);
  if (!existsSync(configPath)) return { tokenSource: null, repositoryOverrides: [] };
  const parsed = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, UNIDESK_CLI_CONFIG_PATH);
  const github = optionalRecord(parsed.github, `${UNIDESK_CLI_CONFIG_PATH}.github`);
  const auth = optionalRecord(github?.auth, `${UNIDESK_CLI_CONFIG_PATH}.github.auth`);
  const tokenSource = optionalRecord(auth?.tokenSource, `${GITHUB_TOKEN_SOURCE_CONFIG_REF}`);
  const repositoryOverrides = optionalArray(auth?.repositoryOverrides, GITHUB_REPOSITORY_OVERRIDES_CONFIG_REF);
  const parsedTokenSource = tokenSource === null ? null : {
    enabled: booleanField(tokenSource, "enabled", GITHUB_TOKEN_SOURCE_CONFIG_REF),
    priority: tokenSourcePriorityField(tokenSource, "priority", GITHUB_TOKEN_SOURCE_CONFIG_REF),
    root: stringField(tokenSource, "root", GITHUB_TOKEN_SOURCE_CONFIG_REF),
    sourceRef: stringField(tokenSource, "sourceRef", GITHUB_TOKEN_SOURCE_CONFIG_REF),
    sourceKey: envKeyField(tokenSource, "sourceKey", GITHUB_TOKEN_SOURCE_CONFIG_REF),
    format: tokenSourceFormatField(tokenSource, "format", GITHUB_TOKEN_SOURCE_CONFIG_REF),
    scope: "global" as const,
    configRef: GITHUB_TOKEN_SOURCE_CONFIG_REF,
  };
  const seenRepositories = new Set<string>();
  const parsedOverrides = repositoryOverrides.map((value, index) => {
    const configRef = `${GITHUB_REPOSITORY_OVERRIDES_CONFIG_REF}[${index}]`;
    const override = asRecord(value, configRef);
    const repository = repositoryField(override, "repository", configRef);
    const repositoryKey = repositoryLookupKey(repository);
    if (seenRepositories.has(repositoryKey)) throw new Error(`${GITHUB_REPOSITORY_OVERRIDES_CONFIG_REF} contains duplicate repository ${repository}`);
    seenRepositories.add(repositoryKey);
    return {
      repository,
      priority: tokenSourcePriorityField(override, "priority", configRef),
      root: stringField(override, "root", configRef),
      sourceRef: stringField(override, "sourceRef", configRef),
      sourceKey: envKeyField(override, "sourceKey", configRef),
      format: tokenSourceFormatField(override, "format", configRef),
      ...(override.projectedEnv === undefined ? {} : { projectedEnv: envKeyField(override, "projectedEnv", configRef) }),
      scope: "repository-override" as const,
      configRef,
    };
  });
  return { tokenSource: parsedTokenSource, repositoryOverrides: parsedOverrides };
}

function parseYamlTokenSource(text: string, config: GitHubYamlTokenSourceConfig): Record<string, string> {
  if (config.format === "raw-token") return { [config.sourceKey]: text.trim() };
  return parseEnvFile(text);
}

function resolveYamlSourceRoot(root: string): string {
  return isAbsolute(root) ? resolve(root) : rootPath(root);
}

function resolveYamlSourcePath(root: string, sourceRef: string): string {
  if (isAbsolute(sourceRef) || sourceRef.includes("..")) throw new Error(`${GITHUB_TOKEN_SOURCE_CONFIG_REF}.sourceRef must be a relative path without ..`);
  const resolved = normalize(join(root, sourceRef));
  if (resolved !== root && !resolved.startsWith(`${root}/`)) throw new Error(`${GITHUB_TOKEN_SOURCE_CONFIG_REF}.sourceRef escapes configured root`);
  return resolved;
}

function fingerprintToken(value: string): string {
  return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
}

function asRecord(value: unknown, path: string): Record<string, unknown> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
  return value as Record<string, unknown>;
}

function optionalRecord(value: unknown, path: string): Record<string, unknown> | null {
  if (value === undefined || value === null) return null;
  return asRecord(value, path);
}

function optionalArray(value: unknown, path: string): unknown[] {
  if (value === undefined || value === null) return [];
  if (!Array.isArray(value)) throw new Error(`${path} must be a YAML list`);
  return value;
}

function stringField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = obj[key];
  if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
  return value.trim();
}

function envKeyField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (!/^[A-Z_][A-Z0-9_]*$/u.test(value)) throw new Error(`${path}.${key} must be an env key`);
  return value;
}

function repositoryField(obj: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(obj, key, path);
  if (!/^[^/\s]+\/[^/\s]+$/u.test(value)) throw new Error(`${path}.${key} must be in owner/name format`);
  return value;
}

function tokenSourcePriorityField(obj: Record<string, unknown>, key: string, path: string): "before-env" | "after-env" {
  const value = stringField(obj, key, path);
  if (value !== "before-env" && value !== "after-env") throw new Error(`${path}.${key} must be before-env or after-env`);
  return value;
}

function tokenSourceFormatField(obj: Record<string, unknown>, key: string, path: string): "env" | "raw-token" {
  const value = obj[key] === undefined ? "env" : stringField(obj, key, path);
  if (value !== "env" && value !== "raw-token") throw new Error(`${path}.${key} must be env or raw-token`);
  return value;
}

function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
  const value = obj[key];
  if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
  return value;
}

export function repoParts(repo: string): { owner: string; name: string } {
  const [owner, name, extra] = repo.split("/");
  if (!owner || !name || extra !== undefined) throw new Error("--repo must be in owner/name format");
  return { owner, name };
}

export function preview(text: string): string {
  return text.length > PREVIEW_CHARS ? `${text.slice(0, PREVIEW_CHARS)}...` : text;
}

export function previewLines(text: string, maxLines = 12): string[] {
  return text.split(/\r?\n/).slice(0, maxLines).map((line) => preview(line));
}

export function bodySha(text: string): string {
  return createHash("sha256").update(text, "utf8").digest("hex");
}

export function binarySha(data: Buffer): string {
  return createHash("sha256").update(data).digest("hex");
}

export function normalizeExpectedSha(raw: string): string {
  const value = raw.replace(/^sha256:/iu, "").toLowerCase();
  if (!/^[a-f0-9]{64}$/u.test(value)) throw new Error("--expect-body-sha must be a 64-character SHA-256 hex value, optionally prefixed with sha256:");
  return value;
}

export function shellPollutionEvidence(body: string): string[] {
  const evidence: string[] = [];
  if (body.includes("\\n")) evidence.push("literal-backslash-n");
  if (body.includes("\\t")) evidence.push("literal-backslash-t");
  if (body.includes("\\`")) evidence.push("escaped-backtick");
  if (/\\x1b|\\u001b|\u001b\[/u.test(body)) evidence.push("ansi-escape");
  if (/\r(?!\n)/u.test(body)) evidence.push("bare-carriage-return");
  return evidence;
}

export function bodySafetySignals(body: string): Record<string, unknown> {
  const evidence = shellPollutionEvidence(body);
  return {
    bodyChars: body.length,
    bodyTrimmedChars: body.trim().length,
    bodyLines: body.length === 0 ? 0 : body.split(/\r?\n/).length,
    bodySha: bodySha(body),
    preservesRawNewlines: body.includes("\n"),
    containsLiteralBackslashN: body.includes("\\n"),
    containsBackticks: body.includes("`"),
    containsMarkdownTable: /^\s*\|.+\|\s*$/m.test(body),
    shellPollution: {
      suspected: evidence.length > 0,
      evidence,
    },
  };
}
