import * as fs from 'node:fs';
import * as path from 'node:path';

type JsonRecord = Record<string, unknown>;

export interface ILoadedMatrixWebappManifest {
  readonly packageDir: string;
  readonly packageName: string;
  readonly manifestPath: string;
  readonly distDir: string;
  readonly entryFile: string;
  readonly appName: string;
  readonly displayName?: string;
  readonly icon?: string;
  readonly navOrder?: number;
  readonly description?: string;
  readonly shells?: readonly string[];
}

export function loadMatrixWebappManifest(packageDir: string): ILoadedMatrixWebappManifest | null {
  const resolvedPackageDir = path.resolve(packageDir);
  const manifestPath = path.join(resolvedPackageDir, 'matrix.json');
  if (!fs.existsSync(manifestPath)) {
    return null;
  }

  const manifest = readJsonObject(manifestPath, 'matrix.json');
  const webapp = asRecord(manifest.webapp);
  if (!webapp) {
    return null;
  }

  const packageName = readRequiredString(
    manifest.name,
    `matrix.json in ${resolvedPackageDir} requires a non-empty "name"`,
  );
  const distDir = path.resolve(
    resolvedPackageDir,
    readOptionalString(webapp.distDir) ?? 'dist',
  );
  const entryFile = readOptionalString(webapp.entry) ?? 'index.html';
  const appName = readOptionalString(webapp.appName) ?? packageName.replace(/^@[^/]+\//, '');
  const displayName = readOptionalString(webapp.displayName);
  const icon = readOptionalString(webapp.icon);
  const navOrder = readOptionalNumber(webapp.navOrder);
  const description = readOptionalString(webapp.description);
  const shells = readStringArray(webapp.shells);

  return {
    packageDir: resolvedPackageDir,
    packageName,
    manifestPath,
    distDir,
    entryFile,
    appName,
    ...(displayName ? { displayName } : {}),
    ...(icon ? { icon } : {}),
    ...(typeof navOrder === 'number' ? { navOrder } : {}),
    ...(description ? { description } : {}),
    ...(shells.length > 0 ? { shells } : {}),
  };
}

function readJsonObject(filePath: string, label: string): JsonRecord {
  try {
    const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
    if (!isRecord(parsed)) {
      throw new Error(`${label} must contain a JSON object`);
    }
    return parsed;
  } catch (error) {
    throw new Error(`Failed to parse ${label}: ${error instanceof Error ? error.message : String(error)}`);
  }
}

function readRequiredString(value: unknown, message: string): string {
  const resolved = readOptionalString(value);
  if (!resolved) {
    throw new Error(message);
  }
  return resolved;
}

function readOptionalString(value: unknown): string | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

function readOptionalNumber(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}

function readStringArray(value: unknown): readonly string[] {
  if (!Array.isArray(value)) {
    return [];
  }
  const entries = value
    .map((entry) => readOptionalString(entry))
    .filter((entry): entry is string => typeof entry === 'string');
  return [...new Set(entries)];
}

function isRecord(value: unknown): value is JsonRecord {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function asRecord(value: unknown): JsonRecord | undefined {
  return isRecord(value) ? value : undefined;
}
