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

const PLUGINS_FOLDER_NAME = 'plugins';
const MANIFEST_FILE_NAMES = ['toju-plugin.json', 'plugin.json'] as const;

export interface LocalPluginManifestDescriptor {
  discoveredAt: number;
  entrypointPath?: string;
  pluginRootUrl: string;
  manifest: unknown;
  manifestPath: string;
  pluginRoot: string;
  readmePath?: string;
}

export interface LocalPluginDiscoveryError {
  manifestPath?: string;
  message: string;
  pluginRoot?: string;
}

export interface LocalPluginDiscoveryResult {
  errors: LocalPluginDiscoveryError[];
  plugins: LocalPluginManifestDescriptor[];
  pluginsPath: string;
}

function resolvePluginsPath(): string {
  return path.join(app.getPath('userData'), PLUGINS_FOLDER_NAME);
}

async function ensurePluginsPath(): Promise<string> {
  const pluginsPath = resolvePluginsPath();

  await fsp.mkdir(pluginsPath, { recursive: true });

  return pluginsPath;
}

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

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

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

function readManifestPath(manifestRecord: Record<string, unknown>, key: string): string | undefined {
  const value = manifestRecord[key];

  return typeof value === 'string' && value.trim()
    ? value.trim()
    : undefined;
}

async function resolveManifestRelativeFile(pluginRoot: string, relativeFilePath: string | undefined): Promise<string | undefined> {
  if (!relativeFilePath || path.isAbsolute(relativeFilePath)) {
    return undefined;
  }

  const normalizedPath = path.normalize(relativeFilePath);

  if (normalizedPath.startsWith('..')) {
    return undefined;
  }

  const candidatePath = path.join(pluginRoot, normalizedPath);
  const [realPluginRoot, realCandidatePath] = await Promise.all([realpathOrSelf(pluginRoot), realpathOrSelf(candidatePath)]);

  if (!isPathInside(realPluginRoot, realCandidatePath)) {
    return undefined;
  }

  try {
    const stats = await fsp.stat(realCandidatePath);

    return stats.isFile() ? realCandidatePath : undefined;
  } catch {
    return undefined;
  }
}

async function findManifestPath(pluginRoot: string): Promise<string | undefined> {
  for (const fileName of MANIFEST_FILE_NAMES) {
    const manifestPath = path.join(pluginRoot, fileName);

    try {
      const stats = await fsp.stat(manifestPath);

      if (stats.isFile()) {
        return manifestPath;
      }
    } catch {
      // Missing manifest candidates are expected while scanning folders.
    }
  }

  return undefined;
}

async function readPluginManifest(pluginRoot: string, manifestPath: string): Promise<LocalPluginManifestDescriptor> {
  const text = await fsp.readFile(manifestPath, 'utf8');
  const manifest = JSON.parse(text) as unknown;
  const manifestRecord = manifest && typeof manifest === 'object' && !Array.isArray(manifest)
    ? manifest as Record<string, unknown>
    : {};
  const entrypointPromise = resolveManifestRelativeFile(pluginRoot, readManifestPath(manifestRecord, 'entrypoint'));
  const readmePromise = resolveManifestRelativeFile(pluginRoot, readManifestPath(manifestRecord, 'readme'));
  const [entrypointPath, readmePath] = await Promise.all([entrypointPromise, readmePromise]);

  return {
    discoveredAt: Date.now(),
    entrypointPath,
    pluginRootUrl: pathToFileURL(pluginRoot + path.sep).toString(),
    manifest,
    manifestPath,
    pluginRoot,
    readmePath
  };
}

export async function getLocalPluginsPath(): Promise<string> {
  return await ensurePluginsPath();
}

export async function listLocalPluginManifests(): Promise<LocalPluginDiscoveryResult> {
  const pluginsPath = await ensurePluginsPath();
  const entries = await fsp.readdir(pluginsPath, { withFileTypes: true });
  const plugins: LocalPluginManifestDescriptor[] = [];
  const errors: LocalPluginDiscoveryError[] = [];

  for (const entry of entries.filter((candidate) => candidate.isDirectory())) {
    const pluginRoot = path.join(pluginsPath, entry.name);
    const manifestPath = await findManifestPath(pluginRoot);

    if (!manifestPath) {
      continue;
    }

    try {
      plugins.push(await readPluginManifest(pluginRoot, manifestPath));
    } catch (error) {
      errors.push({
        manifestPath,
        message: error instanceof Error ? error.message : 'Unable to read plugin manifest',
        pluginRoot
      });
    }
  }

  return {
    errors,
    plugins: plugins.sort((left, right) => left.pluginRoot.localeCompare(right.pluginRoot)),
    pluginsPath
  };
}
