/**
 * P9 model pull: download a HuggingFace repo onto a host into its models dir.
 *
 * Non-blocking job (fire-and-forget like bench/eval), progress over the existing
 * control_job frame (jobType 'action', detail.kind = 'pull'). The repo id is
 * validated server-side as defense in depth on top of the wrapper's own check,
 * then passed as a single token (never interpolated into a shell string in
 * wrapper mode; in shell mode it is the only argument and is regex-clean).
 */

import type { DeltaEmitter } from './delta-emitter.js';
import { publishJob } from './publish-job.js';
import type { SshExec, SshTarget, SshMode } from './ssh-config.js';
import { acquireHostAccess } from './host-access.js';

/**
 * HF repo id: org/name. Each segment MUST start with an alphanumeric (HF's own
 * rule), which also rejects `..`/`.` traversal segments that a plain `[._-]+`
 * class would let through (e.g. `../x`). Exactly one slash; no spaces/metachars.
 */
export const REPO_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;

export function validateRepoId(repo: string): boolean {
  return REPO_ID_RE.test(repo);
}

/**
 * Build the pull command for a host. Pure helper for testing.
 * - wrapper mode: the `pull <repo>` verb (wrapper hardcodes the models dir).
 * - shell mode: a direct `huggingface-cli download` into <modelsDir>/<repo__>.
 */
export function buildPullCommand(mode: SshMode, repo: string, modelsDir?: string): string {
  if (mode === 'wrapper') return `pull ${repo}`;
  const dir = (modelsDir ?? '').replace(/\/+$/, '');
  const local = `${dir}/${repo.replace(/\//g, '__')}`;
  // POSIX single-quote escape the path: handles spaces AND an embedded quote in
  // modelsDir (which comes from the request body). repo is already regex-clean.
  const quoted = `'${local.replace(/'/g, `'\\''`)}'`;
  return `huggingface-cli download ${repo} --local-dir ${quoted}`;
}

export interface PullParams {
  jobId: string;
  providerId: string;
  target: SshTarget;
  repo: string;
  mode: SshMode;
  modelsDir?: string; // required for shell mode
}

export interface PullResult {
  ok: boolean;
  error?: string;
}

/**
 * Run a model pull as a control_job. Resolves when the pull finishes; callers
 * invoke it fire-and-forget so the HTTP response can return 202 immediately.
 */
export async function runModelPull(
  params: PullParams,
  exec: SshExec,
  emitter: DeltaEmitter,
  seq: number = 0,
): Promise<PullResult> {
  const { jobId, providerId, target, repo, mode, modelsDir } = params;

  const grant = await acquireHostAccess(providerId, 'pull');
  if (!grant.ok) {
    publishJob(emitter, { seq, jobType: 'action', jobId, status: 'failed', detail: { kind: 'pull', repo, error: `host access denied: ${grant.reason}` } });
    return { ok: false, error: `host access denied: ${grant.reason}` };
  }

  if (!validateRepoId(repo)) {
    publishJob(emitter, { seq, jobType: 'action', jobId, status: 'failed', detail: { kind: 'pull', repo, error: 'invalid repo id' } });
    return { ok: false, error: 'invalid repo id' };
  }
  if (mode === 'shell' && !modelsDir) {
    publishJob(emitter, { seq, jobType: 'action', jobId, status: 'failed', detail: { kind: 'pull', repo, error: 'shell mode requires a models directory' } });
    return { ok: false, error: 'shell mode requires a models directory' };
  }

  publishJob(emitter, { seq, jobType: 'action', jobId, status: 'running', detail: { kind: 'pull', repo } });

  try {
    const res = await exec(target, buildPullCommand(mode, repo, modelsDir));
    if (res.code !== 0) {
      const error = `pull failed (exit ${res.code}): ${res.stderr.slice(0, 500)}`;
      publishJob(emitter, { seq, jobType: 'action', jobId, status: 'failed', detail: { kind: 'pull', repo, error } });
      return { ok: false, error };
    }
    publishJob(emitter, { seq, jobType: 'action', jobId, status: 'completed', detail: { kind: 'pull', repo, output: res.stdout.slice(-500) } });
    return { ok: true };
  } catch (err) {
    const error = (err as Error).message ?? String(err);
    publishJob(emitter, { seq, jobType: 'action', jobId, status: 'failed', detail: { kind: 'pull', repo, error } });
    return { ok: false, error };
  }
}
