import { createHash } from 'node:crypto';
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { ConvexHttpClient } from 'convex/browser';

import type { Id } from '@spoon/backend/convex/_generated/dataModel.js';
import { api } from '@spoon/backend/convex/_generated/api.js';

import { env } from './env';
import { runExecInContainer } from './runtime/docker';

const client = new ConvexHttpClient(env.convexUrl);

export type UserEnvironment = {
  username: string;
  enabled: boolean;
  dotfilesRepoUrl?: string;
  dotfilesRepoRef?: string;
  setupCommand?: string;
  files: { path: string; content: string; isExecutable: boolean }[];
};

/** The job owner's resolved environment (username + dotfiles, decrypted). */
export const fetchUserEnvironment = async (
  jobId: Id<'agentJobs'>,
): Promise<UserEnvironment | null> =>
  await client.action(api.userDotfilesNode.getEnvironmentForJob, {
    workerToken: env.workerToken,
    jobId,
  });

const shellQuote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;

// Keep a written path inside the home directory.
const safeHomeJoin = (homeDir: string, relPath: string) => {
  const target = path.resolve(homeDir, relPath);
  const root = path.resolve(homeDir);
  if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
    throw new Error(`Refusing to write dotfile outside home: ${relPath}`);
  }
  return target;
};

/**
 * Materializes the persistent per-user home: a `.bash_profile` so login shells
 * load `~/.bashrc`; (when configured and changed) a clone of the public dotfiles
 * repo + the setup command, run inside the job image so the user's tools/paths
 * apply; then the editable overlay files (which win over the repo). Idempotent
 * via a hash marker so the repo/setup only re-runs when the config changes.
 */
export const materializeUserHome = async (args: {
  homeDir: string;
  containerHome: string;
  boxName: string;
  userEnv: UserEnvironment;
  redact: (value: string) => string;
}): Promise<void> => {
  const { homeDir, containerHome, boxName, userEnv, redact } = args;
  await mkdir(homeDir, { recursive: true });

  // A mounted home has no /etc/skel, so ensure login shells source ~/.bashrc.
  const bashProfile = path.join(homeDir, '.bash_profile');
  await readFile(bashProfile, 'utf8').catch(async () => {
    await writeFile(
      bashProfile,
      '# Spoon: load ~/.bashrc for login shells.\n[ -f ~/.bashrc ] && . ~/.bashrc\n',
    );
  });

  if (!userEnv.enabled) return;

  // Public dotfiles repo + setup command, only re-run when the config changes.
  if (userEnv.dotfilesRepoUrl) {
    const configHash = createHash('sha256')
      .update(
        JSON.stringify({
          repo: userEnv.dotfilesRepoUrl,
          ref: userEnv.dotfilesRepoRef ?? '',
          setup: userEnv.setupCommand ?? '',
        }),
      )
      .digest('hex');
    const markerPath = path.join(homeDir, '.spoon', 'env-hash');
    const previous = await readFile(markerPath, 'utf8').catch(() => '');
    if (previous.trim() !== configHash) {
      const branch = userEnv.dotfilesRepoRef
        ? `--branch ${shellQuote(userEnv.dotfilesRepoRef)} `
        : '';
      const script = [
        'set -e',
        'rm -rf ~/.dotfiles',
        `git clone --depth 1 ${branch}${shellQuote(userEnv.dotfilesRepoUrl)} ~/.dotfiles`,
        userEnv.setupCommand
          ? `cd ~/.dotfiles && bash ${shellQuote(userEnv.setupCommand)}`
          : '',
      ]
        .filter(Boolean)
        .join('\n');
      await runExecInContainer({
        containerName: boxName,
        command: ['bash', '-lc', script],
        containerCwd: containerHome,
        environment: { HOME: containerHome },
        redact,
        timeoutMs: env.jobTimeoutMs,
      });
      await mkdir(path.dirname(markerPath), { recursive: true });
      await writeFile(markerPath, configHash);
    }
  }

  // Editable overlay tree (wins over the repo/setup output).
  for (const file of userEnv.files) {
    const target = safeHomeJoin(homeDir, file.path);
    await mkdir(path.dirname(target), { recursive: true });
    await writeFile(target, file.content);
    if (file.isExecutable) await chmod(target, 0o755);
  }
};
