import { posix as posixPath } from "node:path";
import type { RemoteTransport } from "./ssh2-transport.ts";

export interface RemoteFindInput {
  pattern: string;
  path?: string;
  caseSensitive?: boolean;
  includeHidden?: boolean;
  limit?: number;
}

export interface RemoteGrepInput {
  pattern: string;
  path?: string;
  literal?: boolean;
  caseSensitive?: boolean;
  include?: string;
  includeHidden?: boolean;
  limit?: number;
}

export interface RemoteSearchResult {
  text: string;
  backend: string;
  matchCount: number;
  truncated: boolean;
}

const MARKER = "__PI_SSH_SEARCH_BACKEND__:";
const DEFAULT_LIMIT = 50;
const MAX_LIMIT = 200;
const MAX_LINE_CHARS = 800;
const MAX_CAPTURE_CHARS = 512_000;
const SEARCH_TIMEOUT_SECONDS = 30;
const MAX_DIAGNOSTIC_CHARS = 16_000;

function shellQuote(value: string): string {
  return `'${value.replace(/'/g, `'"'"'`)}'`;
}

function validateField(value: unknown, name: string, optional = false): string | undefined {
  if (value === undefined && optional) return undefined;
  if (typeof value !== "string" || value.length === 0) throw new Error(`${name} must be a non-empty string`);
  if (/[\0\r\n]/u.test(value)) throw new Error(`${name} must not contain NUL or newline characters`);
  return value;
}

function normalizeLimit(value: unknown): number {
  if (value === undefined) return DEFAULT_LIMIT;
  if (!Number.isInteger(value) || (value as number) < 1 || (value as number) > MAX_LIMIT) {
    throw new Error(`limit must be an integer from 1 to ${MAX_LIMIT}`);
  }
  return value as number;
}

export function resolveRemoteSearchPath(path: string | undefined, remoteCwd: string, remoteHome: string): string {
  const value = path ?? ".";
  validateField(value, "path");
  if (value === "~") return remoteHome;
  if (value.startsWith("~/")) return posixPath.normalize(posixPath.join(remoteHome, value.slice(2)));
  if (value.startsWith("~")) throw new Error("path supports only ~ or ~/... remote HOME expansion");
  if (value.startsWith("/")) return posixPath.normalize(value);
  return posixPath.normalize(posixPath.join(remoteCwd, value));
}

const STATUS_HELPER = [
  `pi_ssh_accept_status() {`,
  `  local actual="$1" accepted`,
  `  shift`,
  `  for accepted in "$@"; do`,
  `    if [ "$actual" -eq "$accepted" ]; then return 0; fi`,
  `  done`,
  `  return "$actual"`,
  `}`,
].join("\n");

const GREP_RECORD_HELPER = [
  `pi_ssh_limit_colon_records() {`,
  `  local path match line content count=0`,
  `  while IFS= read -r -d '' path && IFS= read -r match; do`,
  '    line="${match%%:*}"',
  '    content="${match#*:}"',
  '    printf \'%s\\0%s\\0%s\\0\' "${path:0:' + MAX_LINE_CHARS + '}" "$line" "${content:0:' + MAX_LINE_CHARS + '}"',
  `    count=$((count + 1))`,
  `    if [ "$count" -ge "$PI_SSH_TAKE" ]; then return 0; fi`,
  `  done`,
  `}`,
  `pi_ssh_limit_git_records() {`,
  `  local path line content count=0`,
  `  while IFS= read -r -d '' path && IFS= read -r -d '' line && IFS= read -r content; do`,
  '    printf \'%s\\0%s\\0%s\\0\' "${path:0:' + MAX_LINE_CHARS + '}" "$line" "${content:0:' + MAX_LINE_CHARS + '}"',
  `    count=$((count + 1))`,
  `    if [ "$count" -ge "$PI_SSH_TAKE" ]; then return 0; fi`,
  `  done`,
  `}`,
] .join("\n");

