// Shared helpers for the mm-harness command modules: flag parsing, adapter
// resolution, script composition, and the teaching-error emitter. Each command
// module composes adapters/ scripts directly via spawnScript.
//
// Composition seam (overridable for contract tests):
//   MM_HARNESS_SCRIPT_BIN_<STEM>  — override a specific script by its basename.
//   For node invocations (bin === process.execPath) the stem is derived from the
//   script path in args[0], e.g. MM_HARNESS_SCRIPT_BIN_OPEN_DEBUG_MJS.

import { spawn, spawnSync } from 'node:child_process';
import path from 'node:path';

import { detectAdapter } from '../harness.ts';
import { resolveLeafInvoke, shellLeafMissing } from '../leaf-invoke.ts';
import { runnerDir } from '../paths.ts';
import type { MetaMaskRecipeAdapter } from '../types.ts';

// Exit-code taxonomy (docs/CLI-SPEC.md §5.6).
export const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 } as const;

// Adapters that launch a live app surface (core is headless).
export type DeviceAdapter = 'mobile' | 'extension';

export interface ParsedFlags {
  positional: string[];
  options: Record<string, string | boolean>;
}

// Positionals + boolean/valued flags; camelCases --foo-bar to fooBar.
export function parseFlags(argv: string[], booleans: Set<string>): ParsedFlags {
  const positional: string[] = [];
  const options: Record<string, string | boolean> = {};
  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (!arg.startsWith('--')) {
      positional.push(arg);
      continue;
    }
    const body = arg.slice(2);
    const eq = body.indexOf('=');
    const rawKey = eq === -1 ? body : body.slice(0, eq);
    const inline = eq === -1 ? undefined : body.slice(eq + 1);
    const key = rawKey.replace(/-([a-z])/gu, (_, c: string) => c.toUpperCase());
    if (booleans.has(key)) {
      options[key] = inline === undefined ? true : inline !== 'false';
      continue;
    }
    if (inline !== undefined) {
      options[key] = inline;
      continue;
    }
    // Valued flag with a following token, unless the next token is itself a flag.
    const next = argv[i + 1];
    if (next === undefined || next.startsWith('--')) {
      options[key] = true;
      continue;
    }
    options[key] = next;
    i += 1;
  }
  return { positional, options };
}

export function str(options: Record<string, string | boolean>, key: string): string | undefined {
  const value = options[key];
  return typeof value === 'string' ? value : undefined;
}

export function flag(options: Record<string, string | boolean>, key: string): boolean {
  return options[key] === true;
}

export function targetOf(options: Record<string, string | boolean>): string {
  return path.resolve(str(options, 'target') ?? str(options, 'projectRoot') ?? process.cwd());
}

const ADAPTER_TOKENS: readonly string[] = ['mobile', 'extension', 'core'];

// Explicit --adapter/--platform, else an optional hint, else auto-detect from the
// target. Reimplemented here (rather than shared with cli.ts) to avoid a cycle.
export function resolveAdapter(
  options: Record<string, string | boolean>,
  target: string,
  hint?: MetaMaskRecipeAdapter,
): MetaMaskRecipeAdapter | undefined {
  const explicit = str(options, 'adapter') ?? str(options, 'platform');
  if (explicit && ADAPTER_TOKENS.includes(explicit)) return explicit as MetaMaskRecipeAdapter;
  if (hint) return hint;
  return detectAdapter(target);
}

export interface ScriptResult {
  status: number;
  output: string;
}

// Compose an adapters/ script directly. Output is always captured (needed for
// heal classification) and, in human mode, forwarded to stderr. --json keeps
// stdout clean for the machine summary. `env` overlays extra vars onto the
// inherited environment for spawns that need a scoped variable (e.g. color mode).
export function spawnScript(
  script: string,
  args: string[],
  cwd: string,
  json: boolean,
  env?: Record<string, string>,
): ScriptResult {
  // For node invocations (script === process.execPath) the seam stem is derived
  // from args[0] so each spawned script has its own override key.
  const isNodeScript = script === process.execPath && args.length > 0;
  // Seam stem: for node invocations derive from args[0] so each script has its
  // own override key; for shell and other leaves derive from the script basename.
  const stem = isNodeScript
    ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase()
    : path.basename(script).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase();
  const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
  const bin = override ?? script;
  const directArgs = override !== undefined && isNodeScript ? args.slice(1) : args;

  // Shell leaves (.sh) run through bash so file mode need not be executable.
  // Bash exits 127 (not a Node spawn error) for a missing file, so pre-check.
  if (shellLeafMissing(bin)) {
    const message =
      `leaf could not start: ${path.basename(bin)} (ENOENT)\n` +
      `  Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`;
    process.stderr.write(`${message}\n`);
    return { status: 1, output: message };
  }

  const { bin: invokeBin, args: spawnArgs } = resolveLeafInvoke(bin, directArgs);
  const result = spawnSync(invokeBin, spawnArgs, {
    cwd,
    encoding: 'utf8',
    env: env ? { ...process.env, ...env } : process.env,
    maxBuffer: 64 * 1024 * 1024,
  });
  if (result.error) {
    // Spawn failure for non-shell leaves (ENOENT/EACCES) or when bash itself
    // cannot start. Always emit so a leaf that cannot start is never silent.
    // For node invocations the leaf name comes from args[0], not process.execPath.
    const leaf = isNodeScript ? path.basename(args[0]) : path.basename(script);
    const code = (result.error as NodeJS.ErrnoException).code ?? 'ESPAWN';
    const message =
      `leaf could not start: ${leaf} (${code})\n` +
      `  Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`;
    process.stderr.write(`${message}\n`);
    return { status: 1, output: message };
  }
  const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
  if (!json && output) process.stderr.write(output);
  return { status: result.status ?? 1, output };
}

