/** `imprint emit` — generate <assetRoot>/<site>/<toolName>/index.ts: a thin wrapper
 *  around runtime.executeWorkflow with the workflow JSON embedded inline. */

import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { basename, dirname, join as pathJoin, resolve as pathResolve } from 'node:path';
import { loadJsonFile } from './load-json.ts';
import { ensureImprintRuntimeLink } from './runtime-link.ts';
import { isLoginFieldKey } from './sensitive-keys.ts';
import { type Workflow, WorkflowSchema } from './types.ts';

interface EmitOptions {
  /** Path to workflow.json */
  workflowPath: string;
  /** Output dir; defaults to dirname(workflowPath). File: <outDir>/index.ts. */
  outDir?: string;
  /** Overwrite an existing index.ts. Default false. */
  force?: boolean;
}

interface EmitResult {
  workflowPath: string;
  outPath: string;
  toolName: string;
  /** Summary of the parameters the generated tool accepts. */
  parameters: Workflow['parameters'];
}

export function emit(opts: EmitOptions): EmitResult {
  const workflow = loadJsonFile(
    opts.workflowPath,
    WorkflowSchema,
    {
      notFound: '→ run `imprint generate <session>` to create one.',
      badSchema:
        '→ regenerate with `imprint generate <session>` (the LLM may have produced bad output).',
    },
    'workflow.json',
  );

  assertNoCredentialShapedParams(workflow);

  const outDir = opts.outDir ?? defaultOutDir(opts.workflowPath, workflow);

  mkdirSync(outDir, { recursive: true });
  const outPath = pathJoin(outDir, 'index.ts');

  if (existsSync(outPath) && !opts.force) {
    throw new Error(
      `${outPath} already exists. Pass --force to overwrite, or move/delete the existing file first.`,
    );
  }

  // Ensure IMPRINT_HOME has a node_modules/imprint symlink so generated
  // tools can `import 'imprint/runtime'` via standard module resolution.
  // discoverTools also calls this at runtime so dangling links self-heal
  // without re-emitting.
  ensureImprintRuntimeLink(pathResolve(outDir, '..', '..'));

  const source = renderModule(workflow);
  writeFileSync(outPath, source, 'utf8');

  return {
    workflowPath: opts.workflowPath,
    outPath,
    toolName: workflow.toolName,
    parameters: workflow.parameters,
  };
}

function defaultOutDir(workflowPath: string, workflow: Workflow): string {
  const workflowDir = pathResolve(dirname(workflowPath));
  if (basename(workflowDir) === workflow.toolName) return workflowDir;
  return pathJoin(workflowDir, workflow.toolName);
}

function renderModule(workflow: Workflow): string {
  const paramTypeFields = workflow.parameters
    .map((p) => {
      const optional = p.default !== undefined ? '?' : '';
      return `  /** ${escapeJsdoc(p.description)} */\n  ${p.name}${optional}: ${p.type};`;
    })
    .join('\n');

  const defaultsBlock = workflow.parameters
    .filter((p) => p.default !== undefined)
    .map((p) => `    ${p.name}: input.${p.name} ?? ${JSON.stringify(p.default)},`)
    .join('\n');

  const requiredCopies = workflow.parameters
    .filter((p) => p.default === undefined)
    .map((p) => `    ${p.name}: input.${p.name},`)
    .join('\n');

  const workflowJson = JSON.stringify(workflow, null, 2);

  return `/**
 * GENERATED by \`imprint emit\` — DO NOT EDIT BY HAND.
 *
 * Tool: ${workflow.toolName}
 * Site: ${workflow.site}
 * Intent: ${workflow.intent.description}
 *
 * To regenerate: imprint emit ~/.imprint/${workflow.site}/${workflow.toolName}/workflow.json --force
 */

import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import {
  executeWorkflow,
  type CredentialStore,
} from 'imprint/runtime';
import type { ToolResult, Workflow } from 'imprint/types';

const WORKFLOW: Workflow = ${workflowJson};

export interface ${pascalCase(workflow.toolName)}Input {
${paramTypeFields}
}

export async function ${camelCase(workflow.toolName)}(
  ${workflow.parameters.length === 0 ? '_input' : 'input'}: ${pascalCase(workflow.toolName)}Input,
  opts: { credentials?: CredentialStore; fetchImpl?: typeof fetch; initialState?: Record<string, unknown> } = {},
): Promise<ToolResult> {
  const __dirname = dirname(fileURLToPath(import.meta.url));
  const params: Record<string, string | number | boolean> = {
${defaultsBlock || requiredCopies || '    // (no parameters)'}
${defaultsBlock && requiredCopies ? requiredCopies : ''}
  };
  return executeWorkflow({
    workflow: WORKFLOW,
    params,
    credentials: opts.credentials,
    fetchImpl: opts.fetchImpl,
    initialState: opts.initialState,
    workflowPath: join(__dirname, 'workflow.json'),
  });
}

export { WORKFLOW };
`;
}

