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

import path from "node:path";
import { coreInternalFetch } from "../microservices";
import { preview, previewLines } from "./auth-and-safety";
import { DEFAULT_COMMANDER_BRIEF_CLAUDEQQ_BASE_URL, DEFAULT_COMMANDER_BRIEF_CLAUDEQQ_USER_ID } from "./types";
import type { ClaudeQqConfig, ClaudeQqEndpointResult, ClaudeQqSendResult, ClaudeQqTargetType, CommanderBriefDiff } from "./types";

export function envEnabled(name: string, defaultValue: boolean): boolean {
  const raw = process.env[name];
  if (raw === undefined || raw.length === 0) return defaultValue;
  return !["0", "false", "no", "off", "disabled"].includes(raw.toLowerCase());
}

export function positiveEnvInteger(name: string, defaultValue: number): number {
  const raw = process.env[name];
  if (raw === undefined || raw.length === 0) return defaultValue;
  const value = Number(raw);
  return Number.isInteger(value) && value > 0 ? value : defaultValue;
}

export function commanderBriefClaudeQqConfig(): ClaudeQqConfig {
  const targetTypeRaw = (process.env.UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_TARGET_TYPE ?? "private").toLowerCase();
  const targetType: ClaudeQqTargetType = targetTypeRaw === "group" ? "group" : "private";
  return {
    enabled: envEnabled("UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_ENABLED", true),
    baseUrl: process.env.UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_BASE_URL ?? DEFAULT_COMMANDER_BRIEF_CLAUDEQQ_BASE_URL,
    targetType,
    userId: process.env.UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_USER_ID ?? DEFAULT_COMMANDER_BRIEF_CLAUDEQQ_USER_ID,
    groupId: process.env.UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_GROUP_ID,
    timeoutMs: positiveEnvInteger("UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_TIMEOUT_MS", 15_000),
  };
}

export function maskedTarget(config: ClaudeQqConfig): Record<string, unknown> {
  if (config.targetType === "group") return { targetType: "group", groupId: config.groupId === undefined ? null : maskId(config.groupId) };
  return { targetType: "private", userId: config.userId === undefined ? null : maskId(config.userId) };
}

export function maskId(value: string): string {
  if (value.length <= 4) return "*".repeat(value.length);
  return `${value.slice(0, 2)}***${value.slice(-2)}`;
}

export function sanitizeUrlForOutput(value: string): string {
  try {
    const parsed = new URL(value);
    parsed.username = parsed.username.length > 0 ? "***" : "";
    parsed.password = parsed.password.length > 0 ? "***" : "";
    parsed.search = parsed.search.length > 0 ? "?..." : "";
    parsed.hash = "";
    return parsed.toString();
  } catch {
    return value.includes("?") ? `${value.split("?")[0]}?...` : value;
  }
}

export function claudeQqPayload(config: ClaudeQqConfig, message: string): Record<string, unknown> {
  if (config.targetType === "group") return { targetType: "group", groupId: config.groupId ?? "", message };
  return { targetType: "private", userId: config.userId ?? DEFAULT_COMMANDER_BRIEF_CLAUDEQQ_USER_ID, message };
}

export function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

