import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { validateManifestForMxCli } from './manifestValidator.js';

type JsonRecord = Record<string, unknown>;

export interface IResolvedManifest {
  readonly packageName: string;
  readonly manifest: JsonRecord;
}

const DEFAULT_GLOBAL_PACKAGES_ROOT = path.join(os.homedir(), '.matrix', 'packages');
const DEFAULT_GLOBAL_REGISTRY_ROOT = path.join(os.homedir(), '.matrix', 'registry');

export function resolvePackagesRoot(cwd: string, packagesDir?: string): string {
  return path.resolve(cwd, packagesDir ?? DEFAULT_GLOBAL_PACKAGES_ROOT);
}

export function resolvePackagesNodeModulesRoot(cwd: string, packagesDir?: string): string {
  return path.join(resolvePackagesRoot(cwd, packagesDir), 'node_modules');
}

export function resolveRegistryRoot(cwd: string, registryDir?: string): string {
  return path.resolve(cwd, registryDir ?? DEFAULT_GLOBAL_REGISTRY_ROOT);
}

export function packageNameToPath(packageName: string): string {
  if (packageName.includes('/')) {
    return packageName;
  }
  return packageName.trim();
}

export function listInstalledPackageNames(nodeModulesRoot: string): string[] {
  if (!fs.existsSync(nodeModulesRoot)) {
    return [];
  }
  const entries = fs.readdirSync(nodeModulesRoot, { withFileTypes: true });
  const names: string[] = [];

  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    if (entry.name.startsWith('.')) continue;

    if (entry.name.startsWith('@')) {
      const scopeRoot = path.join(nodeModulesRoot, entry.name);
      for (const scoped of fs.readdirSync(scopeRoot, { withFileTypes: true })) {
        if (!scoped.isDirectory()) continue;
        names.push(`${entry.name}/${scoped.name}`);
      }
      continue;
    }

    names.push(entry.name);
  }

  return names.sort((a, b) => a.localeCompare(b));
}

function readJsonFile(filePath: string): JsonRecord {
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    throw new Error(`${filePath} must contain a JSON object`);
  }
  return parsed as JsonRecord;
}

function asRecord(value: unknown): JsonRecord | null {
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
    return null;
  }
  return value as JsonRecord;
}

function readOptionalString(value: unknown): string {
  return typeof value === 'string' ? value.trim() : '';
}

function readPackageJsonIfExists(packageDir: string): JsonRecord | null {
  const packageJsonPath = path.join(packageDir, 'package.json');
  return fs.existsSync(packageJsonPath) ? readJsonFile(packageJsonPath) : null;
}

function packageNameComparable(value: string): string {
  const trimmed = value.trim();
  if (trimmed.startsWith('@matrix/')) {
    return trimmed.slice('@matrix/'.length);
  }
  if (trimmed.startsWith('@open-matrix/')) {
    return trimmed.slice('@open-matrix/'.length);
  }
  if (trimmed.startsWith('@') && trimmed.includes('/')) {
    return trimmed.slice(trimmed.indexOf('/') + 1);
  }
  return trimmed;
}

function assertMatrixAndPackageJsonAgree(
  packageDir: string,
  matrixManifest: JsonRecord,
  packageJson: JsonRecord | null,
): void {
  if (!packageJson) {
    return;
  }

  const matrixName = readOptionalString(matrixManifest.name);
  const packageName = readOptionalString(packageJson.name);
  if (
    matrixName
    && packageName
    && matrixName !== packageName
    && packageNameComparable(matrixName) !== packageNameComparable(packageName)
  ) {
    throw new Error(
      `MX_PACKAGE_IDENTITY_MISMATCH: matrix.json name (${matrixName}) `
      + `does not match package.json name (${packageName}) in ${packageDir}`,
    );
  }

  const matrixVersion = readOptionalString(matrixManifest.version);
  const packageVersion = readOptionalString(packageJson.version);
  if (matrixVersion && packageVersion && matrixVersion !== packageVersion) {
    throw new Error(
      `MX_PACKAGE_VERSION_MISMATCH: matrix.json version (${matrixVersion}) `
      + `does not match package.json version (${packageVersion}) in ${packageDir}`,
    );
  }
}

export function readManifestFromPackageDir(packageDir: string): IResolvedManifest {
  const matrixJsonPath = path.join(packageDir, 'matrix.json');
  if (fs.existsSync(matrixJsonPath)) {
    const matrixManifest = readJsonFile(matrixJsonPath);
    const packageJson = readPackageJsonIfExists(packageDir);
    assertMatrixAndPackageJsonAgree(packageDir, matrixManifest, packageJson);
    const packageName = readOptionalString(matrixManifest.name);
    if (!packageName) {
      throw new Error(`matrix.json must contain a non-empty "name" field (${matrixJsonPath})`);
    }
    return {
      packageName,
      manifest: matrixManifest,
    };
  }

  const packageJsonPath = path.join(packageDir, 'package.json');
  if (!fs.existsSync(packageJsonPath)) {
    throw new Error(`Package manifest not found in ${packageDir} (expected matrix.json or package.json)`);
  }

  const packageJson = readJsonFile(packageJsonPath);
  const packageName = typeof packageJson.name === 'string' ? packageJson.name.trim() : '';
  if (!packageName) {
    throw new Error(`package.json must contain a non-empty "name" field (${packageJsonPath})`);
  }

  const matrix = asRecord(packageJson.matrix) ?? {};
  const manifest: JsonRecord = {
    name: packageName,
    version: packageJson.version,
    runtime: matrix.runtime,
    components: matrix.components,
    permissions: matrix.permissions,
    install: matrix.install,
  };
  return { packageName, manifest };
}

export function validateResolvedManifest(packageName: string, manifest: JsonRecord): void {
  const validation = validateManifestForMxCli({
    ...manifest,
    name: manifest.name ?? packageName,
  });
  if (!validation.valid) {
    throw new Error(
      `Manifest validation failed for ${packageName}: ${validation.errors.join('; ')}`
    );
  }
}

export function readPackageJson(packageDir: string): JsonRecord {
  const packageJsonPath = path.join(packageDir, 'package.json');
  if (!fs.existsSync(packageJsonPath)) {
    throw new Error(`package.json not found in ${packageDir}`);
  }
  return readJsonFile(packageJsonPath);
}

export function readVersionFromPackageDir(packageDir: string): string {
  const matrixJsonPath = path.join(packageDir, 'matrix.json');
  const packageJson = readPackageJsonIfExists(packageDir);
  if (fs.existsSync(matrixJsonPath)) {
    const matrixManifest = readJsonFile(matrixJsonPath);
    assertMatrixAndPackageJsonAgree(packageDir, matrixManifest, packageJson);
    const matrixVersion = readOptionalString(matrixManifest.version);
    const packageVersion = readOptionalString(packageJson?.version);
    if (packageVersion.length > 0) {
      return packageVersion;
    }
    if (matrixVersion.length > 0) {
      return matrixVersion;
    }
  }

  const version = readOptionalString((packageJson ?? readPackageJson(packageDir)).version);
  if (!version) {
    throw new Error(`Unable to resolve package version in ${packageDir}`);
  }
  return version;
}
