/**
 * Debian-specific rollup config for node-lit.
 *
 * Uses only plugins available as Debian packages.  Replaces the upstream
 * rollup-common.js during the package build.
 *
 * Differences from upstream:
 *  - rollup-plugin-summary omitted (build stats only, no effect on output)
 *  - rollup-plugin-copy omitted (only copies HTML test files; we skip tests)
 *  - @rollup/plugin-virtual is not packaged in Debian, so the name-cache
 *    seeder build (which upstream needs it for) uses the small inlined
 *    equivalent `virtual()` defined below instead. The seeder itself is
 *    retained verbatim: it is what makes the minified output reproducible.
 *    This build-time shim can be dropped once @rollup/plugin-virtual is
 *    packaged in Debian, after which node-lit can build with upstream's
 *    rollup-common.js directly.
 */

import {resolve as pathResolve, dirname as pathDirname} from 'path';
import {terser} from 'rollup-plugin-terser';
import replace from '@rollup/plugin-replace';
import sourcemaps from 'rollup-plugin-sourcemaps';
import nodeResolve from '@rollup/plugin-node-resolve';
import inject from '@rollup/plugin-inject';

// ---------------------------------------------------------------------------
// Minimal inline replacement for @rollup/plugin-virtual (not packaged in
// Debian). Behaviourally identical to the upstream plugin: it serves the
// in-memory seeder module and resolves relative imports from it back to real
// on-disk files. Used only by the name-cache seeder build below.
// ---------------------------------------------------------------------------

const VIRTUAL_PREFIX = '\0virtual:';

const virtual = (modules) => {
  const resolvedIds = new Map();
  for (const id of Object.keys(modules)) {
    resolvedIds.set(pathResolve(id), modules[id]);
  }
  return {
    name: 'virtual',
    resolveId(id, importer) {
      if (id in modules) {
        return VIRTUAL_PREFIX + id;
      }
      if (importer) {
        const importerNoPrefix = importer.startsWith(VIRTUAL_PREFIX)
          ? importer.slice(VIRTUAL_PREFIX.length)
          : importer;
        const resolved = pathResolve(pathDirname(importerNoPrefix), id);
        if (resolvedIds.has(resolved)) {
          return VIRTUAL_PREFIX + resolved;
        }
      }
      return null;
    },
    load(id) {
      if (id.startsWith(VIRTUAL_PREFIX)) {
        const idNoPrefix = id.slice(VIRTUAL_PREFIX.length);
        return idNoPrefix in modules
          ? modules[idNoPrefix]
          : resolvedIds.get(idNoPrefix);
      }
      return null;
    },
  };
};

// Discards a build's output; used so the name-cache seeder build populates the
// shared terser name cache without writing any files.
const skipBundleOutput = {
  generateBundle(options, bundles) {
    for (const name in bundles) {
      delete bundles[name];
    }
  },
};

// ---------------------------------------------------------------------------
// Terser name-cache setup (adapted from upstream rollup-common.js)
//
// Properties starting with _$ are accessed across package boundaries and must
// mangle to the same letter in every package.  We pre-seed the name cache
// with the upstream-defined stable mapping so terser never re-uses those
// letters for other properties.
//
// ONCE A MANGLED NAME HAS BEEN ASSIGNED HERE IT MUST NEVER CHANGE.
// ---------------------------------------------------------------------------

const STABLE_PROPERTY_PREFIX = '_$A';

const PACKAGE_CLASS_PREFIXES = {
  lit: '_$B',
  'lit-html': '_$C',
  'lit-element': '_$D',
  '@lit/reactive-element': '_$E',
  '@lit/context': '_$Q',
};

// Properties reserved as-is (never mangled).
const reservedProperties = [
  '_$litType$',
  '_$litDirective$',
  '_$litPart$',
  '_$litElement$',
  '_$litStatic$',
  '_$cssResult$',
  '_$litProps$',
];

// Stable cross-package properties with their fixed mangled names.
const stableProperties = {
  _$startNode: 'A',
  _$endNode: 'B',
  _$getTemplate: 'C',
  _$template: 'D',
  _$didUpdate: 'E',
  _$renderImpl: 'F',
  _$needsHydration: 'G',
  _$committedValue: 'H',
  _$setValue: 'I',
  _$handlesPrepareStyles: 'J',
  _$attributeToProperty: 'K',
  _$changedProperties: 'L',
  _$parent: 'M',
  _$disconnectableChildren: 'N',
  _$notifyDirectiveConnectionChanged: 'O',
  _$notifyConnectionChanged: 'P',
  _$reparentDisconnectables: 'Q',
  _$clear: 'R',
  _$resolve: 'S',
  _$initialize: 'T',
  _$isConnected: 'U',
  _$parts: 'V',
  _$litServerRenderMode: 'W',
};

const makeNameCache = () => ({
  props: {
    props: Object.fromEntries(
      Object.entries(stableProperties).map(([name, val]) => [
        '$' + name,
        STABLE_PROPERTY_PREFIX + val,
      ])
    ),
  },
});

const addedClassPrefix = new WeakSet();
const reactiveElementInclude = [
  '**/packages/reactive-element/development/reactive-element.js',
];
const productionReplacements = {
  'const DEV_MODE = true': 'const DEV_MODE = false',
  'const ENABLE_EXTRA_SECURITY_HOOKS = true':
    'const ENABLE_EXTRA_SECURITY_HOOKS = false',
  'const ENABLE_SHADYDOM_NOPATCH = true':
    'const ENABLE_SHADYDOM_NOPATCH = false',
};
const nodeInject = () =>
  inject({
    HTMLElement: ['@lit-labs/ssr-dom-shim', 'HTMLElement'],
    customElements: ['@lit-labs/ssr-dom-shim', 'customElements'],
    include: reactiveElementInclude,
  });
