'use node';

import { getAuthUserId } from '@convex-dev/auth/server';
import { ConvexError, v } from 'convex/values';

import type { Id } from './_generated/dataModel';
import type { ActionCtx } from './_generated/server';
import { internal } from './_generated/api';
import { action } from './_generated/server';
import { normalizeDotfilePath } from './model';
import { decryptSecret, encryptSecret } from './secretCrypto';

const MAX_FILE_BYTES = 512 * 1024; // 512 KB per dotfile

const getRequiredUserId = async (ctx: ActionCtx): Promise<Id<'users'>> => {
  const userId = await getAuthUserId(ctx);
  if (!userId) throw new ConvexError('Not authenticated.');
  return userId;
};

const requireWorkerToken = (workerToken: string) => {
  const expected = process.env.SPOON_WORKER_TOKEN;
  if (!expected) throw new ConvexError('Worker token is not configured.');
  if (workerToken !== expected) throw new ConvexError('Invalid worker token.');
};

const putOne = async (
  ctx: ActionCtx,
  ownerId: Id<'users'>,
  rawPath: string,
  content: string,
  isExecutable?: boolean,
) => {
  const path = normalizeDotfilePath(rawPath);
  const size = Buffer.byteLength(content, 'utf8');
  if (size > MAX_FILE_BYTES) {
    throw new ConvexError(`${path} is too large (max 512 KB).`);
  }
  await ctx.runMutation(internal.userDotfiles.upsertFileInternal, {
    ownerId,
    path,
    encryptedContent: encryptSecret(content),
    size,
    isExecutable,
  });
  return path;
};

/** Create/update a single dotfile (used by the in-app editor and "new file"). */
export const putFile = action({
  args: {
    path: v.string(),
    content: v.string(),
    isExecutable: v.optional(v.boolean()),
  },
  handler: async (ctx, args): Promise<{ path: string }> => {
    const ownerId = await getRequiredUserId(ctx);
    const path = await putOne(
      ctx,
      ownerId,
      args.path,
      args.content,
      args.isExecutable,
    );
    return { path };
  },
});

/** Bulk import (drag-and-drop folder/files). */
export const importFiles = action({
  args: {
    files: v.array(
      v.object({
        path: v.string(),
        content: v.string(),
        isExecutable: v.optional(v.boolean()),
      }),
    ),
  },
  handler: async (ctx, args): Promise<{ imported: number }> => {
    const ownerId = await getRequiredUserId(ctx);
    for (const file of args.files) {
      await putOne(ctx, ownerId, file.path, file.content, file.isExecutable);
    }
    return { imported: args.files.length };
  },
});

/** Decrypts one file's content for the editor (owner only). */
export const getFileContent = action({
  args: { fileId: v.id('userDotfiles') },
  handler: async (ctx, { fileId }): Promise<{ content: string }> => {
    const ownerId = await getRequiredUserId(ctx);
    const file = await ctx.runQuery(internal.userDotfiles.getRawFileInternal, {
      fileId,
    });
    if (file?.ownerId !== ownerId) {
      throw new ConvexError('Dotfile not found.');
    }
    return { content: decryptSecret(file.encryptedContent) };
  },
});

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

/** Worker-facing: the job owner's full environment with dotfiles decrypted. */
export const getEnvironmentForJob = action({
  args: { workerToken: v.string(), jobId: v.id('agentJobs') },
  handler: async (ctx, args): Promise<WorkerEnvironment | null> => {
    requireWorkerToken(args.workerToken);
    const raw = await ctx.runQuery(
      internal.userEnvironment.getRawEnvironmentForJobInternal,
      { jobId: args.jobId },
    );
    if (!raw) return null;
    return {
      username: raw.username,
      enabled: raw.enabled,
      dotfilesRepoUrl: raw.dotfilesRepoUrl,
      dotfilesRepoRef: raw.dotfilesRepoRef,
      setupCommand: raw.setupCommand,
      files: raw.files.map((f) => ({
        path: f.path,
        content: decryptSecret(f.encryptedContent),
        isExecutable: f.isExecutable,
      })),
    };
  },
});
