/**
 * `webagents fn` CLI commands.
 *
 * Subcommands:
 *
 *   webagents fn new <name> [--runtime js-v1]
 *     Scaffold a new function under `./functions/<name>` (folder shape)
 *     or `./functions/<name>.<ext>` (single-file with frontmatter).
 *     v1 enables `js-v1` only; `python-pyodide-v1` is deferred (ADR-0008)
 *     and `wasm-v1` is reserved.
 *
 *   webagents fn deploy <name>
 *     Read AGENT.md frontmatter + ./functions/, validate, and POST to
 *     POST /api/agents/<id>/functions for each declared function.
 *
 *   webagents fn list
 *     List functions on the active agent (cloud) or as discovered locally.
 *
 *   webagents fn invoke <name> [--data '<json>']
 *     Call POST /api/agents/<id>/functions/<name>/invoke for one-shot replays.
 *
 * The `webagents dev` watcher reloads via `agent.updateCapabilities()`
 * on any change to AGENT.md or anything under `./functions/`.
 */

import * as fs from 'fs';
import * as path from 'path';
import {
  validateManifest,
  parseFrontmatter,
} from '../skills/functions/validator';
import type {
  FunctionManifest,
  FunctionRuntimeId,
} from '../skills/functions/manifest';

interface AgentMdFrontmatter {
  name?: string;
  description?: string;
  model?: string;
  functions?: Record<string, FunctionDeclaration>;
  skills?: Record<string, unknown>;
}

interface FunctionDeclaration {
  contentId?: string;
  description?: string;
  runtime?: FunctionRuntimeId;
  permissions?: FunctionManifest['permissions'];
  limits?: FunctionManifest['limits'];
  parameters?: Record<string, unknown>;
  /** Localhost-only: relative path under `./functions/`. */
  file?: string;
}

const RUNTIME_EXT: Record<FunctionRuntimeId, string> = {
  'js-v1': 'js',
  'python-pyodide-v1': 'py',
  'wasm-v1': 'wasm',
};

const SCAFFOLD_TEMPLATES: Record<FunctionRuntimeId, string> = {
  'js-v1':
    `/**
 * @robutler-function
 * @runtime js-v1
 * @entrypoint handler
 * @description Generated by \`webagents fn new\`
 */
export default async function handler(ctx) {
  return { ok: true };
}
`,
  // Deferred runtimes — kept in the type union for forward-compat but the
  // CLI scaffolds an empty file and `fn new` rejects them below.
  'python-pyodide-v1': '',
  'wasm-v1': '',
};

// ---------------------------------------------------------------------------
// AGENT.md parser
// ---------------------------------------------------------------------------

export function readAgentMd(cwd: string = process.cwd()): AgentMdFrontmatter | null {
  const p = path.join(cwd, 'AGENT.md');
  if (!fs.existsSync(p)) return null;
  const raw = fs.readFileSync(p, 'utf-8');
  return parseAgentMdFrontmatter(raw);
}

export function parseAgentMdFrontmatter(raw: string): AgentMdFrontmatter | null {
  const m = /^---\s*\n([\s\S]*?)\n---/m.exec(raw);
  if (!m) return null;
  // Lazy-load YAML to keep startup fast.
  try {
    const yaml = require('js-yaml') as { load(s: string): unknown };
    return (yaml.load(m[1]) as AgentMdFrontmatter) ?? {};
  } catch {
    // Fallback: tiny key=value parser supporting only `key: scalar`.
    const out: AgentMdFrontmatter = {};
    for (const line of m[1].split('\n')) {
      const kv = /^(\w+):\s*(.+)$/.exec(line);
      if (kv) (out as Record<string, unknown>)[kv[1]] = kv[2].trim();
    }
    return out;
  }
}

// ---------------------------------------------------------------------------
// ./functions/ directory walker
// ---------------------------------------------------------------------------

export interface DiscoveredFunction {
  name: string;
  /** Single-file path or folder. */
  pathAbs: string;
  isFolder: boolean;
  manifest: FunctionManifest;
  source?: string;
}