function findPipeline(input: RemoteFindInput, root: string, limit: number): string {
  const pattern = validateField(input.pattern, "pattern") as string;
  const fdCase = input.caseSensitive ? "--case-sensitive" : "--ignore-case";
  const fdHidden = input.includeHidden ? "--hidden" : "";
  const grepCase = input.caseSensitive ? "" : "-i";
  const hiddenFilter = input.includeHidden ? "cat" : "grep -Ev '(^|/)\\.'";
  const take = limit + 1;
  return [
    STATUS_HELPER,
    `if command -v fd >/dev/null 2>&1; then`,
    `  printf '${MARKER}fd\\n'`,
    `  fd --type f --color never --fixed-strings ${fdCase} ${fdHidden} --exclude .git --exclude node_modules -- ${shellQuote(pattern)} ${shellQuote(root)} | head -n ${take} | cut -c 1-${MAX_LINE_CHARS}`,
    '  statuses=("${PIPESTATUS[@]}")',
    '  pi_ssh_accept_status "${statuses[0]}" 0 141 || exit $?',
    `elif command -v fdfind >/dev/null 2>&1; then`,
    `  printf '${MARKER}fdfind\\n'`,
    `  fdfind --type f --color never --fixed-strings ${fdCase} ${fdHidden} --exclude .git --exclude node_modules -- ${shellQuote(pattern)} ${shellQuote(root)} | head -n ${take} | cut -c 1-${MAX_LINE_CHARS}`,
    '  statuses=("${PIPESTATUS[@]}")',
    '  pi_ssh_accept_status "${statuses[0]}" 0 141 || exit $?',
    `elif command -v git >/dev/null 2>&1 && git -C ${shellQuote(root)} rev-parse --is-inside-work-tree >/dev/null 2>&1; then`,
    `  printf '${MARKER}git-ls-files\\n'`,
    `  git -C ${shellQuote(root)} ls-files -co --exclude-standard | ${hiddenFilter} | grep -F ${grepCase} -- ${shellQuote(pattern)} | head -n ${take} | cut -c 1-${MAX_LINE_CHARS}`,
    '  statuses=("${PIPESTATUS[@]}")',
    '  pi_ssh_accept_status "${statuses[0]}" 0 141 || exit $?',
    '  pi_ssh_accept_status "${statuses[1]}" 0 1 141 || exit $?',
    '  pi_ssh_accept_status "${statuses[2]}" 0 1 141 || exit $?',
    `else`,
    `  printf '${MARKER}find\\n'`,
    `  find ${shellQuote(root)} -type f ! -path '*/.git/*' ! -path '*/node_modules/*' | ${hiddenFilter} | grep -F ${grepCase} -- ${shellQuote(pattern)} | head -n ${take} | cut -c 1-${MAX_LINE_CHARS}`,
    '  statuses=("${PIPESTATUS[@]}")',
    '  pi_ssh_accept_status "${statuses[0]}" 0 141 || exit $?',
    '  pi_ssh_accept_status "${statuses[1]}" 0 1 141 || exit $?',
    '  pi_ssh_accept_status "${statuses[2]}" 0 1 141 || exit $?',
    `fi`,
  ].join("\n");
}