export function proxiedClaudeQqBasePath(baseUrl: string): string | null {
  try {
    const parsed = new URL(baseUrl);
    const path = parsed.pathname.replace(/\/$/u, "");
    if (parsed.hostname === "backend-core" && path === "/api/microservices/claudeqq/proxy") return path;
    if ((parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost") && parsed.port === "8080" && path === "/api/microservices/claudeqq/proxy") return path;
    return null;
  } catch {
    return baseUrl === "/api/microservices/claudeqq/proxy" ? baseUrl : null;
  }
}

export function normalizeClaudeQqEndpoint(basePath: string, endpoint: string): string {
  return `${basePath.replace(/\/$/u, "")}${endpoint}`;
}

export function claudeQqResponseOk(response: unknown): boolean {
  if (!isRecord(response)) return false;
  if (response.ok === false) return false;
  if (typeof response.status === "number" && (response.status < 200 || response.status >= 300)) return false;
  const body = response.body;
  if (isRecord(body) && (body.ok === false || body.success === false)) return false;
  return response.ok === true || typeof response.status === "number";
}

export function summarizeClaudeQqProxyFailure(response: unknown, endpoint: string): ClaudeQqEndpointResult {
  if (!isRecord(response)) return { ok: false, endpoint, degradedReason: "invalid-response", message: "ClaudeQQ proxy returned a non-object response", response };
  const status = typeof response.status === "number" ? response.status : undefined;
  const stdoutTail = typeof response.stdoutTail === "string" ? response.stdoutTail : "";
  let parsedTail: unknown = null;
  if (stdoutTail.length > 0) {
    try {
      parsedTail = JSON.parse(stdoutTail.trim()) as unknown;
    } catch {
      parsedTail = null;
    }
  }
  return {
    ok: false,
    endpoint,
    status,
    degradedReason: response.ok === false ? "microservice-proxy-failed" : "invalid-response",
    message: typeof response.error === "string"
      ? response.error
      : typeof response.stderrTail === "string" && response.stderrTail.length > 0
        ? response.stderrTail.slice(-500)
        : "ClaudeQQ proxy request failed",
    response: parsedTail ?? response,
  };
}

export async function sendClaudeQqEndpoint(config: ClaudeQqConfig, endpoint: string, payload: Record<string, unknown>): Promise<ClaudeQqEndpointResult> {
  const basePath = proxiedClaudeQqBasePath(config.baseUrl);
  if (basePath !== null) {
    const response = coreInternalFetch(normalizeClaudeQqEndpoint(basePath, endpoint), {
      method: "POST",
      body: payload,
      maxResponseBytes: 240_000,
      timeoutMs: config.timeoutMs,
    });
    if (claudeQqResponseOk(response)) return { ok: true, endpoint, status: isRecord(response) && typeof response.status === "number" ? response.status : 200, response };
    return summarizeClaudeQqProxyFailure(response, endpoint);
  }

  return {
    ok: false,
    endpoint,
    degradedReason: "notification-path-unavailable",
    message: "ClaudeQQ notifications must use backend-core /api/microservices/claudeqq/proxy; local skill, powershell, direct host, and ad-hoc ClaudeQQ URLs are not supported.",
    response: {
      baseUrl: sanitizeUrlForOutput(config.baseUrl),
      requiredBaseUrl: DEFAULT_COMMANDER_BRIEF_CLAUDEQQ_BASE_URL,
      recommendedCommand: "bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST --body-json '<payload>' --raw",
    },
  };
}

export async function sendCommanderBriefClaudeQq(config: ClaudeQqConfig, message: string): Promise<ClaudeQqSendResult> {
  const target = maskedTarget(config);
  if (!config.enabled) {
    return { ok: true, attempted: false, skipped: true, skippedReason: "UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_ENABLED disabled", target };
  }
  if (config.targetType === "group" && (config.groupId === undefined || config.groupId.length === 0)) {
    return { ok: false, attempted: false, skipped: true, skippedReason: "group target requires UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_GROUP_ID", target };
  }
  const payload = claudeQqPayload(config, message);
  const first = await sendClaudeQqEndpoint(config, "/api/push/text", payload);
  if (first.ok) return { ok: true, attempted: true, endpoint: first.endpoint, status: first.status, response: first.response, target };
  const second = await sendClaudeQqEndpoint(config, "/api/send/text", payload);
  if (second.ok) return { ok: true, attempted: true, endpoint: second.endpoint, status: second.status, response: second.response, target };
  return {
    ok: false,
    attempted: true,
    endpoint: second.endpoint,
    status: second.status ?? first.status,
    degradedReason: second.degradedReason ?? first.degradedReason ?? "claudeqq-send-failed",
    message: second.message ?? first.message ?? "ClaudeQQ send failed",
    response: { attempts: [first, second] },
    target,
  };
}

export function commanderBriefNotificationPlan(issueNumber: number, body: string, diff: CommanderBriefDiff, config: ClaudeQqConfig): Record<string, unknown> {
  const proxyBasePath = proxiedClaudeQqBasePath(config.baseUrl);
  return {
    enabled: config.enabled,
    issueNumber,
    bodyChars: body.length,
    diff: {
      ok: diff.ok,
      mode: diff.mode,
      chars: diff.chars,
      sectionCount: diff.sectionCount,
      skippedReason: diff.skippedReason ?? null,
      preview: diff.ok ? preview(diff.message) : "",
      previewLines: diff.ok ? previewLines(diff.message) : [],
      containsLiteralBackslashN: diff.message.includes("\\n"),
      containsBackticks: diff.message.includes("`"),
      containsMarkdownTable: /^\s*\|.+\|\s*$/m.test(diff.message),
    },
    claudeqq: {
      dryRun: true,
      wouldSend: config.enabled && diff.ok && proxyBasePath !== null,
      baseUrl: sanitizeUrlForOutput(config.baseUrl),
      proxyBasePath,
      servicePath: proxyBasePath === null ? null : normalizeClaudeQqEndpoint(proxyBasePath, "/api/push/text"),
      target: maskedTarget(config),
      timeoutMs: config.timeoutMs,
      ...(proxyBasePath === null
        ? {
          blockedReason: "notification-path-unavailable",
          recommendedCommand: "bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST --body-json '<payload>' --raw",
        }
        : {}),
    },
  };
}