export function discoverFunctions(cwd: string = process.cwd()): DiscoveredFunction[] {
  const dir = path.join(cwd, 'functions');
  if (!fs.existsSync(dir)) return [];
  const out: DiscoveredFunction[] = [];
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    if (entry.isDirectory()) {
      const manifestPath = path.join(dir, entry.name, 'manifest.json');
      if (!fs.existsSync(manifestPath)) continue;
      const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as FunctionManifest;
      out.push({
        name: manifest.name ?? entry.name,
        pathAbs: path.join(dir, entry.name),
        isFolder: true,
        manifest,
      });
    } else {
      const full = path.join(dir, entry.name);
      const source = fs.readFileSync(full, 'utf-8');
      const parsed = parseFrontmatter(source);
      const baseName = path.parse(entry.name).name;
      // `parseFrontmatter` returns a `Partial<FunctionManifest>`; fill in
      // `runtime` (required on `FunctionManifest`) so the resulting shape
      // satisfies the type. `js-v1` is the default for files lacking
      // explicit frontmatter.
      const manifest: FunctionManifest = {
        ...(parsed ?? {}),
        runtime: parsed?.runtime ?? ('js-v1' as FunctionRuntimeId),
        name: parsed?.name ?? baseName,
      };
      out.push({
        name: manifest.name ?? baseName,
        pathAbs: full,
        isFolder: false,
        manifest,
        source,
      });
    }
  }
  return out;
}

// ---------------------------------------------------------------------------
// Subcommands
// ---------------------------------------------------------------------------

export async function fnNew(name: string, opts: { runtime?: FunctionRuntimeId; cwd?: string } = {}): Promise<string> {
  const runtime = opts.runtime ?? 'js-v1';
  if (runtime !== 'js-v1') {
    throw new Error(
      `runtime ${runtime} is not enabled in v1. Use js-v1 (python-pyodide-v1 is deferred per ADR-0008; wasm-v1 is reserved for v2).`,
    );
  }
  const cwd = opts.cwd ?? process.cwd();
  const dir = path.join(cwd, 'functions');
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
  const ext = RUNTIME_EXT[runtime];
  if (!ext) throw new Error(`unsupported runtime: ${runtime}`);
  const filePath = path.join(dir, `${name}.${ext}`);
  if (fs.existsSync(filePath)) throw new Error(`already exists: ${filePath}`);
  fs.writeFileSync(filePath, SCAFFOLD_TEMPLATES[runtime]);
  return filePath;
}

export interface FnDeployOpts {
  cwd?: string;
  agentId: string;
  portalUrl: string;
  authToken: string;
}

export async function fnDeploy(opts: FnDeployOpts): Promise<{ deployed: string[]; skipped: string[] }> {
  const cwd = opts.cwd ?? process.cwd();
  const found = discoverFunctions(cwd);
  const deployed: string[] = [];
  const skipped: string[] = [];
  for (const fn of found) {
    const v = validateManifest(fn.manifest, { cloud: true, functionName: fn.name });
    if (!v.ok) {
      skipped.push(`${fn.name}: ${v.errors?.[0]?.message ?? 'invalid'}`);
      continue;
    }
    const r = await fetch(`${opts.portalUrl}/api/agents/${opts.agentId}/functions`, {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        authorization: `Bearer ${opts.authToken}`,
        'function-authoring-surface': 'cli',
      },
      body: JSON.stringify({
        name: fn.name,
        manifest: fn.manifest,
        source: fn.source,
      }),
    });
    if (!r.ok) {
      skipped.push(`${fn.name}: ${r.status} ${await r.text().catch(() => '')}`);
    } else {
      deployed.push(fn.name);
    }
  }
  return { deployed, skipped };
}

/** Watcher hook used by `webagents dev` — calls `onChange` on AGENT.md / ./functions/. */
export function watchFunctions(cwd: string, onChange: () => void): () => void {
  const targets = [path.join(cwd, 'AGENT.md'), path.join(cwd, 'functions')];
  const watchers = targets.filter((t) => fs.existsSync(t)).map((t) =>
    fs.watch(t, { recursive: true }, () => onChange()),
  );
  return () => watchers.forEach((w) => w.close());
}