function grepPipeline(input: RemoteGrepInput, root: string, limit: number): string {
  const pattern = validateField(input.pattern, "pattern") as string;
  const include = validateField(input.include, "include", true);
  if (include?.includes("/")) throw new Error("include must be a basename glob without /");
  const rgMode = input.literal === false ? "" : "-F";
  const grepMode = input.literal === false ? "-E" : "-F";
  const insensitive = input.caseSensitive ? "" : "-i";
  const rgHidden = input.includeHidden
    ? "--hidden"
    : "--glob '!.*' --glob '!**/.*' --glob '!**/.*/**'";
  const rgGlob = include ? `-g ${shellQuote(include)}` : "";
  const gitPathspecs = [
    ...(include ? [`:(glob)**/${include}`] : []),
    ...(input.includeHidden ? [] : [":(exclude,glob)**/.*", ":(exclude,glob)**/.*/**"]),
  ];
  const gitPath = gitPathspecs.length > 0 ? `-- ${gitPathspecs.map(shellQuote).join(" ")}` : "";
  const findName = include ? `-name ${shellQuote(include)}` : "";
  const includeHidden = input.includeHidden ? 1 : 0;
  const take = limit + 1;
  return [
    STATUS_HELPER,
    GREP_RECORD_HELPER,
    `PI_SSH_TAKE=${take}`,
    `PI_SSH_ROOT=${shellQuote(root)}`,
    `PI_SSH_INCLUDE_HIDDEN=${includeHidden}`,
    `if command -v rg >/dev/null 2>&1; then`,
    `  printf '${MARKER}ripgrep\\n'`,
    `  rg --null --line-number --no-heading --color never --with-filename --max-columns ${MAX_LINE_CHARS} --max-columns-preview ${rgMode} ${insensitive} ${rgHidden} ${rgGlob} --glob '!.git/**' --glob '!node_modules/**' -- ${shellQuote(pattern)} ${shellQuote(root)} | pi_ssh_limit_colon_records`,
    '  statuses=("${PIPESTATUS[@]}")',
    '  pi_ssh_accept_status "${statuses[0]}" 0 1 141 || exit $?',
    '  pi_ssh_accept_status "${statuses[1]}" 0 || exit $?',
    `elif command -v git >/dev/null 2>&1 && git -C ${shellQuote(root)} rev-parse --is-inside-work-tree >/dev/null 2>&1; then`,
    `  printf '${MARKER}git-grep\\n'`,
    `  git -C ${shellQuote(root)} grep --untracked --exclude-standard -z -n -I ${grepMode} ${insensitive} -e ${shellQuote(pattern)} ${gitPath} | pi_ssh_limit_git_records`,
    '  statuses=("${PIPESTATUS[@]}")',
    '  pi_ssh_accept_status "${statuses[0]}" 0 1 141 || exit $?',
    '  pi_ssh_accept_status "${statuses[1]}" 0 || exit $?',
    `else`,
    `  printf '${MARKER}grep\\n'`,
    `  find ${shellQuote(root)} -type f ! -path '*/.git/*' ! -path '*/node_modules/*' ${findName} -print0 | (`,
    `    while IFS= read -r -d '' file; do`,
    `      if [ "$PI_SSH_INCLUDE_HIDDEN" -eq 0 ]; then`,
    `        if [ -d "$PI_SSH_ROOT" ]; then`,
    '          relative=${file#"$PI_SSH_ROOT"/}',
    `        else`,
    '          relative=${file##*/}',
    `        fi`,
    '        case "$relative" in .*|*/.*) continue ;; esac',
    `      fi`,
    `      grep -n -I ${grepMode} ${insensitive} -- ${shellQuote(pattern)} "$file" | while IFS= read -r match; do`,
    `        printf '%s\\0%s\\n' "$file" "$match"`,
    `      done`,
    '      grep_statuses=("${PIPESTATUS[@]}")',
    '      pi_ssh_accept_status "${grep_statuses[0]}" 0 1 141 || exit $?',
    '      pi_ssh_accept_status "${grep_statuses[1]}" 0 1 141 || exit $?',
    `    done`,
    `  ) | pi_ssh_limit_colon_records`,
    '  statuses=("${PIPESTATUS[@]}")',
    '  pi_ssh_accept_status "${statuses[0]}" 0 141 || exit $?',
    '  pi_ssh_accept_status "${statuses[1]}" 0 141 || exit $?',
    '  pi_ssh_accept_status "${statuses[2]}" 0 || exit $?',
    `fi`,
  ].join("\n");
}

export function buildRemoteFindCommand(input: RemoteFindInput, root: string): { command: string; limit: number } {
  const limit = normalizeLimit(input.limit);
  validateField(root, "resolved path");
  return { command: findPipeline(input, root, limit), limit };
}

export function buildRemoteGrepCommand(input: RemoteGrepInput, root: string): { command: string; limit: number } {
  const limit = normalizeLimit(input.limit);
  validateField(root, "resolved path");
  return { command: grepPipeline(input, root, limit), limit };
}

function prefixGitPath(path: string, root: string): string {
  return path.startsWith("/") || path.startsWith("../") ? path : posixPath.join(root, path);
}

function visibleField(value: string): string {
  return value.replace(/\r/gu, "\\r").replace(/\n/gu, "\\n");
}

function normalizeDiagnostic(raw: Buffer): string {
  return raw
    .toString("utf8")
    .replace(/\0/gu, " ")
    .replace(new RegExp(`${MARKER}[^\\n]*`, "gu"), "")
    .replace(/\s+/gu, " ")
    .trim()
    .slice(0, 300);
}

