/**
 * install-manifest.ts
 *
 * Read/write helpers for ~/.config/mosaic/.install-manifest.json
 *
 * The manifest is the authoritative record of what the installer mutated on the
 * host system so that `mosaic uninstall` can precisely reverse every change.
 * If the manifest is absent the uninstaller falls back to heuristic mode and
 * warns the user.
 */

import { readFileSync, writeFileSync, existsSync, chmodSync } from 'node:fs';
import { join } from 'node:path';

export const MANIFEST_FILENAME = '.install-manifest.json';
export const MANIFEST_VERSION = 1;

/** A single runtime asset copy recorded during install. */
export interface RuntimeAssetCopy {
  /** Absolute path to the source file in MOSAIC_HOME (or the npm package). */
  source: string;
  /** Absolute path to the destination on the host. */
  dest: string;
  /**
   * Absolute path to the backup that was created when an existing file was
   * displaced.  Undefined when no pre-existing file was found.
   */
  backup?: string;
}

/** The full shape of the install manifest (version 1). */
export interface InstallManifest {
  version: 1;
  /** ISO-8601 timestamp of when the install completed. */
  installedAt: string;
  /** Version of @mosaicstack/mosaic that was installed. */
  cliVersion: string;
  /** Framework schema version (integer) that was installed. */
  frameworkVersion: number;
  mutations: {
    /** Directories that were created by the installer. */
    directories: string[];
    /** npm global packages that were installed. */
    npmGlobalPackages: string[];
    /**
     * Exact lines that were appended to ~/.npmrc.
     * Each entry is the full line text (no trailing newline).
     */
    npmrcLines: string[];
    /**
     * Shell profile edits — each entry is an object recording which file was
     * edited and what line was appended.
     */
    shellProfileEdits: Array<{ file: string; line: string }>;
    /** Runtime asset copies performed by mosaic-link-runtime-assets. */
    runtimeAssetCopies: RuntimeAssetCopy[];
  };
}

/** Default empty mutations block. */
function emptyMutations(): InstallManifest['mutations'] {
  return {
    directories: [],
    npmGlobalPackages: [],
    npmrcLines: [],
    shellProfileEdits: [],
    runtimeAssetCopies: [],
  };
}

/**
 * Build a new manifest with sensible defaults.
 * Callers fill in the mutation fields before persisting.
 */
export function createManifest(
  cliVersion: string,
  frameworkVersion: number,
  partial?: Partial<InstallManifest['mutations']>,
): InstallManifest {
  return {
    version: MANIFEST_VERSION,
    installedAt: new Date().toISOString(),
    cliVersion,
    frameworkVersion,
    mutations: { ...emptyMutations(), ...partial },
  };
}

/**
 * Return the absolute path to the manifest file.
 */
export function manifestPath(mosaicHome: string): string {
  return join(mosaicHome, MANIFEST_FILENAME);
}

/**
 * Read the manifest from disk.
 * Returns `undefined` if the file does not exist or cannot be parsed.
 * Never throws — callers decide how to handle heuristic-fallback mode.
 */
export function readManifest(mosaicHome: string): InstallManifest | undefined {
  const p = manifestPath(mosaicHome);
  if (!existsSync(p)) return undefined;
  try {
    const raw = readFileSync(p, 'utf8');
    const parsed: unknown = JSON.parse(raw);
    if (!isValidManifest(parsed)) return undefined;
    return parsed;
  } catch {
    return undefined;
  }
}

/**
 * Persist the manifest to disk with mode 0600 (owner read/write only).
 * Creates the mosaicHome directory if it does not exist.
 */
export function writeManifest(mosaicHome: string, manifest: InstallManifest): void {
  const p = manifestPath(mosaicHome);
  const json = JSON.stringify(manifest, null, 2) + '\n';
  writeFileSync(p, json, { encoding: 'utf8' });
  try {
    chmodSync(p, 0o600);
  } catch {
    // chmod may fail on some systems (e.g. Windows); non-fatal
  }
}

/**
 * Narrow an unknown value to InstallManifest.
 * Only checks the minimum structure; does not validate every field.
 */
function isValidManifest(v: unknown): v is InstallManifest {
  if (typeof v !== 'object' || v === null) return false;
  const m = v as Record<string, unknown>;
  if (m['version'] !== 1) return false;
  if (typeof m['installedAt'] !== 'string') return false;
  if (typeof m['cliVersion'] !== 'string') return false;
  if (typeof m['frameworkVersion'] !== 'number') return false;
  if (typeof m['mutations'] !== 'object' || m['mutations'] === null) return false;
  return true;
}

/**
 * The known set of runtime asset destinations managed by
 * mosaic-link-runtime-assets / framework/install.sh.
 *
 * Used by heuristic mode when no manifest is available.
 */
export function heuristicRuntimeAssetDests(homeDir: string): string[] {
  return [
    join(homeDir, '.claude', 'CLAUDE.md'),
    join(homeDir, '.claude', 'settings.json'),
    join(homeDir, '.claude', 'hooks-config.json'),
    join(homeDir, '.claude', 'context7-integration.md'),
    join(homeDir, '.config', 'opencode', 'AGENTS.md'),
    join(homeDir, '.codex', 'instructions.md'),
  ];
}

/** The npmrc scope line added by tools/install.sh. */
export const DEFAULT_SCOPE_LINE =
  '@mosaicstack:registry=https://git.mosaicstack.dev/api/packages/mosaicstack/npm/';