/** Pre-emit guardrail: refuse to write a workflow whose parameters look
 *  like login credentials (`password`, `userid`, `email`, etc., per the
 *  shared dictionary in sensitive-keys.ts) but are templated as plain
 *  `${param.X}` instead of credential-store references like
 *  `${credential.X}`.
 *
 *  This catches the failure mode where upstream credential extraction
 *  silently failed (e.g. unusual Content-Type, body framing the parser
 *  didn't recognise, declined credential-save prompt), so the compile
 *  agent had no credential anchor and chose to model the login fields as
 *  ordinary callable parameters. The resulting MCP tool would advertise
 *  `userid`/`password` as required inputs, forward whatever the caller
 *  passed verbatim, and (most often) silently produce empty results when
 *  the caller passed empty strings.
 *
 *  We require either:
 *    - The parameter isn't credential-shaped, OR
 *    - The body template references `${credential.<name>}` (or another
 *      `credential.*` reference), in which case the workflow is pulling
 *      from the credential store and the `${param.X}` parameter is
 *      effectively a no-op the user can safely ignore.
 *
 *  Throws with the remediation steps the user needs to take. */
function assertNoCredentialShapedParams(workflow: Workflow): void {
  const offenders: Array<{ name: string; matches: string[] }> = [];
  for (const param of workflow.parameters) {
    if (!isLoginFieldKey(param.name)) continue;
    const paramRef = `\${param.${param.name}}`;
    const credentialRef = `\${credential.${param.name}}`;
    const requestsUsingParam: string[] = [];
    let coveredByCredentialRef = false;
    for (let i = 0; i < workflow.requests.length; i++) {
      const req = workflow.requests[i];
      if (!req) continue;
      const haystack = `${req.url} ${req.body ?? ''} ${Object.values(req.headers).join(' ')}`;
      if (haystack.includes(credentialRef)) {
        coveredByCredentialRef = true;
      }
      if (haystack.includes(paramRef)) {
        requestsUsingParam.push(`requests[${i}] (${req.method} ${req.url})`);
      }
    }
    // Only flag if the body templates the param and there's no parallel
    // credential reference. A workflow that uses both `${param.X}` and
    // `${credential.X}` is suspicious but not necessarily broken — leave
    // it to the user. The dangerous case is `${param.X}` alone.
    if (requestsUsingParam.length > 0 && !coveredByCredentialRef) {
      offenders.push({ name: param.name, matches: requestsUsingParam });
    }
  }
  if (offenders.length === 0) return;

  const lines = [
    `Workflow ${JSON.stringify(workflow.toolName)} declares ${offenders.length} credential-shaped parameter(s) that are templated as plain \`\${param.X}\` instead of \`\${credential.X}\`:`,
    '',
  ];
  for (const o of offenders) {
    lines.push(`  • parameter \`${o.name}\` — used in:`);
    for (const m of o.matches) lines.push(`      - ${m}`);
  }
  lines.push(
    '',
    'Credentials MUST be pulled from the credential store via `${credential.<name>}`, never modelled as plain workflow parameters.',
    "This usually means the redact stage failed to extract a username+password pair from the recorded login request — common causes include unusual Content-Type headers, multipart bodies, or login fields the extractor dictionary doesn't yet cover.",
    '',
    'To fix:',
    `  1. Delete the redacted session: rm ${workflowToolHint(workflow)}/sessions/*.redacted.json (or the relevant one)`,
    `  2. Re-run from the redact stage: imprint teach ${workflow.site} --from redact`,
    '  3. Accept the "Save credentials for site to the credential manager?" prompt this time.',
    '  4. Let teach continue through generate → compile-playbook → emit.',
    '',
    "If the prompt does NOT appear during step 3, the extractor still cannot pair this site's login fields — please file a bug attaching the (redacted!) session.",
  );
  throw new Error(lines.join('\n'));
}

/** Pretty path hint for the error message above. We don't have IMPRINT_HOME
 *  in scope and don't need it — `~/.imprint/<site>` is the convention. */
function workflowToolHint(workflow: Workflow): string {
  return `~/.imprint/${workflow.site}`;
}

function pascalCase(s: string): string {
  return s
    .split(/[_-]+/)
    .map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())
    .join('');
}

function camelCase(s: string): string {
  const pc = pascalCase(s);
  return pc.charAt(0).toLowerCase() + pc.slice(1);
}

function escapeJsdoc(s: string): string {
  return s.replace(/\*\//g, '*\\/');
}