export function formatRemoteSearchOutput(
  raw: string | Buffer,
  root: string,
  limit: number,
  kind: "find" | "grep",
): RemoteSearchResult {
  const output = Buffer.isBuffer(raw) ? raw.toString("utf8") : raw;
  const markerIndex = output.indexOf(MARKER);
  const markerEnd = markerIndex < 0 ? -1 : output.indexOf("\n", markerIndex);
  if (markerIndex < 0 || markerEnd < 0) throw new Error(`remote ${kind} did not report a search backend`);
  const backend = output.slice(markerIndex + MARKER.length, markerEnd).trim() || "unknown";
  const payload = output.slice(markerEnd + 1);
  let sourceRows: string[];
  if (kind === "grep") {
    const fields = payload.length === 0 ? [] : payload.split("\0");
    if (fields.at(-1) === "") fields.pop();
    if (fields.length % 3 !== 0) throw new Error("remote grep returned a malformed or truncated NUL-delimited result");
    sourceRows = [];
    for (let index = 0; index < fields.length; index += 3) {
      const path = backend.startsWith("git-") ? prefixGitPath(fields[index], root) : fields[index];
      sourceRows.push(`${visibleField(path)}:${visibleField(fields[index + 1])}:${visibleField(fields[index + 2])}`);
    }
  } else {
    sourceRows = payload.replace(/\r\n?/gu, "\n").split("\n").filter((line) => line.length > 0);
    if (backend.startsWith("git-")) sourceRows = sourceRows.map((line) => prefixGitPath(line, root));
  }
  const truncated = sourceRows.length > limit;
  const rows = sourceRows.slice(0, limit).map((line) =>
    line.length > MAX_LINE_CHARS ? `${line.slice(0, MAX_LINE_CHARS - 1)}…` : line,
  );
  const header = `Remote ${kind}: ${rows.length} result${rows.length === 1 ? "" : "s"} (backend: ${backend}, root: ${root}, truncated: ${truncated ? "yes" : "no"})`;
  return {
    text: rows.length > 0 ? `${header}\n\n${rows.join("\n")}` : `${header}\n\nNo matches found.`,
    backend,
    matchCount: rows.length,
    truncated,
  };
}

async function runRemoteSearch(
  transport: RemoteTransport,
  command: string,
  executionCwd: string,
  root: string,
  limit: number,
  kind: "find" | "grep",
  signal?: AbortSignal,
): Promise<RemoteSearchResult> {
  const chunks: Buffer[] = [];
  const stderrChunks: Buffer[] = [];
  let captured = 0;
  let stderrCaptured = 0;
  const capture = (target: Buffer[], data: Buffer, stderr = false) => {
    const current = stderr ? stderrCaptured : captured;
    const maximum = stderr ? MAX_DIAGNOSTIC_CHARS : MAX_CAPTURE_CHARS;
    if (current >= maximum) return;
    const remaining = maximum - current;
    const chunk = data.length > remaining ? data.subarray(0, remaining) : data;
    target.push(chunk);
    if (stderr) stderrCaptured += chunk.length;
    else captured += chunk.length;
  };
  let result: { exitCode: number | null };
  try {
    result = await transport.exec(command, executionCwd, {
      signal,
      timeout: SEARCH_TIMEOUT_SECONDS,
      onData(data) { capture(chunks, data); },
      onStderr(data) { capture(stderrChunks, data, true); },
    });
  } catch (error) {
    if (!signal?.aborted && /timed out/iu.test(error instanceof Error ? error.message : String(error))) {
      throw new Error(`remote ${kind} timed out after ${SEARCH_TIMEOUT_SECONDS}s (root: ${root}); narrow the remote path with ssh_find before retrying`);
    }
    throw error;
  }
  const output = Buffer.concat(chunks);
  const stderr = Buffer.concat(stderrChunks);
  if (result.exitCode !== 0) {
    const detail = normalizeDiagnostic(stderr.length > 0 ? stderr : output);
    throw new Error(`remote ${kind} failed${result.exitCode === null ? "" : ` with exit code ${result.exitCode}`}${detail ? `: ${detail}` : ""}`);
  }
  const formatted = formatRemoteSearchOutput(output, root, limit, kind);
  const warning = normalizeDiagnostic(stderr);
  if (warning) formatted.text += `\n\nRemote warning: ${warning}`;
  return formatted;
}

export function runRemoteFind(
  transport: RemoteTransport,
  input: RemoteFindInput,
  remoteCwd: string,
  remoteHome: string,
  signal?: AbortSignal,
): Promise<RemoteSearchResult> {
  const root = resolveRemoteSearchPath(input.path, remoteCwd, remoteHome);
  const built = buildRemoteFindCommand(input, root);
  return runRemoteSearch(transport, built.command, remoteCwd, root, built.limit, "find", signal);
}

export function runRemoteGrep(
  transport: RemoteTransport,
  input: RemoteGrepInput,
  remoteCwd: string,
  remoteHome: string,
  signal?: AbortSignal,
): Promise<RemoteSearchResult> {
  const root = resolveRemoteSearchPath(input.path, remoteCwd, remoteHome);
  const built = buildRemoteGrepCommand(input, root);
  return runRemoteSearch(transport, built.command, remoteCwd, root, built.limit, "grep", signal);
}