// Streaming variant of spawnScript for long-running leaves (mobile prepare:
// native build + Metro + health-bridge poll, minutes long). spawnScript buffers
// via spawnSync and, in --json mode, suppresses output entirely — so those leaves
// run with zero feedback until exit. This tees the child's stdout+stderr to the
// parent's STDERR live (so the --json envelope on stdout stays clean) while still
// capturing the combined output for heal classification. Same seam, leaf-invoke
// resolution, missing-leaf pre-check, and spawn-error contract as spawnScript.
export function spawnScriptStreaming(
  script: string,
  args: string[],
  cwd: string,
  env?: Record<string, string>,
): Promise<ScriptResult> {
  const isNodeScript = script === process.execPath && args.length > 0;
  const stem = isNodeScript
    ? path.basename(args[0]).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase()
    : path.basename(script).replace(/[^A-Za-z0-9]/gu, '_').toUpperCase();
  const override = process.env[`MM_HARNESS_SCRIPT_BIN_${stem}`];
  const bin = override ?? script;
  const directArgs = override !== undefined && isNodeScript ? args.slice(1) : args;

  if (shellLeafMissing(bin)) {
    const message =
      `leaf could not start: ${path.basename(bin)} (ENOENT)\n` +
      `  Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`;
    process.stderr.write(`${message}\n`);
    return Promise.resolve({ status: 1, output: message });
  }

  const { bin: invokeBin, args: spawnArgs } = resolveLeafInvoke(bin, directArgs);
  return new Promise<ScriptResult>((resolve) => {
    const child = spawn(invokeBin, spawnArgs, {
      cwd,
      env: env ? { ...process.env, ...env } : process.env,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    let output = '';
    // Route BOTH child streams to the parent's stderr, live: stdout is reserved
    // for the harness --json envelope, so all human/leaf progress goes to stderr.
    const tee = (chunk: Buffer): void => {
      const text = chunk.toString('utf8');
      output += text;
      process.stderr.write(text);
    };
    child.stdout?.on('data', tee);
    child.stderr?.on('data', tee);
    child.on('error', (error: NodeJS.ErrnoException) => {
      const leaf = isNodeScript ? path.basename(args[0]) : path.basename(script);
      const code = error.code ?? 'ESPAWN';
      const message =
        `leaf could not start: ${leaf} (${code})\n` +
        `  Next: reinstall mm-harness (npm i -g @deeeed/metamask-harness) — the shell leaf is missing or not executable`;
      process.stderr.write(`${message}\n`);
      resolve({ status: 1, output: message });
    });
    child.on('close', (status) => {
      resolve({ status: status ?? 1, output });
    });
  });
}

// Both escapes from a failed repo-type detection (defined at the detection source
// in harness.ts): where to run it (checkout/target) and how to force it (adapter).
export { ADAPTER_DETECT_NEXT } from '../harness.ts';

// Teaching-error emitter (exit 2, machine-readable in --json). `userAction` is
// REQUIRED: a teaching error without a reachable escape must not compile, so the
// escape is a typed parameter rather than free-form prose spliced into `message`.
export function usageOut(json: boolean, command: string, message: string, userAction: string): number {
  if (json) {
    console.log(
      JSON.stringify(
        { schemaVersion: 1, command, status: 'fail', exitCode: EXIT.usage, error: { code: 'USAGE', message, userAction } },
        null,
        2,
      ),
    );
  } else {
    console.error(`✗ mm-harness ${command}: ${message}\n  Next: ${userAction}`);
  }
  return EXIT.usage;
}
