import { app } from 'electron';
import * as fsp from 'fs/promises';
import * as path from 'path';

export const DEFAULT_USER_DATA_SUBDIRS = [
  'server',
  'direct-messages',
  'plugins',
  'plugin-bundles',
  'plugin-cache',
  'themes',
  'metoyou'
] as const;

export function isPathInside(parentPath: string, candidatePath: string): boolean {
  const relativePath = path.relative(parentPath, candidatePath);

  return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
}

async function realpathOrSelf(filePath: string): Promise<string> {
  try {
    return await fsp.realpath(filePath);
  } catch {
    return path.resolve(filePath);
  }
}

function normalizeAllowedSubdirs(allowedSubdirs: readonly string[]): string[] {
  return allowedSubdirs.map((entry) => entry.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')).filter(Boolean);
}

export async function assertPathUnderRoot(
  rootPath: string,
  candidatePath: string,
  allowedSubdirs: readonly string[] = DEFAULT_USER_DATA_SUBDIRS
): Promise<string> {
  if (typeof candidatePath !== 'string' || !candidatePath.trim()) {
    throw new Error('Invalid file path');
  }

  const [realRoot, realCandidate] = await Promise.all([realpathOrSelf(rootPath), realpathOrSelf(candidatePath)]);

  if (!isPathInside(realRoot, realCandidate)) {
    throw new Error('Path is outside allowed app-data paths');
  }

  const relativePath = path.relative(realRoot, realCandidate).replace(/\\/g, '/');
  const [topLevelSegment] = relativePath.split('/');

  if (!topLevelSegment || !normalizeAllowedSubdirs(allowedSubdirs).includes(topLevelSegment)) {
    throw new Error('Path is outside allowed app-data paths');
  }

  return realCandidate;
}

export async function assertPathUnderUserData(
  candidatePath: string,
  allowedSubdirs: readonly string[] = DEFAULT_USER_DATA_SUBDIRS
): Promise<string> {
  return assertPathUnderRoot(app.getPath('userData'), candidatePath, allowedSubdirs);
}

const grantedPluginReadRoots = new Set<string>();

export function grantPluginReadRoot(rootPath: string): void {
  if (typeof rootPath !== 'string' || !rootPath.trim()) {
    return;
  }

  grantedPluginReadRoots.add(path.resolve(rootPath));
}

export function clearGrantedPluginReadRoots(): void {
  grantedPluginReadRoots.clear();
}

async function assertPathUnderGrantedPluginRoot(candidatePath: string): Promise<string> {
  const realCandidate = await realpathOrSelf(candidatePath);

  for (const rootPath of grantedPluginReadRoots) {
    const realRoot = await realpathOrSelf(rootPath);

    if (isPathInside(realRoot, realCandidate)) {
      return realCandidate;
    }
  }

  throw new Error('Path is outside allowed app-data paths');
}

/** Resolves readable paths under app data or user-granted plugin source roots. */
export async function resolveReadablePath(candidatePath: string): Promise<string | null> {
  if (typeof candidatePath !== 'string' || !candidatePath.trim()) {
    return null;
  }

  try {
    return await assertPathUnderUserData(candidatePath);
  } catch {
    try {
      return await assertPathUnderGrantedPluginRoot(candidatePath);
    } catch {
      return null;
    }
  }
}
