import * as fs from 'node:fs';
import * as path from 'node:path';
import { packCommand } from './pack.js';
import { readRegistryCredential } from '../utils/auth-store.js';
import {
  readManifestFromPackageDir,
  readVersionFromPackageDir,
  resolveRegistryRoot,
  validateResolvedManifest,
} from '../utils/package-store.js';
import { publishRegistryVersion } from '../utils/registry-store.js';
import {
  packNpmPublishTarball,
  publishNpmTarball,
} from '../utils/npm-registry-client.js';
import { resolvePackageRegistry } from '../utils/config-store.js';
import { loadMatrixServiceManifest } from '../utils/service-manifest.js';
import { extractDiscoveryMetadata, type IPackageDiscoveryMetadata } from '../utils/discovery-extractor.js';
import { publishPackageDiscoveryMetadata } from '../utils/discovery-index.js';

type JsonRecord = Record<string, unknown>;

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

export interface IPublishCommandOptions {
  readonly cwd?: string;
  readonly registryDir?: string;
  readonly registry?: string;
  readonly credentialsPath?: string;
  readonly dryRun?: boolean;
}

export interface IPublishCommandResult {
  readonly packageName: string;
  readonly version: string;
  readonly tarballPath: string;
  readonly registryTarballPath?: string;
  readonly registryUrl?: string;
  readonly registryRoot: string;
  readonly discoveryMetadata?: IPackageDiscoveryMetadata;
  readonly dryRun: boolean;
}

function ensurePublishPreflight(packageDir: string): { packageName: string; manifest: JsonRecord } {
  const { packageName, manifest } = readManifestFromPackageDir(packageDir);
  validateResolvedManifest(packageName, manifest);

  const runtime = asRecord(manifest.runtime);
  const runtimeEntry = typeof runtime?.entry === 'string' ? runtime.entry.trim() : '';
  if (!runtimeEntry) {
    throw new Error('MX_MANIFEST_INVALID: runtime.entry is required');
  }
  if (!fs.existsSync(path.join(packageDir, runtimeEntry))) {
    throw new Error(`MX_MANIFEST_INVALID: runtime.entry does not exist (${runtimeEntry})`);
  }

  const hasComponents = Array.isArray(manifest.components) && manifest.components.length > 0;
  const root = asRecord(manifest.root);
  const hasRoot = typeof root?.type === 'string'
    && root.type.trim().length > 0
    && typeof root?.export === 'string'
    && root.export.trim().length > 0;
  const hasServiceFactory = asRecord(runtime?.factory) !== null;
  if (hasServiceFactory) {
    loadMatrixServiceManifest(packageDir);
  }
  const webapp = asRecord(manifest.webapp);
  const webappDistDir = typeof webapp?.distDir === 'string' ? webapp.distDir.trim() : '';
  const webappEntry = typeof webapp?.entry === 'string' ? webapp.entry.trim() : '';
  const hasWebapp = Boolean(webapp && webappDistDir && webappEntry);
  if (hasWebapp && !fs.existsSync(path.join(packageDir, webappDistDir, webappEntry))) {
    throw new Error(`MX_MANIFEST_INVALID: webapp entry does not exist (${path.join(webappDistDir, webappEntry)})`);
  }

  if (!hasComponents && !hasWebapp && !hasRoot && !hasServiceFactory) {
    throw new Error('MX_MANIFEST_INVALID: declare a root actor, component, webapp, or matrix.json runtime.factory');
  }

  return { packageName, manifest };
}

export async function publishCommand(options: IPublishCommandOptions = {}): Promise<IPublishCommandResult> {
  const cwd = path.resolve(options.cwd ?? process.cwd());
  const registryRoot = resolveRegistryRoot(cwd, options.registryDir);
  if (options.registry && options.registryDir) {
    throw new Error('MX_REGISTRY_INVALID: use either --registry or --registry-dir, not both');
  }
  const registryUrl = options.registryDir && !options.registry
    ? undefined
    : resolvePackageRegistry(options.registry, { cwd }).registry;

  const auth = readRegistryCredential(cwd, {
    ...(registryUrl ? { registry: registryUrl } : {}),
    credentialsPath: options.credentialsPath,
  });
  if (!auth.credential) {
    throw new Error('MX_AUTH_REQUIRED: run `mx login` before publish');
  }

  const { packageName } = ensurePublishPreflight(cwd);
  const version = readVersionFromPackageDir(cwd);
  const discoveryMetadata = extractDiscoveryMetadata(cwd);
  const packed = registryUrl
    ? await packNpmPublishTarball(cwd, path.join(cwd, '.mx-pack', 'npm'))
    : await packCommand({
      cwd,
      outDir: '.mx-pack',
    });

  if (options.dryRun) {
    return {
      packageName,
      version,
      tarballPath: packed.tarballPath,
      ...(registryUrl ? { registryUrl } : {}),
      registryRoot,
      discoveryMetadata,
      dryRun: true,
    };
  }

  if (registryUrl) {
    const result = await publishNpmTarball(
      packed.tarballPath,
      registryUrl,
      cwd,
      options.credentialsPath,
    );
    if (result.exitCode !== 0) {
      const output = [result.stderr, result.stdout]
        .filter((value) => typeof value === 'string' && value.trim().length > 0)
        .join('\n')
        .trim();
      throw new Error(
        `MX_REGISTRY_PUBLISH_FAILED: failed to publish ${packageName}@${version} to ${registryUrl}`
        + (output ? `\n${output}` : ''),
      );
    }
    return {
      packageName,
      version,
      tarballPath: packed.tarballPath,
      registryUrl,
      registryRoot,
      discoveryMetadata,
      dryRun: false,
    };
  }

  const published = publishRegistryVersion(
    registryRoot,
    packageName,
    version,
    packed.tarballPath,
    auth.credential.username,
  );
  publishPackageDiscoveryMetadata(registryRoot, discoveryMetadata, auth.credential.username);

  return {
    packageName,
    version,
    tarballPath: packed.tarballPath,
    registryTarballPath: published.registryTarballPath,
    registryRoot,
    discoveryMetadata,
    dryRun: false,
  };
}
