import { ConvexError, v } from 'convex/values';

import type { Doc } from './_generated/dataModel';
import {
  internalMutation,
  internalQuery,
  mutation,
  query,
} from './_generated/server';
import { getRequiredUserId, normalizeDotfilePath } from './model';

const fileMeta = (file: Doc<'userDotfiles'>) => ({
  _id: file._id,
  path: file.path,
  size: file.size,
  isExecutable: file.isExecutable ?? false,
  updatedAt: file.updatedAt,
});

/** Lists the user's dotfile tree (metadata only; content is fetched per-file). */
export const listMine = query({
  args: {},
  handler: async (ctx) => {
    const ownerId = await getRequiredUserId(ctx);
    const files = await ctx.db
      .query('userDotfiles')
      .withIndex('by_owner', (q) => q.eq('ownerId', ownerId))
      .collect();
    return files.map(fileMeta).sort((a, b) => a.path.localeCompare(b.path));
  },
});

export const remove = mutation({
  args: { fileId: v.id('userDotfiles') },
  handler: async (ctx, { fileId }) => {
    const ownerId = await getRequiredUserId(ctx);
    const file = await ctx.db.get(fileId);
    if (file?.ownerId !== ownerId) throw new ConvexError('Dotfile not found.');
    await ctx.db.delete(fileId);
    return { success: true };
  },
});

/** Removes every file under a directory prefix (e.g. deleting ".config/nvim"). */
export const removeDirectory = mutation({
  args: { prefix: v.string() },
  handler: async (ctx, { prefix }) => {
    const ownerId = await getRequiredUserId(ctx);
    const normalized = normalizeDotfilePath(prefix);
    const files = await ctx.db
      .query('userDotfiles')
      .withIndex('by_owner', (q) => q.eq('ownerId', ownerId))
      .collect();
    const matches = files.filter(
      (f) => f.path === normalized || f.path.startsWith(`${normalized}/`),
    );
    await Promise.all(matches.map((f) => ctx.db.delete(f._id)));
    return { removed: matches.length };
  },
});

export const rename = mutation({
  args: { fileId: v.id('userDotfiles'), path: v.string() },
  handler: async (ctx, { fileId, path }) => {
    const ownerId = await getRequiredUserId(ctx);
    const file = await ctx.db.get(fileId);
    if (file?.ownerId !== ownerId) throw new ConvexError('Dotfile not found.');
    const normalized = normalizeDotfilePath(path);
    const clash = await ctx.db
      .query('userDotfiles')
      .withIndex('by_owner_path', (q) =>
        q.eq('ownerId', ownerId).eq('path', normalized),
      )
      .unique();
    if (clash && clash._id !== fileId) {
      throw new ConvexError(`A dotfile already exists at ${normalized}.`);
    }
    await ctx.db.patch(fileId, { path: normalized, updatedAt: Date.now() });
    return { success: true };
  },
});

// Read by the decrypting Node action (userDotfilesNode.getFileContent).
export const getRawFileInternal = internalQuery({
  args: { fileId: v.id('userDotfiles') },
  handler: async (ctx, { fileId }) => await ctx.db.get(fileId),
});

// Called by the encrypting Node action (userDotfilesNode). Upserts one file by
// (owner, path).
export const upsertFileInternal = internalMutation({
  args: {
    ownerId: v.id('users'),
    path: v.string(),
    encryptedContent: v.string(),
    size: v.number(),
    isExecutable: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query('userDotfiles')
      .withIndex('by_owner_path', (q) =>
        q.eq('ownerId', args.ownerId).eq('path', args.path),
      )
      .unique();
    const now = Date.now();
    if (existing) {
      await ctx.db.patch(existing._id, {
        encryptedContent: args.encryptedContent,
        size: args.size,
        isExecutable: args.isExecutable,
        updatedAt: now,
      });
      return existing._id;
    }
    return await ctx.db.insert('userDotfiles', {
      ...args,
      updatedAt: now,
    });
  },
});