const nodeExtendsPatch = () =>
  replace({
    preventAssignment: true,
    values: {
      'extends HTMLElement': 'extends (globalThis.HTMLElement ?? HTMLElement)',
    },
    include: reactiveElementInclude,
  });

const generateTerserOptions = (nameCache, classPropertyPrefix = '') => ({
  ecma: 2021,
  compress: {unsafe: true, passes: 2},
  output: {comments: 'some', inline_script: false},
  get nameCache() {
    if (nameCache && !addedClassPrefix.has(this)) {
      const props = nameCache.props.props;
      for (const p in props) {
        if (p.startsWith('$__') && !props[p].startsWith(classPropertyPrefix)) {
          props[p] = classPropertyPrefix + props[p];
        }
      }
      addedClassPrefix.add(this);
    }
    return nameCache;
  },
  mangle: {
    properties: {
      regex: /^_/,
      reserved: reservedProperties,
      debug: false,
    },
  },
});

// ---------------------------------------------------------------------------
// litProdConfig — mirrors the upstream API used by each package's
// rollup.config.js
// ---------------------------------------------------------------------------

export function litProdConfig({
  entryPoints,
  external = [],
  bundled = [],
  packageName,
  outputDir = './',
  includeNodeBuild = false,
} = {}) {
  const classPropertyPrefix = PACKAGE_CLASS_PREFIXES[packageName] ?? '';
  const nameCache = makeNameCache();
  const terserOptions = generateTerserOptions(nameCache, classPropertyPrefix);

  // Name-cache seeder (upstream rollup-common.js). Terser does not consult the
  // shared name cache when *choosing* a mangled name, only when reusing one, so
  // a property mangled per-module can collide with a different property's name
  // in another module. Building every entry point into one throwaway bundle and
  // minifying it first forces terser to assign every property a unique name up
  // front, locking the cache before the real per-module builds run. Without
  // this pass the mangled names depend on per-module processing order and the
  // output is not reproducible.
  const seederInput = 'name-cache-seeder-virtual-input.js';
  const seederContents = [
    // Import every entry point so terser sees all property accesses; give each
    // a unique named import to avoid duplicate-identifier errors.
    ...entryPoints.map(
      (name, idx) => `import * as import${idx} from './development/${name}.js';`
    ),
    // Reference the imports so tree shaking does not drop them.
    ...entryPoints.map((_name, idx) => `console.log(import${idx});`),
    // Reserve every cross-package mangled name even if unused in this package.
    ...Object.keys(stableProperties).map(
      (name) => `console.log(window.${name});`
    ),
  ].join('\n');
  const seederTerserOptions = generateTerserOptions(nameCache);

  const prodReplace = {
    preventAssignment: true,
    values: productionReplacements,
  };
  const nodeBuild = ({dir, minify}) => ({
    input: entryPoints.map((name) => `development/${name}.js`),
    output: {
      dir: `${outputDir}/node${dir}`,
      format: 'esm',
      preserveModules: true,
      sourcemap: true,
    },
    external: [...external, '@lit-labs/ssr-dom-shim'],
    plugins: [
      replace({
        preventAssignment: true,
        values: {
          'const NODE_MODE = false': 'const NODE_MODE = true',
          ...(minify ? productionReplacements : {}),
          'const ENABLE_SHADYDOM_NOPATCH = true':
            'const ENABLE_SHADYDOM_NOPATCH = false',
        },
      }),
      nodeInject(),
      nodeExtendsPatch(),
      sourcemaps(),
      ...(minify ? [terser(terserOptions)] : []),
    ],
  });

  return [
    // Name-cache seeder build. Runs first; its output is discarded, but it
    // populates the shared terser name cache so all subsequent builds mangle
    // properties deterministically. Must precede every other config entry.
    {
      input: seederInput,
      output: {
        file: 'name-cache-seeder-throwaway-output.js',
        format: 'esm',
      },
      external,
      // The seeder module exports nothing, so tree shaking would strip
      // everything and terser would see no properties.
      treeshake: false,
      plugins: [
        virtual({[seederInput]: seederContents}),
        terser(seederTerserOptions),
        skipBundleOutput,
      ],
    },

    // Production ESM build (minified, preserves module structure)
    {
      input: entryPoints.map((name) => `development/${name}.js`),
      output: {
        dir: outputDir,
        format: 'esm',
        preserveModules: true,
        sourcemap: true,
      },
      external,
      plugins: [replace(prodReplace), sourcemaps(), terser(terserOptions)],
    },

    // Node.js build (minified, imports the packaged ssr-dom-shim at runtime)
    ...(includeNodeBuild
      ? [
          nodeBuild({dir: '', minify: true}),
          nodeBuild({dir: '/development', minify: false}),
        ]
      : []),

    // Standalone bundles (polyfill-support, lit-core.min, lit-all.min, etc.)
    ...bundled.map(({file, output, name, format = 'umd'}) => ({
      input: `development/${file}.js`,
      output: {
        file: `${output || file}.js`,
        format,
        name,
        sourcemap: true,
      },
      plugins: [
        nodeResolve({exportConditions: ['development']}),
        replace({
          preventAssignment: false,
          values: {
            ...productionReplacements,
            'var DEV_MODE = true': 'var DEV_MODE = false',
            'var ENABLE_EXTRA_SECURITY_HOOKS = true':
              'var ENABLE_EXTRA_SECURITY_HOOKS = false',
            'var ENABLE_SHADYDOM_NOPATCH = true':
              'var ENABLE_SHADYDOM_NOPATCH = false',
          },
        }),
        sourcemaps(),
        terser(terserOptions),
      ],
    })),
  ];
}
