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

export type MatrixWorkspacePackageStore = 'global' | 'system';

export type MatrixWorkspaceServiceDeclaration =
  | { readonly path: string }
  | { readonly package: string; readonly store?: MatrixWorkspacePackageStore };

export interface MatrixWorkspaceConfig {
  readonly home?: string;
  readonly default?: readonly string[];
  readonly services?: Record<string, MatrixWorkspaceServiceDeclaration>;
}

export interface MatrixWorkspaceInitResult {
  readonly workspaceRoot: string;
  readonly workspaceConfigPath: string;
  readonly home: string;
  readonly created: boolean;
}

export function ensureMatrixWorkspace(workspaceRoot: string): MatrixWorkspaceInitResult {
  const root = path.resolve(workspaceRoot);
  const matrixDir = path.join(root, '.matrix');
  const workspaceConfigPath = path.join(matrixDir, 'workspace.json');
  fs.mkdirSync(matrixDir, { recursive: true });

  let created = false;
  if (!fs.existsSync(workspaceConfigPath)) {
    writeWorkspaceConfig(root, {
      home: '.matrix/home',
      default: [],
      services: {},
    });
    created = true;
  }

  const config = readWorkspaceConfig(root);
  const home = path.resolve(root, config.home ?? '.matrix/home');
  fs.mkdirSync(home, { recursive: true });
  return {
    workspaceRoot: root,
    workspaceConfigPath,
    home,
    created,
  };
}

export function readWorkspaceConfig(workspaceRoot: string): MatrixWorkspaceConfig {
  const workspaceConfigPath = path.join(path.resolve(workspaceRoot), '.matrix', 'workspace.json');
  const parsed = JSON.parse(fs.readFileSync(workspaceConfigPath, 'utf8')) as unknown;
  if (!isRecord(parsed)) {
    throw new Error(`${workspaceConfigPath} must contain a JSON object`);
  }
  return {
    ...(typeof parsed.home === 'string' && parsed.home.trim() ? { home: parsed.home.trim() } : {}),
    ...(Array.isArray(parsed.default) ? { default: parsed.default.filter(isNonEmptyString) } : {}),
    ...(isRecord(parsed.services) ? { services: normalizeServices(parsed.services, workspaceConfigPath) } : {}),
  };
}

export function writeWorkspaceConfig(workspaceRoot: string, config: MatrixWorkspaceConfig): string {
  const root = path.resolve(workspaceRoot);
  const matrixDir = path.join(root, '.matrix');
  const workspaceConfigPath = path.join(matrixDir, 'workspace.json');
  fs.mkdirSync(matrixDir, { recursive: true });
  fs.writeFileSync(workspaceConfigPath, `${JSON.stringify({
    home: config.home ?? '.matrix/home',
    default: config.default ?? [],
    services: config.services ?? {},
  }, null, 2)}\n`, 'utf8');
  return workspaceConfigPath;
}

export function addWorkspaceService(
  workspaceRoot: string,
  serviceId: string,
  declaration: MatrixWorkspaceServiceDeclaration,
  options: { readonly addToDefault?: boolean } = {},
): string {
  ensureMatrixWorkspace(workspaceRoot);
  const config = readWorkspaceConfig(workspaceRoot);
  const services = { ...(config.services ?? {}) };
  services[serviceId] = declaration;
  const currentDefault = config.default ?? [];
  const nextDefault = options.addToDefault === false || currentDefault.includes(serviceId)
    ? currentDefault
    : [...currentDefault, serviceId];
  return writeWorkspaceConfig(workspaceRoot, {
    ...config,
    services,
    default: nextDefault,
  });
}

export function ensureBaseHostServices(workspaceRoot: string): string {
  ensureMatrixWorkspace(workspaceRoot);
  const config = readWorkspaceConfig(workspaceRoot);
  const services = { ...(config.services ?? {}) };

  const systemSource = findSourceCheckoutPackage(workspaceRoot, 'system');
  const hostControlSource = findSourceCheckoutPackage(workspaceRoot, 'host-control');

  if (!services.system) {
    services.system = systemSource
      ? { path: relativeWorkspacePath(workspaceRoot, systemSource) }
      : { package: '@open-matrix/system', store: 'system' };
  }
  if (!services['host-control']) {
    services['host-control'] = hostControlSource
      ? { path: relativeWorkspacePath(workspaceRoot, hostControlSource) }
      : { package: '@open-matrix/host-control', store: 'system' };
  }

  const currentDefault = config.default ?? [];
  const prefix = ['system', 'host-control'];
  return writeWorkspaceConfig(workspaceRoot, {
    ...config,
    services,
    default: [
      ...prefix,
      ...currentDefault.filter((entry) => !prefix.includes(entry)),
    ],
  });
}

export function findSourceCheckoutPackage(workspaceRoot: string, packageDirName: string): string | null {
  const root = path.resolve(workspaceRoot);
  const candidates = [
    path.join(root, 'projects', 'matrix-3', 'packages', packageDirName),
    path.join(root, 'packages', packageDirName),
  ];
  for (const candidate of candidates) {
    if (
      fs.existsSync(path.join(candidate, 'package.json'))
      && fs.existsSync(path.join(candidate, 'matrix.json'))
    ) {
      return candidate;
    }
  }
  return null;
}

export function relativeWorkspacePath(workspaceRoot: string, target: string): string {
  const relative = path.relative(path.resolve(workspaceRoot), path.resolve(target)).replaceAll(path.sep, '/');
  if (!relative) {
    return '.';
  }
  return relative.startsWith('.') ? relative : `./${relative}`;
}

function normalizeServices(
  services: Record<string, unknown>,
  workspaceConfigPath: string,
): Record<string, MatrixWorkspaceServiceDeclaration> {
  const normalized: Record<string, MatrixWorkspaceServiceDeclaration> = {};
  for (const [id, value] of Object.entries(services)) {
    if (!isRecord(value)) {
      throw new Error(`${workspaceConfigPath}: services.${id} must be an object`);
    }
    if (typeof value.path === 'string' && value.path.trim()) {
      normalized[id] = { path: value.path.trim() };
      continue;
    }
    if (typeof value.package === 'string' && value.package.trim()) {
      const store = value.store === 'system' ? 'system' : value.store === 'global' ? 'global' : undefined;
      normalized[id] = {
        package: value.package.trim(),
        ...(store ? { store } : {}),
      };
      continue;
    }
    throw new Error(`${workspaceConfigPath}: services.${id} must declare path or package`);
  }
  return normalized;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function isNonEmptyString(value: unknown): value is string {
  return typeof value === 'string' && value.trim().length > 0;
}
