#!/usr/bin/env node

"use strict";

const fs = require("fs");
const crypto = require("crypto");
const path = require("path");
const Module = require("module");
const packageJson = require("../package.json");

const DEFAULT_CONFIG_FILE = "jso.config.json";
const DEFAULT_CONFIG_FILES = ["jso.config.json", "jso.config.cjs", "jso.config.mjs", "jso.config.js"];
const DEFAULT_ENDPOINT = "https://www.javascriptobfuscator.com/HttpApi.ashx";
const DEFAULT_EXTENSIONS = [".js", ".jsx"];
const DEFAULT_MARKUP_EXTENSIONS = [".html", ".htm", ".php", ".phtml", ".asp", ".aspx", ".jsp", ".cshtml", ".vbhtml"];
const DEFAULT_EXCLUDE = ["**/*.map", "**/node_modules/**", "**/*-obfuscated.js"];
const DEFAULT_ASSET_EXCLUDE = ["**/*.map"];
const WEB_PRESET_FORMAT = "javascript-obfuscator-web-preset";
const CONDITIONAL_MARKER_PATTERN = /\/\/\s*javascript-obfuscator:(disable|enable)\b|\/\*\s*javascript-obfuscator:(disable|enable)\s*\*\//g;
const HTML_SCRIPT_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;

const PRESET_OPTIONS = {
  standard: {
    EncodeStrings: true,
    MoveStrings: true,
    ReplaceNames: true,
    SelfCompression: true,
    CompressionRatio: "Best"
  },
  balanced: {
    IdentityStyle: "v2abcd",
    EncodeStrings: true,
    MoveStrings: true,
    ReplaceNames: true,
    SelfCompression: true,
    SelfCompressionMinSize: 0,
    CompressionRatio: "Best",
    DeepObfuscate: true,
    ReorderCode: true,
    EncryptStrings: true,
    FlatTransform: true
  },
  maximum: {
    IdentityStyle: "v2abcd",
    EncodeStrings: true,
    MoveStrings: true,
    ReplaceNames: true,
    RenameGlobals: true,
    RenameMembers: true,
    MoveMembers: true,
    SelfCompression: true,
    SelfCompressionMinSize: 0,
    CompressionRatio: "Best",
    DeepObfuscate: true,
    ReorderCode: true,
    AddDeadCode: true,
    DeadcodeLevel: "Low",
    EncryptStrings: true,
    FlatTransform: true
  }
};

const WEB_FEATURE_OPTIONS = {
  "Short Local Name": { IdentityStyle: "v2abcd" },
  "Compressor": { SelfCompression: true, SelfCompressionMinSize: 0, CompressionRatio: "Best" },
  "Deep Obfuscation": { DeepObfuscate: true },
  "Code Transposition": { ReorderCode: true },
  "Dead Code Insertion": { AddDeadCode: true, DeadcodeLevel: "Low" },
  "Encrypt Strings": { EncryptStrings: true },
  "Move Members": { MoveMembers: true },
  "Replace Globals": { RenameGlobals: true },
  "Protect Members": { RenameMembers: true },
  "Keep Header Comment": { KeepComment: true },
  "Move Strings": { MoveStrings: true },
  "Flat Transform": { FlatTransform: true },
  "Encode Strings": { EncodeStrings: true }
};

const JAVASCRIPT_OBFUSCATOR_MIGRATION_MAP = [
  { source: "optionsPreset", target: ["preset"], confidence: "approximate", note: "default/low and VM low/default presets map to standard, medium and VM medium map to balanced, and high plus stronger VM presets map to maximum." },
  { source: "stringArray", target: ["MoveStrings"], confidence: "direct", note: "Moves string literals into generated lookup structures." },
  { source: "stringArrayEncoding", target: ["EncodeStrings", "EncryptStrings"], confidence: "approximate", note: "rc4-style values map to encrypted strings; other values map to encoded strings." },
  { source: "stringArrayThreshold", target: ["MoveStrings"], confidence: "approximate", note: "Threshold values are not one-to-one; review output size and behavior." },
  { source: "unicodeEscapeSequence", target: ["EncodeStrings"], confidence: "approximate", note: "Maps to encoded strings." },
  { source: "controlFlowFlattening", target: ["FlatTransform", "DeepObfuscate"], confidence: "approximate", note: "Maps to deeper control-flow and flat-transform protection." },
  { source: "deadCodeInjection", target: ["AddDeadCode"], confidence: "direct", note: "Enables dead-code insertion." },
  { source: "deadCodeInjectionThreshold", target: ["DeadcodeLevel"], confidence: "approximate", note: "Threshold is converted to the nearest Low, Medium, or High level." },
  { source: "identifierNamesGenerator", target: ["IdentityStyle"], confidence: "approximate", note: "Hexadecimal maps to v1hex; other styles map to v2abcd." },
  { source: "renameGlobals", target: ["RenameGlobals"], confidence: "direct", note: "Enables global renaming." },
  { source: "renameProperties", target: ["RenameMembers"], confidence: "approximate", note: "Review public member access before enabling." },
  { source: "reservedNames", target: ["reservedNames", "VariableExclusion"], confidence: "direct", note: "Reserved name expressions are preserved." },
  { source: "domainLock", target: ["LockDomain", "LockDomainList"], confidence: "approximate", note: "Domain lock lists map to LockDomain and LockDomainList; review subdomain and redirect behavior." },
  { source: "compact", target: ["SelfCompression", "CompressionRatio", "WriteFormats"], confidence: "approximate", note: "Compact true maps to compression; compact false maps to formatted output." },
  { source: "target", target: ["OptimizationMode"], confidence: "approximate", note: "browser maps to Web; node maps to NodeJS." },
  { source: "parseHtml", target: ["parseHtml"], confidence: "direct", note: "Protects marked <script data-javascript-obfuscator> blocks in HTML or template files." },
  { source: "ignoreImports", target: ["ignoreImports"], confidence: "approximate", note: "Preserves import/export-from statements and common static require/import() statements before sending the remaining code to the hosted API." }
];

const JS_CONFUSER_MIGRATION_MAP = [
  { source: "preset", target: ["preset"], confidence: "approximate", note: "JS-Confuser low/medium/high presets map to standard/balanced/maximum." },
  { source: "target", target: ["OptimizationMode"], confidence: "direct", note: "browser maps to Web; node maps to NodeJS." },
  { source: "renameVariables", target: ["ReplaceNames"], confidence: "approximate", note: "Variable renaming maps to local-name replacement." },
  { source: "renameGlobals", target: ["RenameGlobals"], confidence: "direct", note: "Enables global renaming." },
  { source: "globalConcealing", target: ["RenameGlobals"], confidence: "approximate", note: "Global concealing maps to global renaming; review runtime behavior." },
  { source: "stringEncoding", target: ["EncodeStrings"], confidence: "approximate", note: "String encoding maps to encoded strings." },
  { source: "stringConcealing", target: ["EncryptStrings"], confidence: "approximate", note: "String concealing maps to encrypted strings." },
  { source: "duplicateLiteralsRemoval", target: ["MoveStrings"], confidence: "approximate", note: "Duplicate literal removal maps to moved string storage." },
  { source: "stringCompression", target: ["SelfCompression", "CompressionRatio"], confidence: "approximate", note: "String compression maps to self-compression." },
  { source: "compact", target: ["SelfCompression", "CompressionRatio", "WriteFormats"], confidence: "approximate", note: "Compact output maps to self-compression; false maps to formatted output." },
  { source: "minify", target: ["SelfCompression", "CompressionRatio", "WriteFormats"], confidence: "approximate", note: "Minify maps to self-compression; false maps to formatted output." },
  { source: "controlFlowFlattening", target: ["FlatTransform", "DeepObfuscate"], confidence: "approximate", note: "Maps to deeper control-flow and flat-transform protection." },
  { source: "deadCode", target: ["AddDeadCode"], confidence: "direct", note: "Enables dead-code insertion." },
  { source: "identifierGenerator", target: ["IdentityStyle"], confidence: "approximate", note: "Hexadecimal generators map to v1hex; other styles map to v2abcd." },
  { source: "lock.domainLock", target: ["LockDomain", "LockDomainList"], confidence: "approximate", note: "Domain lock maps to LockDomain and LockDomainList; review subdomain behavior." },
  { source: "lock.endDate", target: ["LockDate", "LockDateValue"], confidence: "approximate", note: "End-date lock maps to LockDate and yyyyMMdd LockDateValue." }
];

const JAVASCRIPT_OBFUSCATOR_REVIEW_OPTIONS = {
  debugProtection: "Review anti-debugging behavior against JavaScript Obfuscator runtime-defense options.",
  debugProtectionInterval: "Review anti-debugging interval behavior manually.",
  disableConsoleOutput: "No direct config conversion; decide whether console behavior should be handled in app code.",
  domainLockRedirectUrl: "Review and convert to LockDomainMsg or application-level handling.",
  forceTransformStrings: "Review string handling manually.",
  identifiersDictionary: "Custom identifier dictionaries are not exposed by the hosted API workflow; review naming requirements manually.",
  identifiersPrefix: "Global identifier prefixes are not exposed by the hosted API workflow; review multi-file naming requirements manually.",
  identifierNamesCache: "Identifier-name cache files are not emitted by the hosted API workflow; review deterministic rename requirements manually.",
  identifierNamesCachePath: "Identifier-name cache files are not emitted by the hosted API workflow; remove cache-file assumptions from release scripts.",
  inputFileName: "Review source-map input file naming manually; the hosted API workflow protects named request items.",
  log: "Review logging needs in CI; use --json, --dry-run, --doctor, and manifests for structured release output.",
  numbersToExpressions: "Review numeric expression transforms manually.",
  renamePropertiesMode: "Review property rename mode manually before enabling RenameMembers.",
  reservedStrings: "Review reserved string requirements manually; string handling is controlled by MoveStrings, EncodeStrings, and EncryptStrings.",
  seed: "Seeded deterministic output is not exposed by the hosted API workflow; review repeatability requirements manually.",
  selfDefending: "Review tamper-defense requirements manually.",
  simplify: "Simplify is accepted for migration compatibility; review whether hosted API compression and formatting options meet the same goal.",
  sourceMap: "Do not publish source maps with protected release output unless your release policy allows it.",
  sourceMapBaseUrl: "Source maps are not emitted by the hosted API workflow; review release source-map policy.",
  sourceMapFileName: "Source maps are not emitted by the hosted API workflow; review release source-map policy.",
  sourceMapMode: "Review source map handling manually.",
  sourceMapSourcesMode: "Source maps are not emitted by the hosted API workflow; review release source-map policy.",
  splitStrings: "Review string splitting needs; MoveStrings and EncryptStrings may already cover the goal.",
  splitStringsChunkLength: "Review string splitting needs manually.",
  stringArrayCallsTransform: "Review string-call transform needs manually.",
  stringArrayCallsTransformThreshold: "Review string-call transform thresholds manually.",
  stringArrayIndexesType: "Review string-array index style manually.",
  stringArrayRotate: "Review string-array rotation needs manually.",
  stringArrayShuffle: "Review string-array shuffle needs manually.",
  stringArrayWrappersChainedCalls: "Review string-array wrapper chained-call behavior manually.",
  stringArrayWrappersCount: "Review string wrapper behavior manually.",
  stringArrayWrappersParametersMaxCount: "Review string-array wrapper parameter behavior manually.",
  stringArrayWrappersType: "Review string-array wrapper style manually.",
  transformObjectKeys: "Review object-key transform needs manually."
};

const JS_CONFUSER_REVIEW_OPTIONS = {
  "lock.antiDebug": "Review anti-debugging requirements manually.",
  "lock.countermeasures": "Review runtime countermeasure behavior manually.",
  "lock.customLocks": "Custom runtime locks need manual review.",
  "lock.integrity": "Integrity and tamper detection need manual review.",
  "lock.selfDefending": "Self-defending runtime behavior needs manual review.",
  "lock.startDate": "Start-date locks have no direct hosted API equivalent; review gating behavior manually.",
  "lock.tamperProtection": "Tamper-protection behavior needs manual review.",
  astScrambler: "AST scrambling has no direct hosted API mapping; review output manually.",
  calculator: "Calculator transforms have no direct hosted API mapping; review output manually.",
  dispatcher: "Dispatcher control-flow transforms have no direct hosted API mapping; review output manually.",
  flatten: "Function flattening has no direct hosted API mapping; review output manually.",
  hexadecimalNumbers: "Hexadecimal number output has no direct hosted API mapping; review output manually.",
  movedDeclarations: "Moved declaration behavior has no direct hosted API mapping; review output manually.",
  objectExtraction: "Object extraction has no direct hosted API mapping; review output manually.",
  opaquePredicates: "Opaque predicate transforms have no direct hosted API mapping; review output manually.",
  pack: "Packing/runtime loader behavior has no direct hosted API mapping; review distribution changes manually.",
  preserveFunctionLength: "Function-length preservation needs manual review.",
  renameLabels: "Label renaming has no direct hosted API mapping; review output manually.",
  rgf: "RGF transforms have no direct hosted API mapping; review output manually.",
  shuffle: "Shuffle transforms have no direct hosted API mapping; review output manually.",
  variableMasking: "Variable masking has no direct hosted API mapping; review output manually."
};

const JS_CONFUSER_DETECT_KEYS = [
  "astScrambler",
  "calculator",
  "deadCode",
  "dispatcher",
  "duplicateLiteralsRemoval",
  "flatten",
  "globalConcealing",
  "hexadecimalNumbers",
  "identifierGenerator",
  "lock",
  "minify",
  "movedDeclarations",
  "objectExtraction",
  "opaquePredicates",
  "pack",
  "preserveFunctionLength",
  "renameLabels",
  "renameVariables",
  "rgf",
  "shuffle",
  "stringCompression",
  "stringConcealing",
  "stringEncoding",
  "variableMasking"
];

const JAVASCRIPT_OBFUSCATOR_REVIEW_OPTION_TYPES = {
  debugProtection: "boolean",
  debugProtectionInterval: "nonNegativeNumber",
  disableConsoleOutput: "boolean",
  domainLockRedirectUrl: "string",
  forceTransformStrings: "stringArray",
  identifiersDictionary: "stringArray",
  identifiersPrefix: "string",
  identifierNamesCache: "objectOrNull",
  identifierNamesCachePath: "string",
  inputFileName: "string",
  log: "boolean",
  numbersToExpressions: "boolean",
  renamePropertiesMode: "string",
  reservedStrings: "stringArray",
  seed: "stringOrNumber",
  selfDefending: "boolean",
  simplify: "boolean",
  sourceMap: "boolean",
  sourceMapBaseUrl: "string",
  sourceMapFileName: "string",
  sourceMapMode: "string",
  sourceMapSourcesMode: "string",
  splitStrings: "boolean",
  splitStringsChunkLength: "nonNegativeNumber",
  stringArrayCallsTransform: "boolean",
  stringArrayCallsTransformThreshold: "nonNegativeNumber",
  stringArrayIndexesType: "stringOrStringArray",
  stringArrayRotate: "boolean",
  stringArrayShuffle: "boolean",
  stringArrayWrappersChainedCalls: "boolean",
  stringArrayWrappersCount: "nonNegativeNumber",
  stringArrayWrappersParametersMaxCount: "nonNegativeNumber",
  stringArrayWrappersType: "string",
  ignoreImports: "boolean",
  transformObjectKeys: "boolean"
};

const OPTION_REFERENCE = [
  { name: "EncodeStrings", type: "boolean", category: "strings", description: "Encode string literals." },
  { name: "MoveStrings", type: "boolean", category: "strings", description: "Move string literals into a generated lookup structure." },
  { name: "EncryptStrings", type: "boolean", category: "strings", description: "Encrypt strings for stronger string hiding." },
  { name: "ReplaceNames", type: "boolean", category: "names", description: "Rename local JavaScript identifiers." },
  { name: "RenameGlobals", type: "boolean", category: "names", description: "Rename global identifiers more aggressively." },
  { name: "RenameMembers", type: "boolean", category: "names", description: "Rename JavaScript members more aggressively." },
  { name: "MoveMembers", type: "boolean", category: "members", description: "Move object members into generated lookup structures." },
  { name: "VariableExclusion", type: "string", category: "names", description: "Multiline regular-expression list for names that must be preserved." },
  { name: "IdentityStyle", type: "enum", values: ["v1hex", "v2abcd"], category: "names", description: "Generated identifier style." },
  { name: "SelfCompression", type: "boolean", category: "compression", description: "Compress protected JavaScript output." },
  { name: "SelfCompressionMinSize", type: "number", category: "compression", description: "Minimum size threshold for compression." },
  { name: "CompressionRatio", type: "enum", values: ["Auto", "Low", "Medium", "High", "Best"], category: "compression", description: "Compression strength." },
  { name: "DeepObfuscate", type: "boolean", category: "control-flow", description: "Apply deeper obfuscation transforms." },
  { name: "ReorderCode", type: "boolean", category: "control-flow", description: "Reorder JavaScript source code." },
  { name: "FlatTransform", type: "boolean", category: "control-flow", description: "Apply flat transform protection." },
  { name: "AddDeadCode", type: "boolean", category: "control-flow", description: "Insert dead code." },
  { name: "DeadcodeLevel", type: "enum", values: ["Low", "Medium", "High"], category: "control-flow", description: "Dead-code insertion level." },
  { name: "OptimizationMode", type: "enum", values: ["Auto", "Web", "Html5", "Game", "Mobile", "NodeJS"], category: "runtime", description: "Runtime optimization target." },
  { name: "WriteFormats", type: "boolean", category: "formatting", description: "Write formatted output for debugging." },
  { name: "WriteFormats_KeepIndent", type: "boolean", category: "formatting", description: "Keep indentation when formatted output is enabled." },
  { name: "WriteFormats_LineNumbers", type: "boolean", category: "formatting", description: "Add line numbers when formatted output is enabled." },
  { name: "LockDomain", type: "boolean", category: "locks", description: "Enable domain locking." },
  { name: "LockDomainSubs", type: "boolean", category: "locks", description: "Allow subdomains in domain locking." },
  { name: "LockDomainList", type: "string", category: "locks", description: "Allowed domain list for domain locking." },
  { name: "LockDomainMsg", type: "string", category: "locks", description: "Message shown when domain locking fails." },
  { name: "LockDate", type: "boolean", category: "locks", description: "Enable date locking." },
  { name: "LockDateValue", type: "string", category: "locks", description: "Lock date in yyyyMMdd format." },
  { name: "LockDateMsg", type: "string", category: "locks", description: "Message shown when date locking fails." }
];

const CONVENIENCE_OPTION_ALIASES = [
  { configKey: "keepHeaderComment", optionName: "KeepComment", type: "boolean" },
  { configKey: "protectObjectDeclaration", optionName: "ReorderCodeObjectDeclare", type: "boolean" },
  { configKey: "moveNestedFunction", optionName: "MoveNested", type: "boolean" },
  { configKey: "formattedOutput", optionName: "WriteFormats", type: "boolean" },
  { configKey: "keepIndent", optionName: "WriteFormats_KeepIndent", type: "boolean" },
  { configKey: "lineNumbers", optionName: "WriteFormats_LineNumbers", type: "boolean" },
  { configKey: "lockDomainSubdomains", optionName: "LockDomainSubs", type: "boolean" },
  { configKey: "lockDomainMessage", optionName: "LockDomainMsg", type: "string" },
  { configKey: "lockDate", optionName: "LockDate", type: "boolean" },
  { configKey: "lockDateValue", optionName: "LockDateValue", type: "string" },
  { configKey: "lockDateMessage", optionName: "LockDateMsg", type: "string" }
];

const INIT_TEMPLATE_CONFIGS = {
  "browser-app": {
    projectName: "browser-release",
    input: "dist",
    output: "dist-protected",
    preset: "balanced",
    extensions: [".js", ".jsx"],
    exclude: ["**/*.map", "**/vendor/**", "**/*-obfuscated.js"],
    copyAssets: true,
    assetExclude: ["**/*.map"],
    mixedServer: false,
    parseHtml: false,
    honorConditionalComments: false,
    ignoreImports: false,
    keepHeaderComment: true,
    protectObjectDeclaration: false,
    moveNestedFunction: false,
    formattedOutput: false,
    keepIndent: false,
    lineNumbers: false,
    reservedNames: ["^PublicApi$", "^keep_"],
    options: {
      OptimizationMode: "Web",
      LockDomain: false,
      LockDate: false
    }
  },
  "html-app": {
    projectName: "html-release",
    input: "dist",
    output: "dist-protected",
    preset: "balanced",
    extensions: [".js", ".jsx"],
    exclude: ["**/*.map", "**/vendor/**", "**/*-obfuscated.js"],
    copyAssets: true,
    assetExclude: ["**/*.map"],
    mixedServer: false,
    parseHtml: true,
    honorConditionalComments: false,
    ignoreImports: false,
    keepHeaderComment: true,
    protectObjectDeclaration: false,
    moveNestedFunction: false,
    formattedOutput: false,
    keepIndent: false,
    lineNumbers: false,
    reservedNames: ["^PublicApi$", "^keep_"],
    options: {
      OptimizationMode: "Web",
      LockDomain: false,
      LockDate: false
    }
  },
  "node-app": {
    projectName: "node-release",
    input: "dist",
    output: "dist-protected",
    preset: "balanced",
    extensions: [".js", ".cjs", ".mjs"],
    exclude: ["**/*.map", "**/node_modules/**", "**/*-obfuscated.js"],
    copyAssets: false,
    assetExclude: ["**/*.map"],
    mixedServer: false,
    parseHtml: false,
    honorConditionalComments: false,
    ignoreImports: true,
    keepHeaderComment: true,
    protectObjectDeclaration: false,
    moveNestedFunction: false,
    formattedOutput: false,
    keepIndent: false,
    lineNumbers: false,
    reservedNames: ["^PublicApi$", "^keep_"],
    options: {
      OptimizationMode: "NodeJS",
      LockDomain: false,
      LockDate: false
    }
  },
  "electron-app": {
    projectName: "electron-release",
    input: "dist",
    output: "dist-protected",
    preset: "balanced",
    extensions: [".js", ".cjs", ".mjs"],
    exclude: ["**/*.map", "**/node_modules/**", "**/*-obfuscated.js"],
    copyAssets: true,
    assetExclude: ["**/*.map"],
    mixedServer: true,
    parseHtml: false,
    honorConditionalComments: false,
    ignoreImports: true,
    keepHeaderComment: true,
    protectObjectDeclaration: false,
    moveNestedFunction: false,
    formattedOutput: false,
    keepIndent: false,
    lineNumbers: false,
    reservedNames: ["^PublicApi$", "^keep_"],
    options: {
      OptimizationMode: "NodeJS",
      LockDomain: false,
      LockDate: false
    }
  }
};

function printHelp() {
  process.stdout.write(`jso-protector

Usage:
  jso-protector --config jso.config.json
  jso-protector --input dist --output dist-protected
  jso-protector --stdin --stdout --file-name app.js
  jso-protector --preset balanced --input dist --output dist-protected
  jso-protector --migrate-javascript-obfuscator javascript-obfuscator.json --output jso.config.json
  jso-protector --migrate-javascript-obfuscator javascript-obfuscator.config.cjs --output jso.config.json
  jso-protector --migrate-js-confuser js-confuser.config.cjs --output jso.config.json
  jso-protector --init

Options:
  --config <file>      Config file path. Supports JSON, CommonJS .cjs/.js, and ESM .mjs/.js files.
                       Defaults to jso.config.json, jso.config.cjs, jso.config.mjs, or jso.config.js when present.
  --input <path>       Input file or directory.
  --output <path>      Output file or directory.
  --stdin              Read one JavaScript file from standard input.
  --stdout             Write protected stdin output to standard output.
  --file-name <name>   Virtual file name for stdin mode. Defaults to stdin.js.
  --preset <name>      Protection preset: standard, balanced, or maximum.
  --web-preset <file>  Import an exported online-tool JSON preset.
  --migrate-javascript-obfuscator <file> Convert a javascript-obfuscator JSON or CommonJS config.
  --migrate-js-confuser <file> Convert a JS-Confuser JSON or CommonJS config.
  --list-migration-map Print javascript-obfuscator option migration guidance.
  --list-js-confuser-migration-map Print JS-Confuser option migration guidance.
  --explain-compat <option> Explain how one javascript-obfuscator option is handled.
  --explain-js-confuser-compat <option> Explain how one JS-Confuser option is handled.
  --local-only         Print local/offline workflow guidance and exit.
  --verify-manifest <file> Verify protected output against a written manifest.
  --verify-root <path> Override manifest output paths with a different artifact root.
  --option <k=v>       Override an API option. Repeatable. Example: LockDomain=true.
  --reserved-name <re> Preserve a public name pattern. Repeatable.
  --include <glob>     Protect only matching input files. Repeatable.
  --exclude <glob>     Exclude matching input files. Repeatable.
  --asset-exclude <glob> Exclude matching copied assets. Repeatable.
  --parse-html [bool]  Protect marked <script data-javascript-obfuscator> blocks in HTML or template files.
  --honor-conditional-comments Preserve code between javascript-obfuscator:disable/enable markers.
  --ignore-imports [bool] Preserve import/export-from statements and common static require/import() statements.
  --keep-header-comment Preserve the first comment block at the top of the output.
  --protect-object-declaration Hide object literal declaration structure.
  --move-nested-function Move nested functions away from call sites.
  --formatted-output   Emit multi-line formatted output for review builds.
  --keep-indent        Preserve indentation when formatted output is enabled.
  --line-numbers       Add line number hints when formatted output is enabled.
  --options-preset <name> Map javascript-obfuscator preset names.
  --string-array [bool] Map javascript-obfuscator string array extraction.
  --string-array-encoding <value> Map base64/rc4 string encoding.
  --control-flow-flattening [bool] Map control-flow flattening.
  --dead-code-injection [bool] Map dead code injection.
  --dead-code-injection-threshold <n> Map dead-code level.
  --identifier-names-generator <name> Map identifier style.
  --rename-globals [bool] Map global renaming.
  --rename-properties [bool] Map property/member renaming.
  --target <browser|node> Map runtime target.
  --compact [bool] Map compact output.
  --domain-lock <list> Map domain lock list.
  --source-map [bool] Accept source map flag for migration scripts.
  --source-map-sources-mode <value> Accept source-map sources mode for migration review.
  --identifier-names-cache <file> Accept identifier cache flag for migration review.
  --identifier-names-cache-path <file> Accept identifier cache path for migration review.
  --identifiers-dictionary <list> Accept identifier dictionary naming for migration review.
  --identifiers-prefix <value> Accept identifier prefix naming for migration review.
  --self-defending [bool] Accept tamper-defense flag for migration review.
  --debug-protection [bool] Accept anti-debugging flag for migration review.
  --rename-properties-mode <value> Accept property rename mode for migration review.
  --manifest <file>    Write a JSON protection manifest after a successful run.
  --max-output-bytes <n> Fail if any protected file is larger than n bytes.
  --max-growth-ratio <n> Fail if any protected file grows beyond source*n.
  --endpoint <url>     HTTP API endpoint. Defaults to ${DEFAULT_ENDPOINT}
  --api-key <value>    Base64 API key copied from the dashboard. Can use JSO_API_KEY.
  --api-password <v>   Base64 API password copied from the dashboard. Can use JSO_API_PASSWORD.
  --doctor             Check config, credentials, paths, file matching, and output readiness.
  --check-api          With --doctor, send a tiny live API request.
  --release-check      Run validate-config, dry-run planning, and doctor as one CI preflight.
  --strict             Treat validation warnings as failures in validate-config and release-check.
  --validate-config    Validate config shape and local release settings without calling the API.
  --print-config       Print resolved config with credentials redacted.
  --list-presets       Print available presets and their API options.
  --list-options       Print commonly used API option names.
  --no-copy-assets     Only write protected JavaScript output.
  --dry-run            Show files and request summary without calling the API.
  --json               Print machine-readable output for dry runs and summaries.
  --init               Write jso.config.json in the current directory.
  --init-template <n>  Init template: browser-app, html-app, node-app, or electron-app.
  --version, -v        Print package version. Combine with --json for metadata.
  --help               Show this help.
`);
}

function parseArgs(argv) {
  const args = {
    config: null,
    input: null,
    output: null,
    stdin: false,
    stdout: false,
    fileName: "stdin.js",
    preset: null,
    webPreset: null,
    migrateJavascriptObfuscator: null,
    migrateJsConfuser: null,
    listMigrationMap: false,
    listJsConfuserMigrationMap: false,
    explainCompat: null,
    explainJsConfuserCompat: null,
    localOnly: false,
    verifyManifest: null,
    verifyRoot: null,
    options: [],
    reservedNames: [],
    include: [],
    exclude: [],
    assetExclude: [],
    parseHtml: null,
    honorConditionalComments: false,
    keepHeaderComment: null,
    protectObjectDeclaration: null,
    moveNestedFunction: null,
    formattedOutput: null,
    keepIndent: null,
    lineNumbers: null,
    ignoreImports: null,
    manifest: null,
    maxOutputBytes: null,
    maxGrowthRatio: null,
    endpoint: null,
    apiKey: null,
    apiPassword: null,
    doctor: false,
    checkApi: false,
    releaseCheck: false,
    strict: false,
    validateConfig: false,
    printConfig: false,
    listPresets: false,
    listOptions: false,
    noCopyAssets: false,
    dryRun: false,
    json: false,
    init: false,
    initTemplate: "browser-app",
    version: false,
    help: false,
    compatibilityWarnings: []
  };

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    switch (arg) {
      case "--config":
        args.config = requireValue(argv, ++i, arg);
        break;
      case "--input":
      case "-i":
        args.input = requireValue(argv, ++i, arg);
        break;
      case "--output":
      case "-o":
        args.output = requireValue(argv, ++i, arg);
        break;
      case "--stdin":
        args.stdin = true;
        break;
      case "--stdout":
        args.stdout = true;
        break;
      case "--file-name":
        args.fileName = requireValue(argv, ++i, arg);
        break;
      case "--preset":
        args.preset = requireValue(argv, ++i, arg);
        break;
      case "--web-preset":
        args.webPreset = requireValue(argv, ++i, arg);
        break;
      case "--migrate-javascript-obfuscator":
        args.migrateJavascriptObfuscator = requireValue(argv, ++i, arg);
        break;
      case "--migrate-js-confuser":
        args.migrateJsConfuser = requireValue(argv, ++i, arg);
        break;
      case "--list-migration-map":
        args.listMigrationMap = true;
        break;
      case "--list-js-confuser-migration-map":
        args.listJsConfuserMigrationMap = true;
        break;
      case "--explain-compat":
        args.explainCompat = requireValue(argv, ++i, arg);
        break;
      case "--explain-js-confuser-compat":
        args.explainJsConfuserCompat = requireValue(argv, ++i, arg);
        break;
      case "--local-only":
        args.localOnly = true;
        break;
      case "--verify-manifest":
        args.verifyManifest = requireValue(argv, ++i, arg);
        break;
      case "--verify-root":
        args.verifyRoot = requireValue(argv, ++i, arg);
        break;
      case "--option":
        args.options.push(requireValue(argv, ++i, arg));
        break;
      case "--reserved-name":
      case "--reserved-names":
        args.reservedNames.push(requireValue(argv, ++i, arg));
        break;
      case "--include":
        args.include.push(requireValue(argv, ++i, arg));
        break;
      case "--exclude":
        args.exclude.push(requireValue(argv, ++i, arg));
        break;
      case "--asset-exclude":
        args.assetExclude.push(requireValue(argv, ++i, arg));
        break;
      case "--parse-html":
        args.parseHtml = readOptionalBoolean(argv, i + 1);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--honor-conditional-comments":
        args.honorConditionalComments = true;
        break;
      case "--keep-header-comment":
        args.keepHeaderComment = readOptionalBoolean(argv, i + 1);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--protect-object-declaration":
        args.protectObjectDeclaration = readOptionalBoolean(argv, i + 1);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--move-nested-function":
        args.moveNestedFunction = readOptionalBoolean(argv, i + 1);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--formatted-output":
        args.formattedOutput = readOptionalBoolean(argv, i + 1);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--keep-indent":
        args.keepIndent = readOptionalBoolean(argv, i + 1);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--line-numbers":
        args.lineNumbers = readOptionalBoolean(argv, i + 1);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--ignore-imports":
        args.ignoreImports = readOptionalBoolean(argv, i + 1);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--options-preset":
        args.preset = presetFromJavascriptObfuscatorPreset(requireValue(argv, ++i, arg));
        break;
      case "--string-array":
        args.options.push(`MoveStrings=${readOptionalBoolean(argv, i + 1)}`);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--unicode-escape-sequence":
        args.options.push(`EncodeStrings=${readOptionalBoolean(argv, i + 1)}`);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--control-flow-flattening": {
        const value = readOptionalBoolean(argv, i + 1);
        args.options.push(`DeepObfuscate=${value}`);
        args.options.push(`FlatTransform=${value}`);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      }
      case "--dead-code-injection":
        args.options.push(`AddDeadCode=${readOptionalBoolean(argv, i + 1)}`);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--rename-globals":
        args.options.push(`RenameGlobals=${readOptionalBoolean(argv, i + 1)}`);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--rename-properties":
        args.options.push(`RenameMembers=${readOptionalBoolean(argv, i + 1)}`);
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      case "--string-array-encoding":
        addStringArrayEncodingOption(args, requireValue(argv, ++i, arg));
        break;
      case "--string-array-threshold":
        args.options.push(`MoveStrings=${Number(requireValue(argv, ++i, arg)) > 0}`);
        break;
      case "--dead-code-injection-threshold":
        args.options.push(`DeadcodeLevel=${deadCodeLevelFromThreshold(requireValue(argv, ++i, arg))}`);
        break;
      case "--identifier-names-generator":
        args.options.push(`IdentityStyle=${identityStyleFromGenerator(requireValue(argv, ++i, arg))}`);
        break;
      case "--target":
        args.options.push(`OptimizationMode=${optimizationModeFromTarget(requireValue(argv, ++i, arg))}`);
        break;
      case "--domain-lock":
        args.options.push("LockDomain=true");
        args.options.push(`LockDomainList=${requireValue(argv, ++i, arg)}`);
        args.compatibilityWarnings.push("--domain-lock was mapped to LockDomain and LockDomainList; review subdomain and redirect behavior.");
        break;
      case "--domain-lock-redirect-url":
        args.compatibilityWarnings.push("--domain-lock-redirect-url has no direct hosted API equivalent; review LockDomainMsg or application-level redirect handling.");
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--self-defending":
        addReviewOnlyBooleanFlag(args, argv, i + 1, arg, "Self-defending has no one-to-one hosted API flag; review tamper-defense requirements manually.");
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--debug-protection":
        addReviewOnlyBooleanFlag(args, argv, i + 1, arg, "Debug protection has no one-to-one hosted API flag; review anti-debugging requirements manually.");
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--debug-protection-interval":
        args.compatibilityWarnings.push("--debug-protection-interval has no one-to-one hosted API flag; review anti-debugging requirements manually.");
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--disable-console-output":
        addReviewOnlyBooleanFlag(args, argv, i + 1, arg, "Console suppression has no direct hosted API option; handle console policy in application code.");
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--source-map":
        addReviewOnlyBooleanFlag(args, argv, i + 1, arg, "Source maps are not emitted by the hosted API workflow; keep release source maps out of protected artifacts.");
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--source-map-base-url":
      case "--source-map-file-name":
      case "--source-map-mode":
      case "--source-map-sources-mode":
        args.compatibilityWarnings.push(`${arg} was accepted for migration compatibility, but source maps are not emitted by the hosted API workflow.`);
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--identifier-names-cache":
      case "--identifier-names-cache-path":
        args.compatibilityWarnings.push(`${arg} was accepted for migration compatibility, but identifier-name cache files are not emitted by the hosted API workflow.`);
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--identifiers-dictionary":
      case "--identifiers-prefix":
        args.compatibilityWarnings.push(`${arg} was accepted for migration compatibility but hosted API naming controls do not expose the same identifier dictionary or prefix behavior; review output before release.`);
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--reserved-strings":
      case "--force-transform-strings":
      case "--input-file-name":
      case "--log":
      case "--numbers-to-expressions":
      case "--rename-properties-mode":
      case "--seed":
      case "--simplify":
      case "--split-strings":
      case "--split-strings-chunk-length":
      case "--string-array-calls-transform":
      case "--string-array-calls-transform-threshold":
      case "--string-array-indexes-type":
      case "--string-array-rotate":
      case "--string-array-shuffle":
      case "--string-array-wrappers-count":
      case "--string-array-wrappers-chained-calls":
      case "--string-array-wrappers-parameters-max-count":
      case "--string-array-wrappers-type":
      case "--transform-object-keys":
        args.compatibilityWarnings.push(`${arg} was accepted for migration compatibility but has no direct hosted API mapping; review output before release.`);
        i += consumeCompatibilityValue(argv, i + 1, arg);
        break;
      case "--compact": {
        const value = readOptionalBoolean(argv, i + 1);
        if (value) {
          args.options.push("SelfCompression=true");
          args.options.push("CompressionRatio=Best");
        } else {
          args.options.push("SelfCompression=false");
          args.options.push("WriteFormats=true");
        }
        if (hasOptionalValue(argv, i + 1)) i += 1;
        break;
      }
      case "--manifest":
        args.manifest = requireValue(argv, ++i, arg);
        break;
      case "--max-output-bytes":
        args.maxOutputBytes = parsePositiveNumber(requireValue(argv, ++i, arg), arg);
        break;
      case "--max-growth-ratio":
        args.maxGrowthRatio = parsePositiveNumber(requireValue(argv, ++i, arg), arg);
        break;
      case "--endpoint":
        args.endpoint = requireValue(argv, ++i, arg);
        break;
      case "--api-key":
        args.apiKey = requireValue(argv, ++i, arg);
        break;
      case "--api-password":
        args.apiPassword = requireValue(argv, ++i, arg);
        break;
      case "--doctor":
        args.doctor = true;
        break;
      case "--check-api":
        args.checkApi = true;
        break;
      case "--release-check":
        args.releaseCheck = true;
        break;
      case "--strict":
        args.strict = true;
        break;
      case "--validate-config":
        args.validateConfig = true;
        break;
      case "--print-config":
        args.printConfig = true;
        break;
      case "--list-presets":
        args.listPresets = true;
        break;
      case "--list-options":
        args.listOptions = true;
        break;
      case "--no-copy-assets":
        args.noCopyAssets = true;
        break;
      case "--dry-run":
        args.dryRun = true;
        break;
      case "--json":
        args.json = true;
        break;
      case "--init":
        args.init = true;
        break;
      case "--init-template":
        args.initTemplate = normalizeInitTemplateName(requireValue(argv, ++i, arg));
        break;
      case "--version":
      case "-v":
        args.version = true;
        break;
      case "--help":
      case "-h":
        args.help = true;
        break;
      default:
        if (arg.startsWith("--")) {
          throw new Error(`Unknown option: ${arg}`);
        }
        if (!args.input) args.input = arg;
        else if (!args.output) args.output = arg;
        else throw new Error(`Unexpected argument: ${arg}`);
    }
  }

  return args;
}

function normalizeInitTemplateName(value) {
  const normalized = String(value || "browser-app").trim().toLowerCase();
  if (normalized === "default" || normalized === "web" || normalized === "browser") {
    return "browser-app";
  }
  if (normalized === "html") {
    return "html-app";
  }
  if (normalized === "node") {
    return "node-app";
  }
  if (normalized === "electron" || normalized === "desktop") {
    return "electron-app";
  }
  if (!Object.prototype.hasOwnProperty.call(INIT_TEMPLATE_CONFIGS, normalized)) {
    throw new Error(`Unknown init template "${value}". Use browser-app, html-app, node-app, or electron-app.`);
  }
  return normalized;
}

function consumeCompatibilityValue(argv, index) {
  return hasOptionalValue(argv, index) ? 1 : 0;
}

function addReviewOnlyBooleanFlag(args, argv, index, option, enabledMessage) {
  const value = readOptionalBoolean(argv, index);
  if (value) {
    args.compatibilityWarnings.push(`${option}: ${enabledMessage}`);
  }
}

function hasOptionalValue(argv, index) {
  const value = argv[index];
  return !!value && !value.startsWith("--");
}

function readOptionalBoolean(argv, index) {
  if (!hasOptionalValue(argv, index)) return true;
  return parseOptionValue(argv[index]) !== false;
}

function addStringArrayEncodingOption(args, value) {
  const values = String(value).split(",").map((entry) => entry.trim()).filter(Boolean);
  if (values.some((entry) => entry.toLowerCase() === "rc4")) {
    args.options.push("EncryptStrings=true");
  } else if (values.length) {
    args.options.push("EncodeStrings=true");
  }
}

function deadCodeLevelFromThreshold(value) {
  const number = Number(value);
  if (!Number.isFinite(number)) {
    throw new Error("--dead-code-injection-threshold must be a number");
  }
  return number >= 0.66 ? "High" : number >= 0.33 ? "Medium" : "Low";
}

function identityStyleFromGenerator(value) {
  return String(value || "").toLowerCase().includes("hex") ? "v1hex" : "v2abcd";
}

function optimizationModeFromTarget(value) {
  return String(value || "").toLowerCase().includes("node") ? "NodeJS" : "Web";
}

function normalizeDomainLockList(value) {
  const raw = Array.isArray(value) ? value : String(value || "").split(/[,\n]/);
  return raw.map((entry) => String(entry).trim()).filter(Boolean);
}

function translateJavascriptObfuscatorConfigOptions(source = {}) {
  const translated = {
    options: {}
  };

  if (source.optionsPreset !== undefined) {
    translated.preset = presetFromJavascriptObfuscatorPreset(source.optionsPreset);
  }

  mapBooleanConfigOption(source, "stringArray", translated.options, "MoveStrings");
  mapBooleanConfigOption(source, "unicodeEscapeSequence", translated.options, "EncodeStrings");
  mapBooleanConfigOption(source, "controlFlowFlattening", translated.options, "DeepObfuscate");
  mapBooleanConfigOption(source, "controlFlowFlattening", translated.options, "FlatTransform");
  mapBooleanConfigOption(source, "deadCodeInjection", translated.options, "AddDeadCode");
  mapBooleanConfigOption(source, "renameGlobals", translated.options, "RenameGlobals");
  mapBooleanConfigOption(source, "renameProperties", translated.options, "RenameMembers");

  if (source.stringArrayEncoding !== undefined) {
    const encodings = Array.isArray(source.stringArrayEncoding) ? source.stringArrayEncoding : [source.stringArrayEncoding];
    if (encodings.some((encoding) => String(encoding).toLowerCase() === "rc4")) {
      translated.options.EncryptStrings = true;
    } else if (encodings.length) {
      translated.options.EncodeStrings = true;
    }
  }

  if (source.stringArrayThreshold !== undefined && Number(source.stringArrayThreshold) > 0) {
    translated.options.MoveStrings = true;
  }

  if (source.deadCodeInjectionThreshold !== undefined) {
    translated.options.DeadcodeLevel = deadCodeLevelFromThreshold(source.deadCodeInjectionThreshold);
  }

  if (source.identifierNamesGenerator !== undefined) {
    translated.options.IdentityStyle = identityStyleFromGenerator(source.identifierNamesGenerator);
  }

  if (source.compact === true) {
    translated.options.SelfCompression = true;
    translated.options.CompressionRatio = "Best";
  } else if (source.compact === false) {
    translated.options.SelfCompression = false;
    translated.options.WriteFormats = true;
  }

  if (source.target !== undefined) {
    translated.options.OptimizationMode = optimizationModeFromTarget(source.target);
  }

  if (source.parseHtml !== undefined) {
    translated.parseHtml = source.parseHtml === true;
  }

  if (source.ignoreImports !== undefined) {
    translated.ignoreImports = source.ignoreImports === true;
  }

  if (source.domainLock !== undefined) {
    const domains = normalizeDomainLockList(source.domainLock);
    if (domains.length) {
      translated.options.LockDomain = true;
      translated.options.LockDomainList = domains.join("\n");
    }
  }

  return translated;
}

function presetFromJsConfuserPreset(value) {
  const normalized = String(value || "").trim().toLowerCase();
  if (!normalized || normalized === "low") return "standard";
  if (normalized === "medium") return "balanced";
  if (normalized === "high") return "maximum";
  throw new Error(`Unknown JS-Confuser preset "${value}". Use low, medium, or high.`);
}

function translateJsConfuserConfigOptions(source = {}) {
  if (!hasJsConfuserCompatibilityOptions(source)) {
    return { options: {} };
  }

  const translated = {
    options: {}
  };

  if (source.preset !== undefined) {
    translated.preset = presetFromJsConfuserPreset(source.preset);
  }

  mapBooleanConfigOption(source, "renameVariables", translated.options, "ReplaceNames");
  mapBooleanConfigOption(source, "renameGlobals", translated.options, "RenameGlobals");
  mapBooleanConfigOption(source, "globalConcealing", translated.options, "RenameGlobals");
  mapBooleanConfigOption(source, "stringEncoding", translated.options, "EncodeStrings");
  mapBooleanConfigOption(source, "stringConcealing", translated.options, "EncryptStrings");
  mapBooleanConfigOption(source, "duplicateLiteralsRemoval", translated.options, "MoveStrings");
  mapBooleanConfigOption(source, "controlFlowFlattening", translated.options, "DeepObfuscate");
  mapBooleanConfigOption(source, "controlFlowFlattening", translated.options, "FlatTransform");
  mapBooleanConfigOption(source, "deadCode", translated.options, "AddDeadCode");

  if (source.stringCompression === true || source.minify === true || source.compact === true) {
    translated.options.SelfCompression = true;
    translated.options.CompressionRatio = "Best";
  } else if (source.minify === false || source.compact === false) {
    translated.options.WriteFormats = true;
  }

  if (source.identifierGenerator !== undefined) {
    translated.options.IdentityStyle = identityStyleFromGenerator(source.identifierGenerator);
  }

  if (source.target !== undefined) {
    translated.options.OptimizationMode = optimizationModeFromTarget(source.target);
  }

  if (source.lock && typeof source.lock === "object" && !Array.isArray(source.lock)) {
    const lock = source.lock;
    if (lock.domainLock !== undefined) {
      const domains = normalizeDomainLockList(lock.domainLock);
      if (domains.length) {
        translated.options.LockDomain = true;
        translated.options.LockDomainList = domains.join("\n");
      }
    }
    if (lock.endDate !== undefined) {
      translated.options.LockDate = true;
      translated.options.LockDateValue = formatLockDateValue(lock.endDate);
    }
  }

  return translated;
}

function hasJsConfuserCompatibilityOptions(source = {}) {
  if (!source || typeof source !== "object") return false;
  if (JS_CONFUSER_DETECT_KEYS.some((key) => Object.prototype.hasOwnProperty.call(source, key))) {
    return true;
  }
  const preset = String(source.preset || "").trim().toLowerCase();
  return preset === "low" || preset === "medium" || preset === "high";
}

function formatLockDateValue(value) {
  const text = String(value || "").trim();
  if (/^\d{8}$/.test(text)) return text;
  const date = new Date(text);
  if (!Number.isFinite(date.getTime())) return text;
  const year = String(date.getUTCFullYear()).padStart(4, "0");
  const month = String(date.getUTCMonth() + 1).padStart(2, "0");
  const day = String(date.getUTCDate()).padStart(2, "0");
  return `${year}${month}${day}`;
}

function mapBooleanConfigOption(source, sourceKey, target, targetKey) {
  if (source[sourceKey] === true) target[targetKey] = true;
}

function presetFromJavascriptObfuscatorPreset(value) {
  const normalized = String(value || "").trim().toLowerCase().replace(/[\s_]+/g, "-");
  if (
    !normalized ||
    normalized === "default" ||
    normalized === "low" ||
    normalized === "low-obfuscation" ||
    normalized === "standard" ||
    normalized === "vm-default" ||
    normalized === "vm-low" ||
    normalized === "vm-low-obfuscation"
  ) {
    return "standard";
  }
  if (
    normalized === "medium" ||
    normalized === "medium-obfuscation" ||
    normalized === "balanced" ||
    normalized === "vm-medium" ||
    normalized === "vm-medium-obfuscation"
  ) {
    return "balanced";
  }
  if (
    normalized === "high" ||
    normalized === "high-obfuscation" ||
    normalized === "maximum" ||
    normalized === "vm-high" ||
    normalized === "vm-high-obfuscation" ||
    normalized === "vm-ultra-high-obfuscation" ||
    normalized === "vm-anti-llm"
  ) {
    return "maximum";
  }
  throw new Error(`Unknown javascript-obfuscator options preset "${value}". Use default, low-obfuscation, medium-obfuscation, high-obfuscation, or one of the vm-* presets.`);
}

function writeCompatibilityWarnings(args) {
  for (const warning of args.compatibilityWarnings || []) {
    process.stderr.write(`Compatibility warning: ${warning}\n`);
  }
}

function requireValue(argv, index, option) {
  const value = argv[index];
  if (!value || value.startsWith("--")) {
    throw new Error(`Missing value for ${option}`);
  }
  return value;
}

function parsePositiveNumber(value, option) {
  const number = Number(value);
  if (!Number.isFinite(number) || number <= 0) {
    throw new Error(`${option} must be a positive number`);
  }
  return number;
}

function readConfig(configPath) {
  if (!configPath) {
    configPath = DEFAULT_CONFIG_FILES.map((fileName) => path.resolve(fileName)).find((filePath) => fs.existsSync(filePath));
    if (!configPath) return {};
  }

  const resolved = path.resolve(configPath);
  const config = { ...loadConfigFile(resolved) };
  config.__configDir = path.dirname(resolved);
  return config;
}

function loadConfigFile(resolved) {
  const extension = path.extname(resolved).toLowerCase();
  if (extension === ".mjs" || (extension === ".js" && isTypeModuleConfig(resolved))) {
    return loadEsmConfig(resolved);
  }
  if (extension === ".js" || extension === ".cjs") {
    return loadCommonJsConfig(resolved);
  }

  const raw = fs.readFileSync(resolved, "utf8");
  return JSON.parse(raw);
}

function loadCommonJsConfig(resolved) {
  try {
    delete require.cache[require.resolve(resolved)];
    const exported = require(resolved);
    const config = typeof exported === "function" ? exported({ cwd: process.cwd(), env: process.env }) : exported;
    if (!config || typeof config !== "object" || Array.isArray(config)) {
      throw new Error("JavaScript config must export an object or a function that returns an object.");
    }
    return config;
  } catch (error) {
    if (error && error.code === "ERR_REQUIRE_ESM") {
      return loadEsmConfig(resolved);
    }
    throw error;
  }
}

function isTypeModuleConfig(resolved) {
  let currentDir = path.dirname(resolved);
  const { root } = path.parse(currentDir);

  while (true) {
    const packageJsonPath = path.join(currentDir, "package.json");
    if (fs.existsSync(packageJsonPath)) {
      try {
        const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
        return packageJson && packageJson.type === "module";
      } catch (_error) {
        return false;
      }
    }

    if (currentDir === root) return false;
    currentDir = path.dirname(currentDir);
  }
}

function loadEsmConfig(resolved) {
  const raw = fs.readFileSync(resolved, "utf8");
  if (!/\bexport\s+default\b/.test(raw)) {
    throw new Error("ES module config files must use a default export object or function.");
  }

  const compiledSource = raw.replace(/\bexport\s+default\b/, "module.exports =");
  const esmModule = new Module(resolved, module);
  esmModule.filename = resolved;
  esmModule.paths = Module._nodeModulePaths(path.dirname(resolved));

  try {
    esmModule._compile(compiledSource, resolved);
    const exported = esmModule.exports;
    const config = typeof exported === "function"
      ? exported({ cwd: process.cwd(), env: process.env })
      : exported;

    if (!config || typeof config !== "object" || Array.isArray(config)) {
      throw new Error("JavaScript config must export an object or a function that returns an object.");
    }
    return config;
  } catch (error) {
    throw new Error(`Failed to load ES module config ${resolved}: ${error.message}`);
  }
}

function mergeConfig(config, args = {}) {
  const baseDir = config.__configDir || process.cwd();
  const webPreset = loadWebPreset(config, args, baseDir);
  const compatibility = translateJavascriptObfuscatorConfigOptions(config);
  const jsConfuserCompatibility = translateJsConfuserConfigOptions(config);
  const preset = normalizePresetName(args.preset || config.preset || jsConfuserCompatibility.preset || compatibility.preset || (webPreset && webPreset.preset) || "standard");
  const output = args.output || config.output || getCompatibilityDefaultOutput(args, config, baseDir) || "dist-protected";
  const convenienceOptions = mapConvenienceOptions(config, args);
  const options = {
    ...getPresetOptions(preset),
    ...mapWebPresetToOptions(webPreset),
    ...(compatibility.options || {}),
    ...(jsConfuserCompatibility.options || {}),
    ...convenienceOptions,
    ...(config.options || {}),
    ...parseOptionOverrides(args.options || [])
  };
  applyReservedNames(options, {
    ...config,
    reservedNames: (args.reservedNames && args.reservedNames.length) ? args.reservedNames : config.reservedNames
  });

  const merged = {
    endpoint: args.endpoint || config.endpoint || readEnv(["JSO_ENDPOINT", "JAVASCRIPT_OBFUSCATOR_ENDPOINT"]) || DEFAULT_ENDPOINT,
    apiKey: resolveEnv(args.apiKey || config.apiKey || readEnv(["JSO_API_KEY", "JAVASCRIPT_OBFUSCATOR_API_KEY"]) || ""),
    apiPassword: resolveEnv(args.apiPassword || config.apiPassword || readEnv(["JSO_API_PASSWORD", "JAVASCRIPT_OBFUSCATOR_API_PASSWORD"]) || ""),
    projectName: config.projectName || "jso-protector",
    preset,
    input: args.input || config.input || "dist",
    output,
    include: (args.include && args.include.length) ? args.include : (config.include || []),
    extensions: normalizeExtensions(config.extensions || DEFAULT_EXTENSIONS),
    markupExtensions: normalizeExtensions(config.markupExtensions || DEFAULT_MARKUP_EXTENSIONS),
    exclude: mergePatternLists(config.exclude || DEFAULT_EXCLUDE, args.exclude || []),
    assetExclude: mergePatternLists(config.assetExclude || DEFAULT_ASSET_EXCLUDE, args.assetExclude || []),
    copyAssets: args.noCopyAssets ? false : config.copyAssets !== false,
    mixedServer: !!config.mixedServer,
    parseHtml: args.parseHtml !== undefined && args.parseHtml !== null ? !!args.parseHtml : !!(config.parseHtml || compatibility.parseHtml),
    honorConditionalComments: !!(args.honorConditionalComments || config.honorConditionalComments),
    ignoreImports: args.ignoreImports !== undefined && args.ignoreImports !== null ? !!args.ignoreImports : !!(config.ignoreImports || compatibility.ignoreImports),
    manifest: args.manifest || config.manifest || null,
    maxOutputBytes: args.maxOutputBytes || config.maxOutputBytes || null,
    maxGrowthRatio: args.maxGrowthRatio || config.maxGrowthRatio || null,
    options
  };

  validateStringArray("include", merged.include);
  if (merged.parseHtml) {
    merged.extensions = mergePatternLists(merged.extensions, merged.markupExtensions);
  }
  validateStringArray("extensions", merged.extensions);
  validateStringArray("markupExtensions", merged.markupExtensions);
  validateStringArray("exclude", merged.exclude);
  validateStringArray("assetExclude", merged.assetExclude);

  merged.input = resolvePath(baseDir, merged.input);
  merged.output = resolvePath(baseDir, merged.output);
  if (merged.manifest) merged.manifest = resolvePath(baseDir, merged.manifest);
  return merged;
}

function mergePatternLists(base, additions) {
  return Array.from(new Set([...(base || []), ...(additions || [])]));
}

function mapConvenienceOptions(config = {}, args = {}) {
  const mapped = {};
  for (const alias of CONVENIENCE_OPTION_ALIASES) {
    const value = args[alias.configKey] !== undefined && args[alias.configKey] !== null
      ? args[alias.configKey]
      : config[alias.configKey];
    if (value === undefined || value === null) continue;
    mapped[alias.optionName] = alias.type === "boolean" ? value === true : value;
  }
  return mapped;
}

function getCompatibilityDefaultOutput(args, config, baseDir) {
  if (!args.input || args.output || config.output) return null;

  const resolvedInput = resolvePath(baseDir, args.input);
  if (!fs.existsSync(resolvedInput) || !fs.statSync(resolvedInput).isFile()) return null;

  const ext = path.extname(resolvedInput) || ".js";
  const baseName = path.basename(resolvedInput, ext);
  return path.join(path.dirname(resolvedInput), `${baseName}-obfuscated${ext}`);
}

function loadWebPreset(config, args, baseDir) {
  if (config.format === WEB_PRESET_FORMAT) return config;

  const webPresetPath = args.webPreset || config.webPreset;
  if (!webPresetPath) return null;

  const resolved = resolvePath(baseDir, webPresetPath);
  const webPreset = JSON.parse(fs.readFileSync(resolved, "utf8"));
  if (webPreset.format !== WEB_PRESET_FORMAT) {
    throw new Error(`Web preset must use format "${WEB_PRESET_FORMAT}"`);
  }
  if (webPreset.version !== 1) {
    throw new Error(`Unsupported web preset version: ${webPreset.version}`);
  }
  return webPreset;
}

function normalizePresetName(value) {
  const name = String(value || "standard").toLowerCase();
  if (!Object.prototype.hasOwnProperty.call(PRESET_OPTIONS, name)) {
    throw new Error(`Unknown preset "${value}". Use standard, balanced, or maximum.`);
  }
  return name;
}

function getPresetOptions(preset) {
  return { ...PRESET_OPTIONS[normalizePresetName(preset)] };
}

function mapWebPresetToOptions(webPreset) {
  if (!webPreset) return {};
  if (webPreset.format !== WEB_PRESET_FORMAT) {
    throw new Error(`Web preset must use format "${WEB_PRESET_FORMAT}"`);
  }
  if (webPreset.version !== 1) {
    throw new Error(`Unsupported web preset version: ${webPreset.version}`);
  }

  const options = {};
  const standard = webPreset.standardOptions || {};
  if (standard.keepLinefeeds || standard.keepIndentations) options.WriteFormats = true;
  if (standard.keepIndentations) options.WriteFormats_KeepIndent = true;
  if (standard.encodeStrings) options.EncodeStrings = true;
  if (standard.moveStrings) options.MoveStrings = true;
  if (standard.replaceNames) options.ReplaceNames = true;

  for (const feature of webPreset.advancedFeatures || []) {
    Object.assign(options, WEB_FEATURE_OPTIONS[feature] || {});
  }

  if (typeof webPreset.variableExclusionList === "string" && webPreset.variableExclusionList.trim()) {
    options.VariableExclusion = webPreset.variableExclusionList;
  }

  return options;
}

function applyReservedNames(options, config) {
  const optionReservedNames = Array.isArray(options.reservedNames) ? options.reservedNames : null;
  delete options.reservedNames;

  if (options.VariableExclusion) return;
  if (typeof config.variableExclusion === "string" && config.variableExclusion.trim()) {
    options.VariableExclusion = config.variableExclusion;
    return;
  }
  if (Array.isArray(config.reservedNames) && config.reservedNames.length) {
    options.VariableExclusion = config.reservedNames.join("\n");
    return;
  }
  if (optionReservedNames && optionReservedNames.length) {
    options.VariableExclusion = optionReservedNames.join("\n");
  }
}

function parseOptionOverrides(values) {
  const options = {};
  for (const entry of values || []) {
    const index = String(entry).indexOf("=");
    if (index <= 0) {
      throw new Error(`Invalid --option value "${entry}". Use Name=value.`);
    }
    const key = entry.slice(0, index).trim();
    if (!key) {
      throw new Error(`Invalid --option value "${entry}". Use Name=value.`);
    }
    options[key] = parseOptionValue(entry.slice(index + 1));
  }
  return options;
}

function parseOptionValue(value) {
  const trimmed = String(value).trim();
  if (/^(true|false)$/i.test(trimmed)) return trimmed.toLowerCase() === "true";
  if (/^null$/i.test(trimmed)) return null;
  if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
  return value;
}

function validateStringArray(name, value) {
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
    throw new Error(`${name} must be an array of strings`);
  }
}

function validateOptionalString(name, value) {
  if (value !== undefined && value !== null && typeof value !== "string") {
    throw new Error(`${name} must be a string`);
  }
}

function validateOptionalBoolean(name, value) {
  if (value !== undefined && value !== null && typeof value !== "boolean") {
    throw new Error(`${name} must be a boolean`);
  }
}

function validateOptionalPositiveNumber(name, value) {
  if (value !== undefined && value !== null && (!Number.isFinite(Number(value)) || Number(value) <= 0)) {
    throw new Error(`${name} must be a positive number`);
  }
}

function validateOptionalNonNegativeNumber(name, value) {
  if (value !== undefined && value !== null && (!Number.isFinite(Number(value)) || Number(value) < 0)) {
    throw new Error(`${name} must be a non-negative number`);
  }
}

function resolveEnv(value) {
  if (typeof value !== "string") return value;
  if (value.startsWith("$") && value.length > 1) {
    return process.env[value.slice(1)] || "";
  }
  return value;
}

function readEnv(names) {
  for (const name of names) {
    if (process.env[name]) return process.env[name];
  }
  return "";
}

function resolvePath(baseDir, value) {
  if (!value) return value;
  if (path.isAbsolute(value)) return value;
  return path.resolve(baseDir, value);
}

function normalizeExtensions(extensions) {
  return extensions.map((ext) => ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`);
}

function createExampleConfig(initOptions = {}) {
  const normalizedOptions = typeof initOptions === "string" ? { template: initOptions } : (initOptions || {});
  const templateName = normalizeInitTemplateName(normalizedOptions.template || "browser-app");
  const template = INIT_TEMPLATE_CONFIGS[templateName];
  const config = {
    $schema: "./node_modules/jso-protector/jso.config.schema.json",
    endpoint: DEFAULT_ENDPOINT,
    apiKey: "$JSO_API_KEY",
    apiPassword: "$JSO_API_PASSWORD",
    projectName: template.projectName,
    input: template.input,
    output: template.output,
    preset: template.preset,
    extensions: template.extensions.slice(),
    exclude: template.exclude.slice(),
    copyAssets: template.copyAssets,
    assetExclude: template.assetExclude.slice(),
    mixedServer: template.mixedServer,
    parseHtml: template.parseHtml,
    honorConditionalComments: template.honorConditionalComments,
    ignoreImports: template.ignoreImports,
    keepHeaderComment: template.keepHeaderComment,
    protectObjectDeclaration: template.protectObjectDeclaration,
    moveNestedFunction: template.moveNestedFunction,
    formattedOutput: template.formattedOutput,
    keepIndent: template.keepIndent,
    lineNumbers: template.lineNumbers,
    reservedNames: template.reservedNames.slice(),
    options: { ...template.options }
  };

  if (normalizedOptions.input) config.input = normalizedOptions.input;
  if (normalizedOptions.output) config.output = normalizedOptions.output;
  if (normalizedOptions.preset) config.preset = normalizedOptions.preset;
  if (normalizedOptions.include && normalizedOptions.include.length) config.include = normalizedOptions.include.slice();
  if (normalizedOptions.exclude && normalizedOptions.exclude.length) config.exclude = normalizedOptions.exclude.slice();
  if (normalizedOptions.assetExclude && normalizedOptions.assetExclude.length) config.assetExclude = normalizedOptions.assetExclude.slice();
  if (normalizedOptions.parseHtml !== null && normalizedOptions.parseHtml !== undefined) config.parseHtml = !!normalizedOptions.parseHtml;
  if (normalizedOptions.honorConditionalComments !== null && normalizedOptions.honorConditionalComments !== undefined) {
    config.honorConditionalComments = !!normalizedOptions.honorConditionalComments;
  };
  if (normalizedOptions.ignoreImports !== null && normalizedOptions.ignoreImports !== undefined) config.ignoreImports = !!normalizedOptions.ignoreImports;
  if (normalizedOptions.manifest) config.manifest = normalizedOptions.manifest;
  if (normalizedOptions.maxOutputBytes !== null && normalizedOptions.maxOutputBytes !== undefined) config.maxOutputBytes = normalizedOptions.maxOutputBytes;
  if (normalizedOptions.maxGrowthRatio !== null && normalizedOptions.maxGrowthRatio !== undefined) config.maxGrowthRatio = normalizedOptions.maxGrowthRatio;
  if (normalizedOptions.noCopyAssets) config.copyAssets = false;
  if (normalizedOptions.reservedNames && normalizedOptions.reservedNames.length) config.reservedNames = normalizedOptions.reservedNames.slice();
  if (normalizedOptions.options && normalizedOptions.options.length) {
    config.options = {
      ...config.options,
      ...parseOptionOverrides(normalizedOptions.options)
    };
  }

  return config;
}

function initConfig(args = {}) {
  const target = path.resolve(DEFAULT_CONFIG_FILE);
  if (fs.existsSync(target)) {
    throw new Error(`${DEFAULT_CONFIG_FILE} already exists`);
  }
  const templateName = normalizeInitTemplateName(args.initTemplate || "browser-app");
  fs.writeFileSync(target, `${JSON.stringify(createExampleConfig({
    ...args,
    template: templateName
  }), null, 2)}\n`, "utf8");
  process.stdout.write(`Created ${target}\n`);
  process.stdout.write(`Template: ${templateName}\n`);
  process.stdout.write("This package is local-only. Install it from a workspace path, file: dependency, or internal npm pack tarball.\n");
  process.stdout.write("Next: jso-protector --config jso.config.json --release-check --json\n");
}

function migrateJavascriptObfuscatorConfig(sourcePath, args = {}) {
  if (!sourcePath) {
    throw new Error("--migrate-javascript-obfuscator requires a config file path");
  }
  const sourceConfigPath = path.resolve(sourcePath);
  const sourceConfig = loadConfigFile(sourceConfigPath);
  if (!sourceConfig || typeof sourceConfig !== "object" || Array.isArray(sourceConfig)) {
    throw new Error("javascript-obfuscator config must be an object");
  }

  const config = {
    $schema: "./node_modules/jso-protector/jso.config.schema.json",
    endpoint: DEFAULT_ENDPOINT,
    apiKey: "$JSO_API_KEY",
    apiPassword: "$JSO_API_PASSWORD",
    projectName: "browser-release",
    input: args.input || "dist",
    output: args.output && path.extname(args.output).toLowerCase() !== ".json" ? args.output : "dist-protected",
    preset: "balanced",
    extensions: [".js", ".jsx"],
    exclude: ["**/*.map", "**/vendor/**", "**/polyfills-*.js", "**/*-obfuscated.js"],
    copyAssets: true,
    assetExclude: ["**/*.map"],
    mixedServer: false,
    parseHtml: !!sourceConfig.parseHtml,
    honorConditionalComments: false,
    ignoreImports: !!sourceConfig.ignoreImports,
    reservedNames: [],
    manifest: "dist-protected/jso-manifest.json",
    maxGrowthRatio: 8,
    options: {
      OptimizationMode: "Web",
      LockDomain: false,
      LockDate: false
    }
  };
  const notes = [];
  const mapped = [];
  const review = [];

  if (sourceConfig.optionsPreset !== undefined) {
    config.preset = presetFromJavascriptObfuscatorPreset(sourceConfig.optionsPreset);
    mapped.push({ from: "optionsPreset", to: "preset", note: `Options preset mapped to ${config.preset}.` });
  }

  mapBooleanOption(sourceConfig, "stringArray", config.options, "MoveStrings", mapped);
  mapBooleanOption(sourceConfig, "unicodeEscapeSequence", config.options, "EncodeStrings", mapped);
  mapBooleanOption(sourceConfig, "controlFlowFlattening", config.options, "FlatTransform", mapped);
  mapBooleanOption(sourceConfig, "controlFlowFlattening", config.options, "DeepObfuscate", mapped);
  mapBooleanOption(sourceConfig, "deadCodeInjection", config.options, "AddDeadCode", mapped);
  mapBooleanOption(sourceConfig, "renameGlobals", config.options, "RenameGlobals", mapped);
  mapBooleanOption(sourceConfig, "renameProperties", config.options, "RenameMembers", mapped);

  if (sourceConfig.stringArrayEncoding !== undefined) {
    const encodings = Array.isArray(sourceConfig.stringArrayEncoding) ? sourceConfig.stringArrayEncoding : [sourceConfig.stringArrayEncoding];
    if (encodings.some((encoding) => String(encoding).toLowerCase() === "rc4")) {
      config.options.EncryptStrings = true;
      mapped.push({ from: "stringArrayEncoding", to: "EncryptStrings", note: "rc4-style string encoding maps to encrypted strings." });
    } else if (encodings.length) {
      config.options.EncodeStrings = true;
      mapped.push({ from: "stringArrayEncoding", to: "EncodeStrings", note: "String encoding maps to encoded strings." });
    }
  }

  if (sourceConfig.stringArrayThreshold !== undefined && Number(sourceConfig.stringArrayThreshold) > 0) {
    config.options.MoveStrings = true;
    mapped.push({ from: "stringArrayThreshold", to: "MoveStrings", note: "Threshold values are not one-to-one; review output size and behavior." });
  }

  if (sourceConfig.deadCodeInjectionThreshold !== undefined) {
    const threshold = Number(sourceConfig.deadCodeInjectionThreshold);
    config.options.DeadcodeLevel = threshold >= 0.66 ? "High" : threshold >= 0.33 ? "Medium" : "Low";
    mapped.push({ from: "deadCodeInjectionThreshold", to: "DeadcodeLevel", note: "Threshold converted to nearest Low/Medium/High level." });
  }

  if (sourceConfig.identifierNamesGenerator !== undefined) {
    const generator = String(sourceConfig.identifierNamesGenerator).toLowerCase();
    config.options.IdentityStyle = generator.includes("hex") ? "v1hex" : "v2abcd";
    mapped.push({ from: "identifierNamesGenerator", to: "IdentityStyle", note: "Identifier generator converted to the nearest API identity style." });
  }

  if (sourceConfig.compact === true) {
    config.options.SelfCompression = true;
    config.options.CompressionRatio = "Best";
    mapped.push({ from: "compact", to: "SelfCompression", note: "Compact output maps to self-compression." });
  } else if (sourceConfig.compact === false) {
    config.options.WriteFormats = true;
    mapped.push({ from: "compact", to: "WriteFormats", note: "Non-compact output maps to formatted output for review builds." });
  }

  if (sourceConfig.target !== undefined) {
    const target = String(sourceConfig.target).toLowerCase();
    if (target.includes("node")) config.options.OptimizationMode = "NodeJS";
    else if (target.includes("browser")) config.options.OptimizationMode = "Web";
    mapped.push({ from: "target", to: "OptimizationMode", note: "Target converted to nearest runtime optimization mode." });
  }

  if (Array.isArray(sourceConfig.reservedNames) && sourceConfig.reservedNames.length) {
    config.reservedNames = sourceConfig.reservedNames.map((value) => String(value));
    mapped.push({ from: "reservedNames", to: "reservedNames", note: "Reserved name expressions are preserved." });
  }

  if (sourceConfig.domainLock !== undefined) {
    const domains = normalizeDomainLockList(sourceConfig.domainLock);
    if (domains.length) {
      config.options.LockDomain = true;
      config.options.LockDomainList = domains.join("\n");
      mapped.push({ from: "domainLock", to: "LockDomainList", note: "Domain lock entries map to LockDomain and LockDomainList; review subdomain and redirect behavior." });
    }
  }

  if (sourceConfig.parseHtml !== undefined) {
    config.parseHtml = sourceConfig.parseHtml === true;
    if (config.parseHtml) {
      mapped.push({ from: "parseHtml", to: "parseHtml", note: "Marked inline markup script protection is enabled." });
    }
  }

  for (const [key, note] of Object.entries(JAVASCRIPT_OBFUSCATOR_REVIEW_OPTIONS)) {
    if (Object.prototype.hasOwnProperty.call(sourceConfig, key)) {
      review.push({ option: key, note });
    }
  }

  const handled = new Set([
    "compact",
    "controlFlowFlattening",
    "deadCodeInjection",
    "deadCodeInjectionThreshold",
    "domainLock",
    "identifierNamesGenerator",
    "optionsPreset",
    "parseHtml",
    "renameGlobals",
    "renameProperties",
    "reservedNames",
    "stringArray",
    "stringArrayEncoding",
    "stringArrayThreshold",
    "target",
    "unicodeEscapeSequence",
    ...Object.keys(JAVASCRIPT_OBFUSCATOR_REVIEW_OPTIONS)
  ]);
  const unmapped = Object.keys(sourceConfig).filter((key) => !handled.has(key)).sort();
  if (unmapped.length) {
    notes.push(`Unmapped javascript-obfuscator option(s): ${unmapped.join(", ")}.`);
  }
  if (review.length) {
    notes.push("Some source options need manual review because they are not direct one-to-one API settings.");
  }
  notes.push("Run --validate-config after saving, then protect a separate output folder and smoke-test the result.");

  return {
    format: "jso-protector-migration",
    version: 1,
    source: sourceConfigPath,
    summary: buildMigrationReportSummary(sourceConfig, mapped, review, unmapped),
    config,
    mapped,
    review,
    unmapped,
    reviewReference: listJavascriptObfuscatorMigrationMap().review,
    nextCommands: buildMigrationNextCommands(args),
    notes
  };
}

function migrateJsConfuserConfig(sourcePath, args = {}) {
  if (!sourcePath) {
    throw new Error("--migrate-js-confuser requires a config file path");
  }
  const sourceConfigPath = path.resolve(sourcePath);
  const sourceConfig = loadConfigFile(sourceConfigPath);
  if (!sourceConfig || typeof sourceConfig !== "object" || Array.isArray(sourceConfig)) {
    throw new Error("JS-Confuser config must be an object");
  }

  const config = {
    $schema: "./node_modules/jso-protector/jso.config.schema.json",
    endpoint: DEFAULT_ENDPOINT,
    apiKey: "$JSO_API_KEY",
    apiPassword: "$JSO_API_PASSWORD",
    projectName: "browser-release",
    input: args.input || "dist",
    output: args.output && path.extname(args.output).toLowerCase() !== ".json" ? args.output : "dist-protected",
    preset: "balanced",
    extensions: [".js", ".jsx"],
    exclude: ["**/*.map", "**/vendor/**", "**/polyfills-*.js", "**/*-obfuscated.js"],
    copyAssets: true,
    assetExclude: ["**/*.map"],
    mixedServer: false,
    parseHtml: false,
    honorConditionalComments: false,
    ignoreImports: false,
    reservedNames: [],
    manifest: "dist-protected/jso-manifest.json",
    maxGrowthRatio: 8,
    options: {
      OptimizationMode: "Web",
      LockDomain: false,
      LockDate: false
    }
  };
  const notes = [];
  const mapped = [];
  const review = [];

  if (sourceConfig.preset !== undefined) {
    config.preset = presetFromJsConfuserPreset(sourceConfig.preset);
    mapped.push({ from: "preset", to: "preset", note: `JS-Confuser preset mapped to ${config.preset}.` });
  }

  mapBooleanOption(sourceConfig, "renameVariables", config.options, "ReplaceNames", mapped);
  mapBooleanOption(sourceConfig, "renameGlobals", config.options, "RenameGlobals", mapped);
  mapBooleanOption(sourceConfig, "globalConcealing", config.options, "RenameGlobals", mapped);
  mapBooleanOption(sourceConfig, "stringEncoding", config.options, "EncodeStrings", mapped);
  mapBooleanOption(sourceConfig, "stringConcealing", config.options, "EncryptStrings", mapped);
  mapBooleanOption(sourceConfig, "duplicateLiteralsRemoval", config.options, "MoveStrings", mapped);
  mapBooleanOption(sourceConfig, "controlFlowFlattening", config.options, "FlatTransform", mapped);
  mapBooleanOption(sourceConfig, "controlFlowFlattening", config.options, "DeepObfuscate", mapped);
  mapBooleanOption(sourceConfig, "deadCode", config.options, "AddDeadCode", mapped);

  if (sourceConfig.stringCompression === true || sourceConfig.minify === true || sourceConfig.compact === true) {
    config.options.SelfCompression = true;
    config.options.CompressionRatio = "Best";
    mapped.push({ from: sourceConfig.stringCompression === true ? "stringCompression" : sourceConfig.minify === true ? "minify" : "compact", to: "SelfCompression", note: "Compression/minify maps to self-compression." });
  } else if (sourceConfig.minify === false || sourceConfig.compact === false) {
    config.options.WriteFormats = true;
    mapped.push({ from: sourceConfig.minify === false ? "minify" : "compact", to: "WriteFormats", note: "Disabled minify/compact maps to formatted output for review builds." });
  }

  if (sourceConfig.identifierGenerator !== undefined) {
    config.options.IdentityStyle = identityStyleFromGenerator(sourceConfig.identifierGenerator);
    mapped.push({ from: "identifierGenerator", to: "IdentityStyle", note: "Identifier generator converted to the nearest API identity style." });
  }

  if (sourceConfig.target !== undefined) {
    config.options.OptimizationMode = optimizationModeFromTarget(sourceConfig.target);
    mapped.push({ from: "target", to: "OptimizationMode", note: "Target converted to the nearest runtime optimization mode." });
  }

  if (sourceConfig.lock && typeof sourceConfig.lock === "object" && !Array.isArray(sourceConfig.lock)) {
    const lock = sourceConfig.lock;
    if (lock.domainLock !== undefined) {
      const domains = normalizeDomainLockList(lock.domainLock);
      if (domains.length) {
        config.options.LockDomain = true;
        config.options.LockDomainList = domains.join("\n");
        mapped.push({ from: "lock.domainLock", to: "LockDomainList", note: "Domain lock entries map to LockDomain and LockDomainList; review subdomain behavior." });
      }
    }
    if (lock.endDate !== undefined) {
      config.options.LockDate = true;
      config.options.LockDateValue = formatLockDateValue(lock.endDate);
      mapped.push({ from: "lock.endDate", to: "LockDateValue", note: "End-date locks map to LockDate and LockDateValue." });
    }

    for (const [key, note] of Object.entries(JS_CONFUSER_REVIEW_OPTIONS)) {
      if (!key.startsWith("lock.")) continue;
      const lockKey = key.slice("lock.".length);
      if (Object.prototype.hasOwnProperty.call(lock, lockKey)) {
        review.push({ option: key, note });
      }
    }
  }

  for (const [key, note] of Object.entries(JS_CONFUSER_REVIEW_OPTIONS)) {
    if (key.startsWith("lock.")) continue;
    if (Object.prototype.hasOwnProperty.call(sourceConfig, key)) {
      review.push({ option: key, note });
    }
  }

  const handled = new Set([
    "compact",
    "controlFlowFlattening",
    "deadCode",
    "duplicateLiteralsRemoval",
    "globalConcealing",
    "identifierGenerator",
    "lock",
    "minify",
    "preset",
    "renameGlobals",
    "renameVariables",
    "stringCompression",
    "stringConcealing",
    "stringEncoding",
    "target",
    ...Object.keys(JS_CONFUSER_REVIEW_OPTIONS).filter((key) => !key.startsWith("lock."))
  ]);
  const unmapped = Object.keys(sourceConfig).filter((key) => !handled.has(key)).sort();
  if (unmapped.length) {
    notes.push(`Unmapped JS-Confuser option(s): ${unmapped.join(", ")}.`);
  }
  if (review.length) {
    notes.push("Some JS-Confuser options need manual review because they are not direct one-to-one hosted API settings.");
  }
  notes.push("Run --validate-config after saving, then protect a separate output folder and smoke-test the result.");

  return {
    format: "jso-protector-migration",
    version: 1,
    source: sourceConfigPath,
    summary: buildMigrationReportSummaryFromMap(sourceConfig, mapped, review, unmapped, JS_CONFUSER_MIGRATION_MAP),
    config,
    mapped,
    review,
    unmapped,
    reviewReference: listJsConfuserMigrationMap().review,
    nextCommands: buildMigrationNextCommands(args),
    notes
  };
}

function buildMigrationNextCommands(args = {}) {
  const configPath = args.output ? path.resolve(args.output) : "jso.config.json";
  const configArg = quoteCommandArg(configPath);
  return [
    {
      label: "validate",
      command: `jso-protector --config ${configArg} --validate-config --json`
    },
    {
      label: "preview",
      command: `jso-protector --config ${configArg} --dry-run --json`
    },
    {
      label: "doctor",
      command: `jso-protector --config ${configArg} --doctor --json`
    },
    {
      label: "release-check",
      command: `jso-protector --config ${configArg} --release-check --json`
    },
    {
      label: "protect",
      command: `jso-protector --config ${configArg} --manifest dist-protected/jso-manifest.json`
    }
  ];
}

function quoteCommandArg(value) {
  const text = String(value || "");
  if (/^[A-Za-z0-9_./:\\-]+$/.test(text)) return text;
  return `"${text.replace(/"/g, '\\"')}"`;
}

function buildMigrationReportSummary(sourceConfig, mapped, review, unmapped) {
  const sourceOptions = Object.keys(sourceConfig || {}).length;
  const mappedSources = Array.from(new Set((mapped || []).map((item) => item.from)));
  const confidenceBySource = new Map(JAVASCRIPT_OBFUSCATOR_MIGRATION_MAP.map((entry) => [entry.source, entry.confidence]));
  const direct = mappedSources.filter((source) => confidenceBySource.get(source) === "direct").length;
  const approximate = mappedSources.filter((source) => confidenceBySource.get(source) === "approximate").length;
  return {
    sourceOptions,
    mappedOptions: mappedSources.length,
    direct,
    approximate,
    reviewOnly: (review || []).length,
    unmapped: (unmapped || []).length,
    automaticCoverage: sourceOptions ? Number((mappedSources.length / sourceOptions).toFixed(4)) : 1
  };
}

function buildMigrationReportSummaryFromMap(sourceConfig, mapped, review, unmapped, migrationMap) {
  const sourceOptions = Object.keys(sourceConfig || {}).length;
  const mappedSources = Array.from(new Set((mapped || []).map((item) => item.from)));
  const confidenceBySource = new Map((migrationMap || []).map((entry) => [entry.source, entry.confidence]));
  const direct = mappedSources.filter((source) => confidenceBySource.get(source) === "direct").length;
  const approximate = mappedSources.filter((source) => confidenceBySource.get(source) === "approximate").length;
  return {
    sourceOptions,
    mappedOptions: mappedSources.length,
    direct,
    approximate,
    reviewOnly: (review || []).length,
    unmapped: (unmapped || []).length,
    automaticCoverage: sourceOptions ? Number((mappedSources.length / sourceOptions).toFixed(4)) : 1
  };
}

function listJavascriptObfuscatorMigrationMap() {
  const mappings = JAVASCRIPT_OBFUSCATOR_MIGRATION_MAP.map((entry) => ({
    source: entry.source,
    target: entry.target.slice(),
    confidence: entry.confidence,
    note: entry.note
  }));
  const review = Object.entries(JAVASCRIPT_OBFUSCATOR_REVIEW_OPTIONS).map(([option, note]) => ({
    option,
    note
  }));

  return {
    summary: buildMigrationMapSummary(mappings, review),
    mappings,
    review
  };
}

function listJsConfuserMigrationMap() {
  const mappings = JS_CONFUSER_MIGRATION_MAP.map((entry) => ({
    source: entry.source,
    target: entry.target.slice(),
    confidence: entry.confidence,
    note: entry.note
  }));
  const review = Object.entries(JS_CONFUSER_REVIEW_OPTIONS).map(([option, note]) => ({
    option,
    note
  }));

  return {
    summary: buildMigrationMapSummary(mappings, review),
    mappings,
    review
  };
}

function normalizeCompatibilityOptionName(option) {
  const text = String(option || "").trim();
  if (!text) return "";
  const withoutPrefix = text.replace(/^--/, "");
  if (!withoutPrefix.includes("-")) return withoutPrefix;
  return withoutPrefix.replace(/-([a-z0-9])/g, (_match, letter) => letter.toUpperCase());
}

function explainCompatibilityOption(option) {
  const normalized = normalizeCompatibilityOptionName(option);
  const mapped = JAVASCRIPT_OBFUSCATOR_MIGRATION_MAP.find((entry) => entry.source.toLowerCase() === normalized.toLowerCase());
  if (mapped) {
    return {
      option: normalized,
      status: "mapped",
      target: mapped.target.slice(),
      confidence: mapped.confidence,
      note: mapped.note
    };
  }

  const reviewKey = Object.keys(JAVASCRIPT_OBFUSCATOR_REVIEW_OPTIONS).find((key) => key.toLowerCase() === normalized.toLowerCase());
  if (reviewKey) {
    return {
      option: reviewKey,
      status: "review-only",
      target: [],
      confidence: "review",
      note: JAVASCRIPT_OBFUSCATOR_REVIEW_OPTIONS[reviewKey]
    };
  }

  return {
    option: normalized || option,
    status: "unknown",
    target: [],
    confidence: "unknown",
    note: "No local compatibility mapping is known yet. Run --list-migration-map --json to inspect supported mappings and review-only options."
  };
}

function explainJsConfuserCompatibilityOption(option) {
  const normalized = String(option || "").trim().replace(/^--/, "");
  const mapped = JS_CONFUSER_MIGRATION_MAP.find((entry) => entry.source.toLowerCase() === normalized.toLowerCase());
  if (mapped) {
    return {
      option: mapped.source,
      status: "mapped",
      target: mapped.target.slice(),
      confidence: mapped.confidence,
      note: mapped.note
    };
  }

  const reviewKey = Object.keys(JS_CONFUSER_REVIEW_OPTIONS).find((key) => key.toLowerCase() === normalized.toLowerCase());
  if (reviewKey) {
    return {
      option: reviewKey,
      status: "review-only",
      target: [],
      confidence: "review",
      note: JS_CONFUSER_REVIEW_OPTIONS[reviewKey]
    };
  }

  return {
    option: normalized || option,
    status: "unknown",
    target: [],
    confidence: "unknown",
    note: "No local JS-Confuser compatibility mapping is known yet. Run --list-js-confuser-migration-map --json to inspect supported mappings and review-only options."
  };
}

function writeCompatibilityExplanation(option, json) {
  const explanation = explainCompatibilityOption(option);
  if (json) {
    process.stdout.write(`${JSON.stringify(explanation, null, 2)}\n`);
    return;
  }
  process.stdout.write(`${explanation.option}: ${explanation.status}\n`);
  if (explanation.target.length) process.stdout.write(`Targets: ${explanation.target.join(", ")}\n`);
  process.stdout.write(`${explanation.note}\n`);
}

function writeJsConfuserCompatibilityExplanation(option, json) {
  const explanation = explainJsConfuserCompatibilityOption(option);
  if (json) {
    process.stdout.write(`${JSON.stringify(explanation, null, 2)}\n`);
    return;
  }
  process.stdout.write(`${explanation.option}: ${explanation.status}\n`);
  if (explanation.target.length) process.stdout.write(`Targets: ${explanation.target.join(", ")}\n`);
  process.stdout.write(`${explanation.note}\n`);
}

function buildMigrationMapSummary(mappings, review) {
  const direct = mappings.filter((entry) => entry.confidence === "direct").length;
  const approximate = mappings.filter((entry) => entry.confidence === "approximate").length;
  return {
    mapped: mappings.length,
    direct,
    approximate,
    reviewOnly: review.length,
    totalKnown: mappings.length + review.length
  };
}

function writeMigrationMap(json) {
  const map = listJavascriptObfuscatorMigrationMap();
  if (json) {
    process.stdout.write(`${JSON.stringify(map, null, 2)}\n`);
    return;
  }

  process.stdout.write("javascript-obfuscator migration map:\n");
  process.stdout.write(`Mapped: ${map.summary.mapped} (${map.summary.direct} direct, ${map.summary.approximate} approximate). Review-only: ${map.summary.reviewOnly}. Total known: ${map.summary.totalKnown}.\n`);
  for (const item of map.mappings) {
    process.stdout.write(`- ${item.source} -> ${item.target.join(", ")} (${item.confidence}) - ${item.note}\n`);
  }
  process.stdout.write("Manual review options:\n");
  for (const item of map.review) {
    process.stdout.write(`- ${item.option}: ${item.note}\n`);
  }
}

function writeJsConfuserMigrationMap(json) {
  const map = listJsConfuserMigrationMap();
  if (json) {
    process.stdout.write(`${JSON.stringify(map, null, 2)}\n`);
    return;
  }

  process.stdout.write("JS-Confuser migration map:\n");
  process.stdout.write(`Mapped: ${map.summary.mapped} (${map.summary.direct} direct, ${map.summary.approximate} approximate). Review-only: ${map.summary.reviewOnly}. Total known: ${map.summary.totalKnown}.\n`);
  for (const item of map.mappings) {
    process.stdout.write(`- ${item.source} -> ${item.target.join(", ")} (${item.confidence}) - ${item.note}\n`);
  }
  process.stdout.write("Manual review options:\n");
  for (const item of map.review) {
    process.stdout.write(`- ${item.option}: ${item.note}\n`);
  }
}

function mapBooleanOption(source, sourceKey, target, targetKey, mapped) {
  if (source[sourceKey] === true) {
    target[targetKey] = true;
    mapped.push({ from: sourceKey, to: targetKey });
  }
}

function writeMigrationReport(report, args = {}) {
  if (args.json) {
    process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
    return;
  }

  const targetPath = args.output ? path.resolve(args.output) : null;
  if (targetPath) {
    if (fs.existsSync(targetPath)) {
      throw new Error(`Output config already exists: ${targetPath}`);
    }
    fs.mkdirSync(path.dirname(targetPath), { recursive: true });
    fs.writeFileSync(targetPath, `${JSON.stringify(report.config, null, 2)}\n`, "utf8");
    process.stdout.write(`Created ${targetPath}\n`);
  } else {
    process.stdout.write(`${JSON.stringify(report.config, null, 2)}\n`);
  }

  if (report.mapped.length) {
    process.stderr.write(`Mapped ${report.mapped.length} option(s).\n`);
  }
  process.stderr.write(`Migration summary: ${report.summary.mappedOptions}/${report.summary.sourceOptions} source option(s) mapped (${report.summary.direct} direct, ${report.summary.approximate} approximate), ${report.summary.reviewOnly} review-only, ${report.summary.unmapped} unmapped.\n`);
  for (const item of report.review) {
    process.stderr.write(`Review ${item.option}: ${item.note}\n`);
  }
  if (report.unmapped.length) {
    process.stderr.write(`Unmapped option(s): ${report.unmapped.join(", ")}\n`);
  }
  for (const note of report.notes) {
    process.stderr.write(`${note}\n`);
  }
  if (report.nextCommands && report.nextCommands.length) {
    process.stderr.write("Next commands:\n");
    for (const entry of report.nextCommands) {
      process.stderr.write(`- ${entry.label}: ${entry.command}\n`);
    }
  }
}

function collectFiles(inputPath, outputPath, extensions, excludePatterns, includePatterns = [], markupExtensions = DEFAULT_MARKUP_EXTENSIONS) {
  if (!fs.existsSync(inputPath)) {
    throw new Error(`Input path does not exist: ${inputPath}`);
  }

  const inputStat = fs.statSync(inputPath);
  const files = [];
  const outputRelative = inputStat.isDirectory() ? getNestedOutputRelative(inputPath, outputPath) : null;

  if (inputStat.isFile()) {
    const ext = path.extname(inputPath).toLowerCase();
    if (!extensions.includes(ext)) return files;
    if (isMarkupFilePath(inputPath, markupExtensions) && !hasHtmlProtectionMarkers(inputPath)) return files;
    if (!isIncluded(path.basename(inputPath), includePatterns)) return files;
    files.push({
      source: inputPath,
      relative: path.basename(inputPath),
      target: fs.existsSync(outputPath) && fs.statSync(outputPath).isDirectory()
        ? path.join(outputPath, path.basename(inputPath))
        : outputPath
    });
    return files;
  }

  walk(inputPath, (filePath) => {
    const relative = path.relative(inputPath, filePath).replace(/\\/g, "/");
    const ext = path.extname(filePath).toLowerCase();
    if (!extensions.includes(ext)) return;
    if (isMarkupFilePath(filePath, markupExtensions) && !hasHtmlProtectionMarkers(filePath)) return;
    if (!isIncluded(relative, includePatterns)) return;
    if (isInsideRelativePath(relative, outputRelative)) return;
    if (isExcluded(relative, excludePatterns)) return;
    files.push({
      source: filePath,
      relative,
      target: path.join(outputPath, relative)
    });
  });

  return files;
}

function collectAssets(inputPath, outputPath, protectedFiles, assetExcludePatterns) {
  const assets = [];
  if (!fs.existsSync(inputPath) || fs.statSync(inputPath).isFile()) {
    return assets;
  }

  const protectedSources = new Set(protectedFiles.map((file) => path.resolve(file.source)));
  const outputRelative = getNestedOutputRelative(inputPath, outputPath);

  walk(inputPath, (filePath) => {
    if (protectedSources.has(path.resolve(filePath))) return;

    const relative = path.relative(inputPath, filePath).replace(/\\/g, "/");
    if (isInsideRelativePath(relative, outputRelative)) return;
    if (isExcluded(relative, assetExcludePatterns)) return;

    assets.push({
      source: filePath,
      relative,
      target: path.join(outputPath, relative)
    });
  });

  return assets;
}

function getNestedOutputRelative(inputPath, outputPath) {
  const relative = path.relative(path.resolve(inputPath), path.resolve(outputPath)).replace(/\\/g, "/");
  if (!relative || relative === "." || relative.startsWith("../") || path.isAbsolute(relative)) return null;
  return relative;
}

function isInsideRelativePath(relativePath, parentRelativePath) {
  if (!parentRelativePath) return false;
  return relativePath === parentRelativePath || relativePath.startsWith(`${parentRelativePath}/`);
}

function walk(dir, visit) {
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const entryPath = path.join(dir, entry.name);
    if (entry.isDirectory()) walk(entryPath, visit);
    else if (entry.isFile()) visit(entryPath);
  }
}

function isExcluded(relativePath, patterns) {
  const normalized = relativePath.replace(/\\/g, "/");
  return patterns.some((pattern) => globLikeMatch(normalized, pattern));
}

function isIncluded(relativePath, patterns) {
  if (!patterns || !patterns.length) return true;
  const normalized = relativePath.replace(/\\/g, "/");
  return patterns.some((pattern) => globLikeMatch(normalized, pattern));
}

function globLikeMatch(value, pattern) {
  const normalizedPattern = pattern.replace(/\\/g, "/");
  const escaped = normalizedPattern
    .replace(/\\/g, "/")
    .replace(/[.+^${}()|[\]\\]/g, "\\$&")
    .replace(/\*\*/g, "\u0000")
    .replace(/\*/g, "[^/]*")
    .replace(/\u0000/g, ".*");
  if (new RegExp(`^${escaped}$`).test(value)) return true;
  if (normalizedPattern.startsWith("**/")) {
    const withoutPrefix = escaped.replace(/^\.\*\//, "");
    return new RegExp(`(^|/)${withoutPrefix}$`).test(value);
  }
  return false;
}

function buildRequest(config, files) {
  return buildRequestFromItems(config, buildProtectionItems(config, files).items);
}

function buildProtectionItems(config, files) {
  const items = [];
  const transforms = new Map();

  for (const file of files) {
    const code = fs.readFileSync(file.source, "utf8");
    const plan = buildFileProtectionPlan(config, file.relative, code);
    items.push(...plan.items);
    if (plan.transform) {
      transforms.set(normalizeName(file.relative), plan.transform);
    }
  }

  return { items, transforms };
}

function buildProtectionItemsFromInputItems(config, inputItems) {
  const items = [];
  const transforms = new Map();

  for (const input of inputItems || []) {
    const fileName = input.FileName;
    const code = String(input.FileCode || "");
    const plan = buildFileProtectionPlan(config, fileName, code);
    items.push(...plan.items);
    if (plan.transform) {
      transforms.set(normalizeName(fileName), plan.transform);
    }
  }

  return { items, transforms };
}

function describeProtectionTransforms(protection) {
  const items = protection && Array.isArray(protection.items) ? protection.items : [];
  const transforms = protection && protection.transforms instanceof Map ? protection.transforms : new Map();
  return {
    apiItems: items.length,
    transformedFiles: Array.from(transforms.entries()).map(([fileName, transform]) => {
      const pieces = Array.isArray(transform && transform.pieces) ? transform.pieces : [];
      return {
        fileName,
        type: transform && transform.type ? transform.type : "unknown",
        apiItems: pieces.filter((piece) => piece.type === "item").length,
        preservedParts: pieces.filter((piece) => piece.type === "raw" && piece.code).length
      };
    })
  };
}

function addProtectionSummary(summary, protection) {
  return {
    ...summary,
    processing: describeProtectionTransforms(protection)
  };
}

function buildFileProtectionPlan(config, fileName, code) {
  if (isMarkupFile(fileName, config.markupExtensions)) {
    if (!config.parseHtml) {
      throw new Error(`${fileName} is a markup/template file. Pass --parse-html and mark inline scripts with data-javascript-obfuscator, or remove the markup extension from protected extensions.`);
    }
    return buildHtmlProtectionPlan(config, fileName, code);
  }
  return buildCodeProtectionPlan(config, fileName, code);
}

function buildCodeProtectionPlan(config, fileName, code) {
  const preservedRanges = collectPreservedCodeRanges(config, fileName, code);
  if (!preservedRanges.length) {
    return {
      items: [{ FileName: fileName, FileCode: code }],
      transform: null
    };
  }

  const items = [];
  const pieces = [];
  let cursor = 0;

  for (const range of preservedRanges) {
    if (range.start > cursor) {
      const segment = code.slice(cursor, range.start);
      if (segment.trim()) {
        const itemName = makePartFileName(fileName, items.length + 1);
        items.push({ FileName: itemName, FileCode: segment });
        pieces.push({ type: "item", itemName });
      } else {
        pieces.push({ type: "raw", code: segment });
      }
    }
    pieces.push({ type: "raw", code: code.slice(range.start, range.end) });
    cursor = range.end;
  }

  if (cursor < code.length) {
    const segment = code.slice(cursor);
    if (segment.trim()) {
      const itemName = makePartFileName(fileName, items.length + 1);
      items.push({ FileName: itemName, FileCode: segment });
      pieces.push({ type: "item", itemName });
    } else {
      pieces.push({ type: "raw", code: segment });
    }
  }

  return {
    items,
    transform: { type: "parts", fileName, pieces }
  };
}

function collectPreservedCodeRanges(config, fileName, code) {
  const ranges = [];

  if (hasConditionalMarkers(code)) {
    if (!config.honorConditionalComments) {
      const markerIndex = findFirstConditionalMarkerIndex(code);
      const location = markerIndex >= 0 ? `:${formatSourceLocation(code, markerIndex)}` : "";
      throw new Error(`${fileName}${location} contains javascript-obfuscator conditional comments. Pass --honor-conditional-comments to preserve disabled regions, or remove the markers.`);
    }

    validateConditionalMarkers(fileName, code);
    ranges.push(...getDisabledConditionalRanges(code));
  }

  if (config.ignoreImports) {
    ranges.push(...findIgnoredImportRanges(code));
  }

  return mergeRanges(ranges);
}

function getDisabledConditionalRanges(code) {
  return splitConditionalCodeWithOffsets(code)
    .filter((piece) => !piece.enabled && piece.end > piece.start)
    .map((piece) => ({ start: piece.start, end: piece.end }));
}

function buildHtmlProtectionPlan(config, fileName, html) {
  validateMarkedHtmlScripts(fileName, html);
  const blocks = findMarkedHtmlScripts(html);
  if (!blocks.length) {
    return {
      items: [],
      transform: {
        type: "static",
        fileName,
        code: html
      }
    };
  }

  const items = [];
  const pieces = [];
  let cursor = 0;

  blocks.forEach((block, index) => {
    pieces.push({ type: "raw", code: html.slice(cursor, block.contentStart) });
    const scriptPlan = buildCodeProtectionPlan(config, `${fileName}.script${index + 1}.js`, block.code);
    for (const item of scriptPlan.items) items.push(item);
    if (scriptPlan.transform) {
      pieces.push(...scriptPlan.transform.pieces);
    } else if (scriptPlan.items[0]) {
      pieces.push({ type: "item", itemName: scriptPlan.items[0].FileName });
    }
    cursor = block.contentEnd;
  });
  pieces.push({ type: "raw", code: html.slice(cursor) });

  return {
    items,
    transform: { type: "parts", fileName, pieces }
  };
}

function composeProtectionOutput(transform, byName) {
  if (!transform) return null;
  if (transform.type === "static") return transform.code;
  return (transform.pieces || []).map((piece) => {
    if (piece.type === "raw") return piece.code;
    const item = byName.get(normalizeName(piece.itemName));
    if (!item) {
      throw new Error(`API response did not include output for ${piece.itemName}`);
    }
    return item.FileCode || "";
  }).join("");
}

function composeProtectionItemOutput(fileName, result, transforms = new Map()) {
  const byName = new Map((result.Items || []).map((item) => [normalizeName(item.FileName), item]));
  const transform = transforms instanceof Map ? transforms.get(normalizeName(fileName)) : null;
  if (transform) return composeProtectionOutput(transform, byName);
  const item = byName.get(normalizeName(fileName));
  if (!item) {
    throw new Error(`API response did not include output for ${fileName}`);
  }
  return item.FileCode || "";
}

function makePartFileName(fileName, index) {
  const normalized = normalizeName(fileName);
  const ext = path.posix.extname(normalized) || ".js";
  const base = normalized.slice(0, normalized.length - ext.length).replace(/[^A-Za-z0-9_./-]/g, "_");
  return `${base}.jso-part-${index}${DEFAULT_MARKUP_EXTENSIONS.includes(ext) ? ".js" : ext}`;
}

function hasConditionalMarkers(code) {
  CONDITIONAL_MARKER_PATTERN.lastIndex = 0;
  return CONDITIONAL_MARKER_PATTERN.test(code);
}

function findFirstConditionalMarkerIndex(code) {
  CONDITIONAL_MARKER_PATTERN.lastIndex = 0;
  const match = CONDITIONAL_MARKER_PATTERN.exec(code);
  return match ? match.index : -1;
}

function validateConditionalMarkers(fileName, code) {
  let disabled = false;
  let disabledAt = -1;
  CONDITIONAL_MARKER_PATTERN.lastIndex = 0;
  let match;
  while ((match = CONDITIONAL_MARKER_PATTERN.exec(code)) !== null) {
    const action = match[1] || match[2];
    if (action === "disable") {
      if (disabled) {
        throw new Error(`${fileName}:${formatSourceLocation(code, match.index)} has nested javascript-obfuscator:disable markers. Close the disabled region with javascript-obfuscator:enable before starting another disabled region.`);
      }
      disabled = true;
      disabledAt = match.index;
    } else if (!disabled) {
      throw new Error(`${fileName}:${formatSourceLocation(code, match.index)} has javascript-obfuscator:enable without a matching javascript-obfuscator:disable marker.`);
    } else {
      disabled = false;
      disabledAt = -1;
    }
  }
  if (disabled) {
    throw new Error(`${fileName}:${formatSourceLocation(code, disabledAt)} has javascript-obfuscator:disable without a matching javascript-obfuscator:enable marker.`);
  }
}

function splitConditionalCode(code) {
  return splitConditionalCodeWithOffsets(code).map((piece) => ({
    enabled: piece.enabled,
    code: piece.code
  }));
}

function splitConditionalCodeWithOffsets(code) {
  const pieces = [];
  let enabled = true;
  let cursor = 0;
  CONDITIONAL_MARKER_PATTERN.lastIndex = 0;
  let match;
  while ((match = CONDITIONAL_MARKER_PATTERN.exec(code)) !== null) {
    if (match.index > cursor) {
      pieces.push({ enabled, code: code.slice(cursor, match.index), start: cursor, end: match.index });
    }
    const marker = match[0];
    const action = match[1] || match[2];
    pieces.push({ enabled: false, code: marker, start: match.index, end: match.index + marker.length });
    enabled = action === "enable";
    cursor = match.index + marker.length;
  }
  if (cursor < code.length) {
    pieces.push({ enabled, code: code.slice(cursor), start: cursor, end: code.length });
  }
  return pieces;
}

function findIgnoredImportRanges(code) {
  const ranges = [];
  ranges.push(...findModuleImportStatementRanges(code));
  ranges.push(...findStaticCallStatementRanges(code, "require"));
  ranges.push(...findStaticCallStatementRanges(code, "import"));
  return mergeRanges(ranges);
}

function findModuleImportStatementRanges(code) {
  const ranges = [];
  let cursor = 0;

  while (cursor < code.length) {
    const lineStart = cursor;
    const statementStart = skipHorizontalWhitespace(code, lineStart);
    if (
      startsWithWord(code, statementStart, "import") ||
      startsWithWord(code, statementStart, "export")
    ) {
      const statementEnd = findStatementEnd(code, statementStart);
      if (statementEnd > statementStart) {
        const statement = code.slice(statementStart, statementEnd);
        if (/^\s*import\b/.test(statement) || /^\s*export\b[\s\S]*\bfrom\b/.test(statement)) {
          ranges.push({ start: lineStart, end: statementEnd });
          cursor = statementEnd;
          continue;
        }
      }
    }
    cursor = advanceToNextLine(code, lineStart);
  }

  return ranges;
}

function findStaticCallStatementRanges(code, calleeName) {
  const ranges = [];
  let searchIndex = 0;

  while (searchIndex < code.length) {
    const callIndex = code.indexOf(`${calleeName}(`, searchIndex);
    if (callIndex === -1) break;
    if (isInsideStringOrComment(code, callIndex) || !startsWithWord(code, callIndex, calleeName)) {
      searchIndex = callIndex + calleeName.length;
      continue;
    }

    const openParenIndex = skipHorizontalWhitespace(code, callIndex + calleeName.length);
    if (code[openParenIndex] !== "(") {
      searchIndex = callIndex + calleeName.length;
      continue;
    }

    const firstArgIndex = skipWhitespace(code, openParenIndex + 1);
    const quote = code[firstArgIndex];
    if (quote !== "'" && quote !== "\"") {
      searchIndex = callIndex + calleeName.length;
      continue;
    }

    const statementStart = findStatementStart(code, callIndex);
    const statementEnd = findStatementEnd(code, callIndex);
    if (statementEnd > statementStart) {
      ranges.push({ start: statementStart, end: statementEnd });
    }
    searchIndex = statementEnd > callIndex ? statementEnd : callIndex + calleeName.length;
  }

  return ranges;
}

function mergeRanges(ranges) {
  if (!ranges.length) return [];
  const sorted = ranges
    .filter((range) => range && Number.isInteger(range.start) && Number.isInteger(range.end) && range.end > range.start)
    .sort((a, b) => a.start - b.start || a.end - b.end);
  if (!sorted.length) return [];

  const merged = [sorted[0]];
  for (let i = 1; i < sorted.length; i += 1) {
    const current = sorted[i];
    const last = merged[merged.length - 1];
    if (current.start <= last.end) {
      last.end = Math.max(last.end, current.end);
    } else {
      merged.push({ ...current });
    }
  }
  return merged;
}

function startsWithWord(code, index, word) {
  const text = String(code || "");
  if (text.slice(index, index + word.length) !== word) return false;
  const before = text[index - 1];
  const after = text[index + word.length];
  return !isIdentifierChar(before) && !isIdentifierChar(after);
}

function isIdentifierChar(value) {
  return typeof value === "string" && /[A-Za-z0-9_$]/.test(value);
}

function skipHorizontalWhitespace(code, index) {
  let cursor = index;
  while (cursor < code.length && (code[cursor] === " " || code[cursor] === "\t")) cursor += 1;
  return cursor;
}

function skipWhitespace(code, index) {
  let cursor = index;
  while (cursor < code.length && /\s/.test(code[cursor])) cursor += 1;
  return cursor;
}

function advanceToNextLine(code, index) {
  const nextNewline = code.indexOf("\n", index);
  return nextNewline === -1 ? code.length : nextNewline + 1;
}

function findStatementStart(code, index) {
  let cursor = index;
  while (cursor > 0) {
    const previous = code[cursor - 1];
    if (previous === "\n" || previous === "\r" || previous === ";" || previous === "{") break;
    cursor -= 1;
  }
  return cursor;
}

function findStatementEnd(code, index) {
  let cursor = index;
  let depth = 0;
  let stringQuote = null;
  let escaped = false;
  let lineComment = false;
  let blockComment = false;

  while (cursor < code.length) {
    const char = code[cursor];
    const next = code[cursor + 1];

    if (lineComment) {
      if (char === "\n") lineComment = false;
      cursor += 1;
      continue;
    }

    if (blockComment) {
      if (char === "*" && next === "/") {
        blockComment = false;
        cursor += 2;
        continue;
      }
      cursor += 1;
      continue;
    }

    if (stringQuote) {
      if (escaped) {
        escaped = false;
      } else if (char === "\\") {
        escaped = true;
      } else if (char === stringQuote) {
        stringQuote = null;
      }
      cursor += 1;
      continue;
    }

    if (char === "/" && next === "/") {
      lineComment = true;
      cursor += 2;
      continue;
    }
    if (char === "/" && next === "*") {
      blockComment = true;
      cursor += 2;
      continue;
    }
    if (char === "'" || char === "\"" || char === "`") {
      stringQuote = char;
      cursor += 1;
      continue;
    }
    if (char === "(" || char === "[" || char === "{") {
      depth += 1;
      cursor += 1;
      continue;
    }
    if ((char === ")" || char === "]" || char === "}") && depth > 0) {
      depth -= 1;
      cursor += 1;
      continue;
    }
    if (char === ";" && depth === 0) {
      return cursor + 1;
    }
    if ((char === "\n" || char === "\r") && depth === 0) {
      return cursor;
    }
    cursor += 1;
  }

  return code.length;
}

function isInsideStringOrComment(code, index) {
  let cursor = 0;
  let stringQuote = null;
  let escaped = false;
  let lineComment = false;
  let blockComment = false;

  while (cursor < index) {
    const char = code[cursor];
    const next = code[cursor + 1];

    if (lineComment) {
      if (char === "\n") lineComment = false;
      cursor += 1;
      continue;
    }

    if (blockComment) {
      if (char === "*" && next === "/") {
        blockComment = false;
        cursor += 2;
        continue;
      }
      cursor += 1;
      continue;
    }

    if (stringQuote) {
      if (escaped) {
        escaped = false;
      } else if (char === "\\") {
        escaped = true;
      } else if (char === stringQuote) {
        stringQuote = null;
      }
      cursor += 1;
      continue;
    }

    if (char === "/" && next === "/") {
      lineComment = true;
      cursor += 2;
      continue;
    }
    if (char === "/" && next === "*") {
      blockComment = true;
      cursor += 2;
      continue;
    }
    if (char === "'" || char === "\"" || char === "`") {
      stringQuote = char;
    }
    cursor += 1;
  }

  return !!(stringQuote || lineComment || blockComment);
}

function isMarkupFile(fileName, markupExtensions = DEFAULT_MARKUP_EXTENSIONS) {
  const effectiveExtensions = Array.isArray(markupExtensions) ? markupExtensions : DEFAULT_MARKUP_EXTENSIONS;
  return effectiveExtensions.includes(path.extname(fileName).toLowerCase());
}

function isMarkupFilePath(filePath, markupExtensions = DEFAULT_MARKUP_EXTENSIONS) {
  return isMarkupFile(filePath, markupExtensions);
}

function findMarkedHtmlScripts(html) {
  const blocks = [];
  HTML_SCRIPT_PATTERN.lastIndex = 0;
  let match;
  while ((match = HTML_SCRIPT_PATTERN.exec(html)) !== null) {
    const attrs = match[1] || "";
    if (!/\sdata-javascript-obfuscator(?:\s|=|$)/i.test(attrs)) continue;
    if (/\ssrc\s*=/i.test(attrs)) continue;
    if (/\stype\s*=\s*["']?module["']?/i.test(attrs)) continue;
    const full = match[0];
    const openTagEnd = match.index + full.indexOf(">") + 1;
    const contentEnd = openTagEnd + match[2].length;
    blocks.push({
      contentStart: openTagEnd,
      contentEnd,
      code: match[2]
    });
  }
  return blocks;
}

function validateMarkedHtmlScripts(fileName, html) {
  HTML_SCRIPT_PATTERN.lastIndex = 0;
  let match;
  while ((match = HTML_SCRIPT_PATTERN.exec(html)) !== null) {
    const attrs = match[1] || "";
    if (!/\sdata-javascript-obfuscator(?:\s|=|$)/i.test(attrs)) continue;
    if (/\ssrc\s*=/i.test(attrs)) {
      throw new Error(`${fileName}:${formatSourceLocation(html, match.index)} marks an external script with data-javascript-obfuscator. Only inline script contents can be protected; protect the referenced file directly or remove the marker.`);
    }
    if (/\stype\s*=\s*["']?module["']?/i.test(attrs)) {
      throw new Error(`${fileName}:${formatSourceLocation(html, match.index)} marks a module script with data-javascript-obfuscator. Module scripts are not protected in HTML mode; build them as JavaScript files or remove the marker.`);
    }
  }
}

function formatSourceLocation(source, index) {
  const safeIndex = Math.max(0, Math.min(Number(index) || 0, String(source || "").length));
  const before = String(source || "").slice(0, safeIndex).split(/\r\n|\r|\n/);
  return `${before.length}:${before[before.length - 1].length + 1}`;
}

function hasMarkedHtmlScriptAttributes(html) {
  HTML_SCRIPT_PATTERN.lastIndex = 0;
  let match;
  while ((match = HTML_SCRIPT_PATTERN.exec(html)) !== null) {
    if (/\sdata-javascript-obfuscator(?:\s|=|$)/i.test(match[1] || "")) return true;
  }
  return false;
}

function hasHtmlProtectionMarkers(filePath) {
  return hasMarkedHtmlScriptAttributes(fs.readFileSync(filePath, "utf8"));
}

function buildRequestFromItems(config, items) {
  const project = {
    APIKey: config.apiKey,
    APIPwd: config.apiPassword,
    Name: config.projectName,
    MixedServer: config.mixedServer,
    Items: items
  };

  for (const [key, value] of Object.entries(config.options)) {
    if (value !== undefined && value !== null && value !== false) {
      project[key] = value;
    }
  }

  return project;
}

async function protectItems(config, items) {
  assertReady(config);
  if (!items.length) return { Type: "Succeed", Items: [] };

  const result = await postJson(config.endpoint, buildRequestFromItems(config, items));
  if (result.Type !== "Succeed") {
    throw new Error(formatApiFailure({
      type: result.Type,
      fileName: result.FileName,
      message: result.Message || result.ErrorCode || "API request failed"
    }, [config.apiKey, config.apiPassword]));
  }
  return result;
}

async function protectCode(config, code, fileName = "bundle.js") {
  return (await protectCodeDetailed(config, code, fileName)).code;
}

async function protectCodeDetailed(config, code, fileName = "bundle.js") {
  const plan = buildFileProtectionPlan(config, fileName, code);
  const transforms = new Map();
  if (plan.transform) {
    transforms.set(normalizeName(fileName), plan.transform);
  }
  const protection = { items: plan.items, transforms };
  const result = await protectItems(config, plan.items);
  let output = null;
  if (plan.transform) {
    const byName = new Map((result.Items || []).map((entry) => [normalizeName(entry.FileName), entry]));
    output = composeProtectionOutput(plan.transform, byName);
  } else {
    const item = (result.Items || []).find((entry) => normalizeName(entry.FileName) === normalizeName(fileName));
    if (item) output = item.FileCode || "";
  }
  if (output === null) {
    throw new Error(`API response did not include output for ${fileName}`);
  }
  return {
    code: output,
    result,
    protection,
    processing: describeProtectionTransforms(protection)
  };
}

function readStdin(stream = process.stdin) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    stream.setEncoding("utf8");
    stream.on("data", (chunk) => chunks.push(chunk));
    stream.on("end", () => resolve(chunks.join("")));
    stream.on("error", reject);
  });
}

async function postJson(endpoint, payload) {
  const response = await fetch(endpoint, {
    method: "POST",
    headers: { "Content-Type": "text/json" },
    body: JSON.stringify(payload)
  });

  const text = await response.text();
  let json;
  try {
    json = JSON.parse(text);
  } catch (error) {
    throw new Error(`API returned non-JSON response (${response.status}): ${text.slice(0, 300)}`);
  }

  if (!response.ok) {
    throw new Error(formatApiFailure({
      status: response.status,
      statusText: response.statusText,
      message: json.Message || json.Error || json.ErrorCode || response.statusText
    }, [payload && payload.APIKey, payload && payload.APIPwd]));
  }

  return json;
}

function formatApiFailure(details = {}, secrets = []) {
  const status = details.status ? ` (${details.status})` : "";
  const type = details.type || "API request failed";
  const location = details.fileName ? ` in ${details.fileName}` : "";
  const rawMessage = details.message || details.statusText || "API request failed";
  const message = redactApiSecrets(rawMessage, secrets);
  const hint = apiFailureHint({ ...details, message });
  return `${type}${status}${location}: ${message}${hint ? ` ${hint}` : ""}`;
}

function apiFailureHint(details = {}) {
  const text = `${details.status || ""} ${details.type || ""} ${details.message || ""}`.toLowerCase();
  if (/payment|paid|plan|subscription|credit|quota|limit|expired|billing|over[- ]?limit/.test(text)) {
    return "Paid API access is enforced by the hosted API; check account status, plan limits, credits, or subscription state.";
  }
  if (details.status === 401 || details.status === 403 || /auth|credential|api ?key|password|unauthori[sz]ed|forbidden|invalid key|invalid password/.test(text)) {
    return "Check JSO_API_KEY and JSO_API_PASSWORD from the paid dashboard.";
  }
  return "";
}

function redactApiSecrets(value, secrets = []) {
  let text = String(value || "");
  for (const secret of secrets) {
    const safeSecret = String(secret || "");
    if (safeSecret.length < 4) continue;
    text = text.split(safeSecret).join("[redacted]");
  }
  return text;
}

function writeResults(files, result, transforms = new Map()) {
  const items = result.Items || [];
  const byName = new Map(items.map((item) => [normalizeName(item.FileName), item]));

  for (const file of files) {
    const transform = transforms instanceof Map ? transforms.get(normalizeName(file.relative)) : null;
    const item = byName.get(normalizeName(file.relative));
    const output = transform ? composeProtectionOutput(transform, byName) : (item && item.FileCode);
    if (!transform && !item) {
      throw new Error(`API response did not include output for ${file.relative}`);
    }
    fs.mkdirSync(path.dirname(file.target), { recursive: true });
    fs.writeFileSync(file.target, output || "", "utf8");
  }
}

function copyAssets(assets) {
  for (const asset of assets) {
    fs.mkdirSync(path.dirname(asset.target), { recursive: true });
    fs.copyFileSync(asset.source, asset.target);
  }
}

function normalizeName(name) {
  return String(name || "").replace(/\\/g, "/");
}

function assertReady(config) {
  if (!config.apiKey) throw new Error("Missing API key. Set JSO_API_KEY, JAVASCRIPT_OBFUSCATOR_API_KEY, or apiKey in config.");
  if (!config.apiPassword) throw new Error("Missing API password. Set JSO_API_PASSWORD, JAVASCRIPT_OBFUSCATOR_API_PASSWORD, or apiPassword in config.");
}

function sha256(value) {
  return crypto.createHash("sha256").update(value).digest("hex");
}

function fileDigest(filePath) {
  const buffer = fs.readFileSync(filePath);
  return {
    bytes: buffer.length,
    sha256: sha256(buffer)
  };
}

function buildProtectionManifest(config, files, assets, result, transforms = new Map()) {
  const byName = new Map((result.Items || []).map((item) => [normalizeName(item.FileName), item]));
  const processing = describeProtectionTransforms({
    items: result.Items || [],
    transforms: transforms instanceof Map ? transforms : new Map()
  });
  return {
    format: "jso-protector-manifest",
    version: 1,
    generatedAt: new Date().toISOString(),
    endpoint: config.endpoint,
    projectName: config.projectName,
    preset: config.preset,
    options: Object.keys(config.options || {}).filter((key) => key !== "reservedNames").sort(),
    processing,
    files: files.map((file) => {
      const item = byName.get(normalizeName(file.relative));
      const transform = transforms instanceof Map ? transforms.get(normalizeName(file.relative)) : null;
      const source = fileDigest(file.source);
      const fallbackOutput = transform ? composeProtectionOutput(transform, byName) : (item && item.FileCode ? item.FileCode : "");
      const output = fs.existsSync(file.target) ? fileDigest(file.target) : {
        bytes: Buffer.byteLength(fallbackOutput, "utf8"),
        sha256: sha256(fallbackOutput)
      };
      return {
        fileName: file.relative,
        sourcePath: file.source,
        outputPath: file.target,
        sourceBytes: source.bytes,
        outputBytes: output.bytes,
        sourceSha256: source.sha256,
        outputSha256: output.sha256
      };
    }),
    assets: (assets || []).map((asset) => {
      const digest = fs.existsSync(asset.target) ? fileDigest(asset.target) : fileDigest(asset.source);
      return {
        fileName: asset.relative,
        sourcePath: asset.source,
        outputPath: asset.target,
        bytes: digest.bytes,
        sha256: digest.sha256
      };
    })
  };
}

function buildStdinManifest(config, fileName, sourceCode, outputCode, outputPath, processing = null) {
  return {
    format: "jso-protector-manifest",
    version: 1,
    generatedAt: new Date().toISOString(),
    endpoint: config.endpoint,
    projectName: config.projectName,
    preset: config.preset,
    options: Object.keys(config.options || {}).filter((key) => key !== "reservedNames").sort(),
    processing: processing || { apiItems: 1, transformedFiles: [] },
    files: [{
      fileName,
      sourcePath: "stdin",
      outputPath,
      sourceBytes: Buffer.byteLength(sourceCode, "utf8"),
      outputBytes: Buffer.byteLength(outputCode, "utf8"),
      sourceSha256: sha256(sourceCode),
      outputSha256: sha256(outputCode)
    }],
    assets: []
  };
}

function buildItemsManifest(config, inputItems, result, outputPathForItem, transforms = new Map()) {
  const byName = new Map((result.Items || []).map((item) => [normalizeName(item.FileName), item]));
  const processing = describeProtectionTransforms({
    items: result.Items || [],
    transforms: transforms instanceof Map ? transforms : new Map()
  });
  return {
    format: "jso-protector-manifest",
    version: 1,
    generatedAt: new Date().toISOString(),
    endpoint: config.endpoint,
    projectName: config.projectName,
    preset: config.preset,
    options: Object.keys(config.options || {}).filter((key) => key !== "reservedNames").sort(),
    processing,
    files: inputItems.map((input) => {
      const item = byName.get(normalizeName(input.FileName));
      const transform = transforms instanceof Map ? transforms.get(normalizeName(input.FileName)) : null;
      const outputCode = transform ? composeProtectionOutput(transform, byName) : (item && item.FileCode ? item.FileCode : "");
      return {
        fileName: input.FileName,
        sourcePath: input.SourcePath || input.FileName,
        outputPath: typeof outputPathForItem === "function" ? outputPathForItem(input.FileName) : (input.OutputPath || input.FileName),
        sourceBytes: Buffer.byteLength(input.FileCode || "", "utf8"),
        outputBytes: Buffer.byteLength(outputCode, "utf8"),
        sourceSha256: sha256(input.FileCode || ""),
        outputSha256: sha256(outputCode)
      };
    }),
    assets: []
  };
}

function writeManifest(manifestPath, manifest) {
  if (!manifestPath) return;
  fs.mkdirSync(path.dirname(path.resolve(manifestPath)), { recursive: true });
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
}

function checkSizeBudgets(manifest, budgets = {}) {
  const failures = [];
  const maxOutputBytes = budgets.maxOutputBytes;
  const maxGrowthRatio = budgets.maxGrowthRatio;
  if (!maxOutputBytes && !maxGrowthRatio) return failures;

  for (const file of manifest.files || []) {
    if (maxOutputBytes && file.outputBytes > maxOutputBytes) {
      failures.push({
        fileName: file.fileName,
        type: "max-output-bytes",
        actual: file.outputBytes,
        limit: maxOutputBytes
      });
    }
    if (maxGrowthRatio && file.sourceBytes > 0) {
      const ratio = file.outputBytes / file.sourceBytes;
      if (ratio > maxGrowthRatio) {
        failures.push({
          fileName: file.fileName,
          type: "max-growth-ratio",
          actual: Number(ratio.toFixed(4)),
          limit: maxGrowthRatio
        });
      }
    }
  }
  return failures;
}

function assertSizeBudgets(manifest, budgets = {}) {
  const failures = checkSizeBudgets(manifest, budgets);
  if (!failures.length) return failures;
  const details = failures.map((failure) => `${failure.fileName} ${failure.type} ${failure.actual} > ${failure.limit}`).join("; ");
  throw new Error(`Size budget failed: ${details}`);
}

async function runDoctor(config, args = {}) {
  const checks = [];
  addDoctorCheck(checks, "endpoint", isValidUrl(config.endpoint), `Endpoint: ${config.endpoint}`, "Endpoint must be a valid URL.");
  addDoctorCheck(checks, "apiKey", !!config.apiKey, "API key is present.", "Missing API key. Set JSO_API_KEY, JAVASCRIPT_OBFUSCATOR_API_KEY, or apiKey in config.");
  addDoctorCheck(checks, "apiPassword", !!config.apiPassword, "API password is present.", "Missing API password. Set JSO_API_PASSWORD, JAVASCRIPT_OBFUSCATOR_API_PASSWORD, or apiPassword in config.");
  addDoctorCheck(checks, "preset", true, `Preset: ${config.preset}`);
  addDoctorCheck(checks, "options", Object.keys(config.options || {}).length > 0, `${Object.keys(config.options || {}).length} option(s) enabled.`, "No protection options are enabled.");

  const inputExists = !!config.input && fs.existsSync(config.input);
  addDoctorCheck(checks, "input", inputExists, `Input exists: ${config.input}`, `Input path does not exist: ${config.input}`);

  let files = [];
  let assets = [];
  if (inputExists) {
    try {
      files = collectFiles(config.input, config.output, config.extensions, config.exclude, config.include);
      assets = config.copyAssets ? collectAssets(config.input, config.output, files, config.assetExclude) : [];
      addDoctorCheck(checks, "files", files.length > 0, `${files.length} JavaScript file(s) match.`, "No matching JavaScript files found.");
      addDoctorCheck(checks, "assets", true, `${assets.length} asset(s) would be copied.`);
      buildProtectionItems(config, files);
      addDoctorCheck(checks, "transforms", true, "Conditional comments and marked HTML scripts are ready.");
    } catch (error) {
      addDoctorCheck(checks, "files", false, "", error.message);
    }
  }

  const outputParent = path.dirname(config.output);
  addDoctorCheck(
    checks,
    "output",
    !!outputParent && fs.existsSync(outputParent),
    `Output parent exists: ${outputParent}`,
    `Output parent does not exist: ${outputParent}`
  );

  if (args.checkApi) {
    if (!config.apiKey || !config.apiPassword) {
      addDoctorCheck(checks, "api", false, "", "API check skipped because credentials are missing.");
    } else {
      try {
        const code = await protectCode(config, "console.log('jso-doctor');", "jso-doctor.js");
        addDoctorCheck(checks, "api", !!code, `Live API check succeeded (${code.length} bytes returned).`, "Live API check returned empty output.");
      } catch (error) {
        addDoctorCheck(checks, "api", false, "", error.message);
      }
    }
  } else {
    addDoctorCheck(checks, "api", true, "Live API check skipped. Pass --check-api to test credentials and endpoint.");
  }

  return {
    ok: checks.every((check) => check.ok),
    endpoint: config.endpoint,
    projectName: config.projectName,
    preset: config.preset,
    input: config.input,
    output: config.output,
    files: files.map((file) => file.relative),
    assets: assets.map((file) => file.relative),
    checks
  };
}

function addDoctorCheck(checks, name, ok, message, error) {
  checks.push({
    name,
    ok: !!ok,
    message: ok ? message : (error || message || "Check failed.")
  });
}

function isValidUrl(value) {
  try {
    const url = new URL(value);
    return url.protocol === "http:" || url.protocol === "https:";
  } catch (_error) {
    return false;
  }
}

function writeDoctorReport(report, json) {
  if (json) {
    process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
    return;
  }

  process.stdout.write(`jso-protector doctor: ${report.ok ? "ok" : "failed"}\n`);
  for (const check of report.checks) {
    process.stdout.write(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.message}\n`);
  }
}

async function runReleaseCheck(config, args = {}) {
  const validation = validateProtectionConfig(config, args);
  let resolved = null;
  let plan = null;
  let doctor = null;

  try {
    resolved = mergeConfig(config, args);
  } catch (error) {
    plan = { ok: false, error: error.message };
  }

  if (resolved) {
    plan = buildReleasePlan(resolved);
    doctor = await runDoctor(resolved, args);
  }

  return {
    format: "jso-protector-release-check",
    version: 1,
    ok: validation.ok && (!plan || plan.ok) && (!doctor || doctor.ok),
    endpoint: resolved ? resolved.endpoint : null,
    projectName: resolved ? resolved.projectName : null,
    preset: resolved ? resolved.preset : null,
    validation,
    plan,
    doctor,
    checkApi: !!args.checkApi
  };
}

function buildReleasePlan(config) {
  try {
    const files = collectFiles(config.input, config.output, config.extensions, config.exclude, config.include);
    const assets = config.copyAssets ? collectAssets(config.input, config.output, files, config.assetExclude) : [];
    const protection = buildProtectionItems(config, files);
    return addProtectionSummary({
      ok: files.length > 0,
      input: config.input,
      output: config.output,
      files: files.map((file) => file.relative),
      assets: assets.map((file) => file.relative),
      options: Object.keys(config.options || {}).filter((key) => key !== "reservedNames"),
      manifest: config.manifest || null,
      maxOutputBytes: config.maxOutputBytes || null,
      maxGrowthRatio: config.maxGrowthRatio || null,
      error: files.length > 0 ? null : "No matching input files found."
    }, protection);
  } catch (error) {
    return {
      ok: false,
      input: config.input,
      output: config.output,
      files: [],
      assets: [],
      options: Object.keys(config.options || {}).filter((key) => key !== "reservedNames"),
      manifest: config.manifest || null,
      maxOutputBytes: config.maxOutputBytes || null,
      maxGrowthRatio: config.maxGrowthRatio || null,
      processing: { apiItems: 0, transformedFiles: [] },
      error: error.message
    };
  }
}

function writeReleaseCheckReport(report, json) {
  if (json) {
    process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
    return;
  }

  process.stdout.write(`jso-protector release-check: ${report.ok ? "ok" : "failed"}\n`);
  process.stdout.write(`validation: ${report.validation && report.validation.ok ? "ok" : "failed"}${report.validation && report.validation.warnings ? ` (${report.validation.warnings} warning(s))` : ""}\n`);
  if (report.plan) {
    const processing = report.plan.processing || { apiItems: 0, transformedFiles: [] };
    process.stdout.write(`plan: ${report.plan.ok ? "ok" : "failed"} (${(report.plan.files || []).length} file(s), ${(report.plan.assets || []).length} asset(s), ${processing.apiItems || 0} API item(s))\n`);
    if (processing.transformedFiles && processing.transformedFiles.length) {
      for (const entry of processing.transformedFiles) {
        process.stdout.write(`transform ${entry.fileName}: ${entry.type}, ${entry.apiItems} API item(s), ${entry.preservedParts} preserved part(s)\n`);
      }
    }
    if (report.plan.error) process.stdout.write(`plan error: ${report.plan.error}\n`);
  }
  if (report.doctor) {
    process.stdout.write(`doctor: ${report.doctor.ok ? "ok" : "failed"}\n`);
    for (const check of report.doctor.checks) {
      process.stdout.write(`${check.ok ? "OK" : "FAIL"} ${check.name}: ${check.message}\n`);
    }
  }
}

function validateProtectionConfig(config, args = {}) {
  const checks = [];
  let resolved = null;

  try {
    validateRawConfigShape(config);
    resolved = mergeConfig(config, args);
    addValidationCheck(checks, "config", "ok", "Config shape is valid.");
  } catch (error) {
    addValidationCheck(checks, "config", "error", error.message);
    return buildValidationReport(checks, null, args.strict);
  }

  addValidationCheck(checks, "endpoint", isValidUrl(resolved.endpoint) ? "ok" : "error", isValidUrl(resolved.endpoint) ? `Endpoint: ${resolved.endpoint}` : "Endpoint must be a valid HTTP or HTTPS URL.");
  addValidationCheck(checks, "apiKey", resolved.apiKey ? "ok" : "error", resolved.apiKey ? "API key is present." : "Missing API key. Set JSO_API_KEY, JAVASCRIPT_OBFUSCATOR_API_KEY, or apiKey in config.");
  addValidationCheck(checks, "apiPassword", resolved.apiPassword ? "ok" : "error", resolved.apiPassword ? "API password is present." : "Missing API password. Set JSO_API_PASSWORD, JAVASCRIPT_OBFUSCATOR_API_PASSWORD, or apiPassword in config.");
  addCredentialStorageValidation(checks, config, args);
  addValidationCheck(checks, "preset", "ok", `Preset: ${resolved.preset}`);
  addValidationCheck(checks, "paths", resolved.input && resolved.output ? "ok" : "error", resolved.input && resolved.output ? "Input and output paths are configured." : "Input and output paths must be configured.");

  if (resolved.input && !fs.existsSync(resolved.input)) {
    addValidationCheck(checks, "input", "warning", `Input path does not exist yet: ${resolved.input}`);
  } else if (resolved.input) {
    addValidationCheck(checks, "input", "ok", `Input exists: ${resolved.input}`);
  }

  const outputParent = resolved.output ? path.dirname(resolved.output) : "";
  if (outputParent && !fs.existsSync(outputParent)) {
    addValidationCheck(checks, "output", "warning", `Output parent does not exist yet: ${outputParent}`);
  } else if (outputParent) {
    addValidationCheck(checks, "output", "ok", `Output parent exists: ${outputParent}`);
  }

  const unknownOptions = findUnknownOptionNames(config, args);
  if (unknownOptions.length) {
    addValidationCheck(checks, "options", "warning", `Unknown option name(s): ${unknownOptions.join(", ")}. Verify spelling or confirm they are supported by the HTTP API.`);
  } else {
    addValidationCheck(checks, "options", "ok", "Option names match the local reference.");
  }

  const compatibilityReviewFields = getCompatibilityReviewFields(config);
  if (compatibilityReviewFields.length) {
    addValidationCheck(checks, "compatibility", "warning", `Config includes javascript-obfuscator review-only field(s): ${compatibilityReviewFields.join(", ")}. They are accepted for migration compatibility, but require manual review because the hosted API has no one-to-one mapping.`);
  }

  if (resolved.manifest) {
    const manifestParent = path.dirname(resolved.manifest);
    addValidationCheck(checks, "manifest", fs.existsSync(manifestParent) ? "ok" : "warning", fs.existsSync(manifestParent) ? `Manifest parent exists: ${manifestParent}` : `Manifest parent does not exist yet: ${manifestParent}`);
  }

  if (resolved.maxOutputBytes || resolved.maxGrowthRatio) {
    addValidationCheck(checks, "budgets", "ok", "Size budgets are configured.");
  }

  return buildValidationReport(checks, resolved, args.strict);
}

function validateRawConfigShape(config) {
  if (!config || typeof config !== "object" || Array.isArray(config)) {
    throw new Error("Config must be a JSON object.");
  }

  const allowedKeys = new Set([
    "$schema",
    "__configDir",
    "format",
    "version",
    "standardOptions",
    "advancedFeatures",
    "variableExclusionList",
    "endpoint",
    "apiKey",
    "apiPassword",
    "projectName",
    "input",
    "output",
    "preset",
    "compact",
    "controlFlowFlattening",
    "deadCodeInjection",
    "deadCodeInjectionThreshold",
    "domainLock",
    "identifierNamesGenerator",
    "optionsPreset",
    "renameGlobals",
    "renameProperties",
    "stringArray",
    "stringArrayEncoding",
    "stringArrayThreshold",
    "target",
    "unicodeEscapeSequence",
    "webPreset",
    "include",
    "extensions",
    "markupExtensions",
    "exclude",
    "assetExclude",
    "copyAssets",
    "mixedServer",
    "parseHtml",
    "honorConditionalComments",
    "ignoreImports",
    "keepHeaderComment",
    "protectObjectDeclaration",
    "moveNestedFunction",
    "formattedOutput",
    "keepIndent",
    "lineNumbers",
    "lockDomainSubdomains",
    "lockDomainMessage",
    "lockDate",
    "lockDateValue",
    "lockDateMessage",
    "reservedNames",
    "variableExclusion",
    "removeSourceMaps",
    "manifest",
    "maxOutputBytes",
    "maxGrowthRatio",
    "options",
    ...Object.keys(JAVASCRIPT_OBFUSCATOR_REVIEW_OPTIONS)
  ]);

  const unknownKeys = Object.keys(config).filter((key) => !allowedKeys.has(key));
  if (unknownKeys.length) {
    throw new Error(`Unknown config field(s): ${unknownKeys.join(", ")}`);
  }

  validateOptionalString("endpoint", config.endpoint);
  validateOptionalString("apiKey", config.apiKey);
  validateOptionalString("apiPassword", config.apiPassword);
  validateOptionalString("projectName", config.projectName);
  validateOptionalString("input", config.input);
  validateOptionalString("output", config.output);
  validateOptionalString("preset", config.preset);
  validateOptionalBoolean("compact", config.compact);
  validateOptionalBoolean("controlFlowFlattening", config.controlFlowFlattening);
  validateOptionalBoolean("deadCodeInjection", config.deadCodeInjection);
  validateOptionalNonNegativeNumber("deadCodeInjectionThreshold", config.deadCodeInjectionThreshold);
  validateOptionalString("identifierNamesGenerator", config.identifierNamesGenerator);
  validateOptionalString("optionsPreset", config.optionsPreset);
  validateOptionalBoolean("renameGlobals", config.renameGlobals);
  validateOptionalBoolean("renameProperties", config.renameProperties);
  validateOptionalBoolean("stringArray", config.stringArray);
  validateOptionalNonNegativeNumber("stringArrayThreshold", config.stringArrayThreshold);
  validateOptionalString("target", config.target);
  validateOptionalBoolean("unicodeEscapeSequence", config.unicodeEscapeSequence);
  validateOptionalString("webPreset", config.webPreset);
  validateOptionalString("variableExclusion", config.variableExclusion);
  validateOptionalString("manifest", config.manifest);
  validateOptionalBoolean("copyAssets", config.copyAssets);
  validateOptionalBoolean("mixedServer", config.mixedServer);
  validateOptionalBoolean("parseHtml", config.parseHtml);
  validateOptionalBoolean("honorConditionalComments", config.honorConditionalComments);
  validateOptionalBoolean("ignoreImports", config.ignoreImports);
  validateOptionalBoolean("keepHeaderComment", config.keepHeaderComment);
  validateOptionalBoolean("protectObjectDeclaration", config.protectObjectDeclaration);
  validateOptionalBoolean("moveNestedFunction", config.moveNestedFunction);
  validateOptionalBoolean("formattedOutput", config.formattedOutput);
  validateOptionalBoolean("keepIndent", config.keepIndent);
  validateOptionalBoolean("lineNumbers", config.lineNumbers);
  validateOptionalBoolean("lockDomainSubdomains", config.lockDomainSubdomains);
  validateOptionalString("lockDomainMessage", config.lockDomainMessage);
  validateOptionalBoolean("lockDate", config.lockDate);
  validateOptionalString("lockDateValue", config.lockDateValue);
  validateOptionalString("lockDateMessage", config.lockDateMessage);
  validateOptionalBoolean("removeSourceMaps", config.removeSourceMaps);
  validateOptionalPositiveNumber("maxOutputBytes", config.maxOutputBytes);
  validateOptionalPositiveNumber("maxGrowthRatio", config.maxGrowthRatio);
  validateCompatibilityReviewConfigFields(config);

  if (config.include !== undefined) validateStringArray("include", config.include);
  if (config.domainLock !== undefined && typeof config.domainLock !== "string" && !Array.isArray(config.domainLock)) {
    throw new Error("domainLock must be a string or array");
  }
  if (Array.isArray(config.domainLock)) validateStringArray("domainLock", config.domainLock);
  if (config.stringArrayEncoding !== undefined && typeof config.stringArrayEncoding !== "string" && !Array.isArray(config.stringArrayEncoding)) {
    throw new Error("stringArrayEncoding must be a string or array");
  }
  if (Array.isArray(config.stringArrayEncoding)) validateStringArray("stringArrayEncoding", config.stringArrayEncoding);
  if (config.extensions !== undefined) validateStringArray("extensions", config.extensions);
  if (config.markupExtensions !== undefined) validateStringArray("markupExtensions", config.markupExtensions);
  if (config.exclude !== undefined) validateStringArray("exclude", config.exclude);
  if (config.assetExclude !== undefined) validateStringArray("assetExclude", config.assetExclude);
  if (config.reservedNames !== undefined) validateStringArray("reservedNames", config.reservedNames);
  if (config.options !== undefined && (!config.options || typeof config.options !== "object" || Array.isArray(config.options))) {
    throw new Error("options must be an object");
  }
}

function getCompatibilityReviewFields(config = {}) {
  return Object.keys(JAVASCRIPT_OBFUSCATOR_REVIEW_OPTIONS).filter((key) => config[key] !== undefined);
}

function validateCompatibilityReviewConfigFields(config = {}) {
  for (const [key, type] of Object.entries(JAVASCRIPT_OBFUSCATOR_REVIEW_OPTION_TYPES)) {
    const value = config[key];
    if (value === undefined) continue;

    switch (type) {
      case "boolean":
        validateOptionalBoolean(key, value);
        break;
      case "nonNegativeNumber":
        validateOptionalNonNegativeNumber(key, value);
        break;
      case "string":
        validateOptionalString(key, value);
        break;
      case "stringArray":
        validateStringArray(key, value);
        break;
      case "stringOrNumber":
        if (typeof value !== "string" && typeof value !== "number") {
          throw new Error(`${key} must be a string or number`);
        }
        break;
      case "stringOrStringArray":
        if (typeof value !== "string" && !Array.isArray(value)) {
          throw new Error(`${key} must be a string or array`);
        }
        if (Array.isArray(value)) validateStringArray(key, value);
        break;
      case "objectOrNull":
        if (value !== null && (typeof value !== "object" || Array.isArray(value))) {
          throw new Error(`${key} must be an object or null`);
        }
        break;
      default:
        break;
    }
  }
}

function findUnknownOptionNames(config, args = {}) {
  const known = new Set(OPTION_REFERENCE.map((option) => option.name));
  const names = new Set(Object.keys(config.options || {}));
  for (const entry of args.options || []) {
    const index = String(entry).indexOf("=");
    if (index > 0) names.add(String(entry).slice(0, index).trim());
  }
  return Array.from(names).filter((name) => name && !known.has(name) && name !== "reservedNames").sort();
}

function addCredentialStorageValidation(checks, config = {}, args = {}) {
  const inlineConfigFields = [];
  if (isInlineCredentialValue(config.apiKey)) inlineConfigFields.push("apiKey");
  if (isInlineCredentialValue(config.apiPassword)) inlineConfigFields.push("apiPassword");
  if (inlineConfigFields.length) {
    addValidationCheck(checks, "credentialStorage", "warning", `Config contains inline ${inlineConfigFields.join(" and ")} value(s). Prefer $JSO_API_KEY and $JSO_API_PASSWORD environment references so dashboard credentials are not committed.`);
    return;
  }

  const inlineCliFields = [];
  if (isInlineCredentialValue(args.apiKey)) inlineCliFields.push("--api-key");
  if (isInlineCredentialValue(args.apiPassword)) inlineCliFields.push("--api-password");
  if (inlineCliFields.length) {
    addValidationCheck(checks, "credentialStorage", "warning", `Command line contains ${inlineCliFields.join(" and ")} value(s). Prefer environment variables so dashboard credentials do not land in shell history or process logs.`);
    return;
  }

  addValidationCheck(checks, "credentialStorage", "ok", "Credentials are configured through environment references or runtime environment variables.");
}

function isInlineCredentialValue(value) {
  if (typeof value !== "string") return false;
  const trimmed = value.trim();
  return !!trimmed && !trimmed.startsWith("$");
}

function addValidationCheck(checks, name, level, message) {
  checks.push({
    name,
    level,
    ok: level !== "error",
    message
  });
}

function buildValidationReport(checks, config, strict = false) {
  const warnings = checks.filter((check) => check.level === "warning").length;
  return {
    ok: checks.every((check) => check.level !== "error") && !(strict && warnings > 0),
    strict: !!strict,
    warnings,
    endpoint: config ? config.endpoint : null,
    projectName: config ? config.projectName : null,
    preset: config ? config.preset : null,
    input: config ? config.input : null,
    output: config ? config.output : null,
    checks
  };
}

function writeValidationReport(report, json) {
  if (json) {
    process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
    return;
  }

  process.stdout.write(`jso-protector validate-config: ${report.ok ? "ok" : "failed"}${report.warnings ? ` (${report.warnings} warning(s))` : ""}\n`);
  for (const check of report.checks) {
    process.stdout.write(`${check.level.toUpperCase()} ${check.name}: ${check.message}\n`);
  }
}

function getRedactedConfig(config) {
  return {
    endpoint: config.endpoint,
    apiKey: config.apiKey ? "[set]" : "[missing]",
    apiPassword: config.apiPassword ? "[set]" : "[missing]",
    projectName: config.projectName,
    preset: config.preset,
    input: config.input,
    output: config.output,
    include: config.include.slice(),
    extensions: config.extensions.slice(),
    markupExtensions: config.markupExtensions.slice(),
    exclude: config.exclude.slice(),
    assetExclude: config.assetExclude.slice(),
    copyAssets: config.copyAssets,
    mixedServer: config.mixedServer,
    parseHtml: config.parseHtml,
    honorConditionalComments: config.honorConditionalComments,
    ignoreImports: config.ignoreImports,
    manifest: config.manifest,
    maxOutputBytes: config.maxOutputBytes,
    maxGrowthRatio: config.maxGrowthRatio,
    options: { ...config.options }
  };
}

function writeResolvedConfig(config, json) {
  const redacted = getRedactedConfig(config);
  if (json) {
    process.stdout.write(`${JSON.stringify(redacted, null, 2)}\n`);
    return;
  }

  process.stdout.write("Resolved jso-protector config:\n");
  process.stdout.write(`endpoint: ${redacted.endpoint}\n`);
  process.stdout.write(`apiKey: ${redacted.apiKey}\n`);
  process.stdout.write(`apiPassword: ${redacted.apiPassword}\n`);
  process.stdout.write(`projectName: ${redacted.projectName}\n`);
  process.stdout.write(`preset: ${redacted.preset}\n`);
  process.stdout.write(`input: ${redacted.input}\n`);
  process.stdout.write(`output: ${redacted.output}\n`);
  process.stdout.write(`include: ${redacted.include.join(", ")}\n`);
  process.stdout.write(`extensions: ${redacted.extensions.join(", ")}\n`);
  process.stdout.write(`markupExtensions: ${redacted.markupExtensions.join(", ")}\n`);
  process.stdout.write(`exclude: ${redacted.exclude.join(", ")}\n`);
  process.stdout.write(`assetExclude: ${redacted.assetExclude.join(", ")}\n`);
  process.stdout.write(`copyAssets: ${redacted.copyAssets}\n`);
  process.stdout.write(`mixedServer: ${redacted.mixedServer}\n`);
  process.stdout.write(`ignoreImports: ${redacted.ignoreImports}\n`);
  process.stdout.write(`manifest: ${redacted.manifest || ""}\n`);
  process.stdout.write(`maxOutputBytes: ${redacted.maxOutputBytes || ""}\n`);
  process.stdout.write(`maxGrowthRatio: ${redacted.maxGrowthRatio || ""}\n`);
  process.stdout.write(`options: ${Object.keys(redacted.options).join(", ")}\n`);
}

function listPresets() {
  return Object.entries(PRESET_OPTIONS).map(([name, options]) => ({
    name,
    options: { ...options }
  }));
}

function listOptions() {
  return OPTION_REFERENCE.map((option) => ({ ...option }));
}

function writePresetList(json) {
  const presets = listPresets();
  if (json) {
    process.stdout.write(`${JSON.stringify({ presets }, null, 2)}\n`);
    return;
  }

  process.stdout.write("Available presets:\n");
  for (const preset of presets) {
    process.stdout.write(`- ${preset.name}: ${Object.keys(preset.options).join(", ")}\n`);
  }
}

function writeOptionList(json) {
  const options = listOptions();
  if (json) {
    process.stdout.write(`${JSON.stringify({ options }, null, 2)}\n`);
    return;
  }

  process.stdout.write("Common API options:\n");
  for (const option of options) {
    const values = option.values ? ` (${option.values.join("|")})` : "";
    process.stdout.write(`- ${option.name}: ${option.type}${values} - ${option.description}\n`);
  }
}

function getPackageMetadata() {
  return {
    name: packageJson.name,
    version: packageJson.version,
    description: packageJson.description,
    homepage: packageJson.homepage,
    endpoint: DEFAULT_ENDPOINT
  };
}

function writeVersion(json) {
  const metadata = getPackageMetadata();
  if (json) {
    process.stdout.write(`${JSON.stringify(metadata, null, 2)}\n`);
    return;
  }

  process.stdout.write(`${metadata.name} ${metadata.version}\n`);
}

function writeLocalOnlyGuidance(json) {
  const guidance = {
    ok: false,
    sourceLeavesMachine: false,
    npmPublished: false,
    packageDistribution: "local-only",
    message: "This package is intentionally local-only and is not published to npm. Install it from a workspace path, a file: dependency, or an internal npm pack tarball. The npm CLI protects JavaScript through the hosted HTTP API. Use --dry-run, --validate-config, --release-check, and --doctor for local preflight only. If project policy requires source code to remain local during protection, use the JavaScript Obfuscator desktop/local workflow instead of this API-backed npm command.",
    localInstallCommands: [
      "npm install --save-dev ./packages/jso-protector",
      "npm install --save-dev ../packages/jso-protector",
      "npm install --save-dev path/to/jso-protector-0.1.1.tgz"
    ],
    localPackageJsonDependency: {
      devDependencies: {
        "jso-protector": "file:../packages/jso-protector"
      }
    },
    internalTarballCommands: [
      "npm pack --json",
      "npm install --save-dev path/to/jso-protector-0.1.1.tgz"
    ],
    localPreflightCommands: [
      "jso-protector --config jso.config.json --release-check --json",
      "jso-protector --config jso.config.json --dry-run --json",
      "jso-protector --config jso.config.json --print-config --json"
    ]
  };
  if (json) {
    process.stdout.write(`${JSON.stringify(guidance, null, 2)}\n`);
    return;
  }
  process.stdout.write(`${guidance.message}\n`);
  process.stdout.write("Local install commands:\n");
  for (const command of guidance.localInstallCommands) {
    process.stdout.write(`- ${command}\n`);
  }
  process.stdout.write("Local preflight commands:\n");
  for (const command of guidance.localPreflightCommands) {
    process.stdout.write(`- ${command}\n`);
  }
}

function readManifest(manifestPath) {
  const resolvedPath = path.resolve(manifestPath);
  let parsed;
  try {
    parsed = JSON.parse(fs.readFileSync(resolvedPath, "utf8"));
  } catch (error) {
    throw new Error(`Failed to read manifest ${resolvedPath}: ${error.message}`);
  }

  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new Error(`Manifest ${resolvedPath} must be a JSON object.`);
  }
  if (parsed.format !== "jso-protector-manifest") {
    throw new Error(`Manifest ${resolvedPath} is not a jso-protector manifest.`);
  }
  if (!Array.isArray(parsed.files) || !Array.isArray(parsed.assets)) {
    throw new Error(`Manifest ${resolvedPath} must include files and assets arrays.`);
  }

  return {
    manifestPath: resolvedPath,
    manifest: parsed
  };
}

function verifyManifestOutputs(manifestInput, options = {}) {
  const manifestPath = typeof manifestInput === "string" ? path.resolve(manifestInput) : path.resolve(manifestInput.manifestPath);
  const manifest = typeof manifestInput === "string" ? readManifest(manifestPath).manifest : manifestInput.manifest;
  const verifyRoot = options.verifyRoot ? path.resolve(options.verifyRoot) : null;
  const files = Array.isArray(manifest.files) ? manifest.files : [];
  const assets = Array.isArray(manifest.assets) ? manifest.assets : [];

  const verifyEntry = (kind, entry) => {
    const entryPath = verifyRoot ? path.resolve(verifyRoot, normalizeName(entry.fileName)) : path.resolve(entry.outputPath);
    const expectedBytes = kind === "file" ? entry.outputBytes : entry.bytes;
    const expectedSha256 = kind === "file" ? entry.outputSha256 : entry.sha256;

    if (!fs.existsSync(entryPath)) {
      return {
        kind,
        fileName: entry.fileName,
        path: entryPath,
        ok: false,
        reason: "missing",
        expectedBytes,
        expectedSha256,
        actualBytes: null,
        actualSha256: null
      };
    }

    const actual = fileDigest(entryPath);
    const matchesBytes = actual.bytes === expectedBytes;
    const matchesSha256 = actual.sha256 === expectedSha256;
    return {
      kind,
      fileName: entry.fileName,
      path: entryPath,
      ok: matchesBytes && matchesSha256,
      reason: matchesBytes && matchesSha256 ? "ok" : (!matchesBytes ? "size-mismatch" : "sha256-mismatch"),
      expectedBytes,
      expectedSha256,
      actualBytes: actual.bytes,
      actualSha256: actual.sha256
    };
  };

  const fileResults = files.map((entry) => verifyEntry("file", entry));
  const assetResults = assets.map((entry) => verifyEntry("asset", entry));
  const entries = [...fileResults, ...assetResults];
  const summary = {
    total: entries.length,
    ok: entries.filter((entry) => entry.ok).length,
    missing: entries.filter((entry) => entry.reason === "missing").length,
    mismatched: entries.filter((entry) => !entry.ok && entry.reason !== "missing").length
  };

  return {
    format: "jso-protector-manifest-check",
    version: 1,
    ok: summary.missing === 0 && summary.mismatched === 0,
    manifestPath,
    verifyRoot,
    generatedAt: new Date().toISOString(),
    projectName: manifest.projectName || null,
    preset: manifest.preset || null,
    summary,
    files: fileResults,
    assets: assetResults
  };
}

function writeManifestVerificationReport(report, json) {
  if (json) {
    process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
    return;
  }

  process.stdout.write(`jso-protector verify-manifest: ${report.ok ? "ok" : "failed"}\n`);
  process.stdout.write(`manifest: ${report.manifestPath}\n`);
  if (report.verifyRoot) process.stdout.write(`verify root: ${report.verifyRoot}\n`);
  process.stdout.write(`entries: ${report.summary.ok}/${report.summary.total} ok`);
  if (report.summary.missing || report.summary.mismatched) {
    process.stdout.write(` (${report.summary.missing} missing, ${report.summary.mismatched} mismatched)\n`);
  } else {
    process.stdout.write("\n");
  }

  for (const entry of [...report.files, ...report.assets]) {
    const label = entry.kind === "file" ? "file" : "asset";
    if (entry.ok) {
      process.stdout.write(`OK ${label} ${entry.fileName}: ${entry.path}\n`);
      continue;
    }
    process.stdout.write(`FAIL ${label} ${entry.fileName}: ${entry.reason} at ${entry.path}\n`);
  }
}

async function main(argv = process.argv.slice(2)) {
  const args = parseArgs(argv);
  if (args.help) {
    printHelp();
    return;
  }
  if (args.version) {
    writeVersion(args.json);
    return;
  }
  if (args.localOnly) {
    writeLocalOnlyGuidance(args.json);
    return;
  }
  if (args.verifyManifest) {
    const report = verifyManifestOutputs(args.verifyManifest, {
      verifyRoot: args.verifyRoot
    });
    writeManifestVerificationReport(report, args.json);
    if (!report.ok) {
      throw new Error("manifest verification failed");
    }
    return;
  }
  if (args.listPresets) {
    writePresetList(args.json);
    return;
  }
  if (args.listOptions) {
    writeOptionList(args.json);
    return;
  }
  if (args.listMigrationMap) {
    writeMigrationMap(args.json);
    return;
  }
  if (args.listJsConfuserMigrationMap) {
    writeJsConfuserMigrationMap(args.json);
    return;
  }
  if (args.explainCompat) {
    writeCompatibilityExplanation(args.explainCompat, args.json);
    return;
  }
  if (args.explainJsConfuserCompat) {
    writeJsConfuserCompatibilityExplanation(args.explainJsConfuserCompat, args.json);
    return;
  }
  if (args.init) {
    initConfig(args);
    return;
  }
  if (args.migrateJavascriptObfuscator) {
    const report = migrateJavascriptObfuscatorConfig(args.migrateJavascriptObfuscator, args);
    writeMigrationReport(report, args);
    return;
  }
  if (args.migrateJsConfuser) {
    const report = migrateJsConfuserConfig(args.migrateJsConfuser, args);
    writeMigrationReport(report, args);
    return;
  }

  writeCompatibilityWarnings(args);

  if (args.releaseCheck) {
    const report = await runReleaseCheck(readConfig(args.config), args);
    writeReleaseCheckReport(report, args.json);
    if (!report.ok) {
      throw new Error("release check failed");
    }
    return;
  }

  if (args.validateConfig) {
    const report = validateProtectionConfig(readConfig(args.config), args);
    writeValidationReport(report, args.json);
    if (!report.ok) {
      throw new Error("config validation failed");
    }
    return;
  }

  const config = mergeConfig(readConfig(args.config), args);
  if (args.printConfig) {
    writeResolvedConfig(config, args.json);
    return;
  }

  if (args.doctor) {
    const report = await runDoctor(config, args);
    writeDoctorReport(report, args.json);
    if (!report.ok) {
      throw new Error("doctor checks failed");
    }
    return;
  }

  if (args.stdin) {
    const fileName = normalizeName(args.fileName || "stdin.js");
    const summary = {
      endpoint: config.endpoint,
      projectName: config.projectName,
      preset: config.preset,
      fileName,
      output: args.stdout ? "stdout" : path.join(config.output, fileName),
      options: Object.keys(config.options).filter((key) => key !== "reservedNames")
    };

    if (args.dryRun) {
      if (args.json) process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
      else process.stdout.write(`Would protect stdin as ${fileName}.\n`);
      return;
    }

    assertReady(config);
    const code = await readStdin();
    if (!code) throw new Error("No input received on stdin.");

    const protectedEntry = await protectCodeDetailed(config, code, fileName);
    const protectedCode = protectedEntry.code;
    const manifestPath = config.manifest || null;
    if (args.stdout) {
      process.stdout.write(protectedCode);
    } else {
      const target = path.join(config.output, fileName);
      fs.mkdirSync(path.dirname(target), { recursive: true });
      fs.writeFileSync(target, protectedCode, "utf8");
      if (args.json) process.stdout.write(`${JSON.stringify({ ...summary, type: "Succeed", written: [target], manifest: manifestPath, processing: protectedEntry.processing }, null, 2)}\n`);
      else process.stdout.write(`Protected stdin into ${target}\n`);
    }
    const manifest = buildStdinManifest(config, fileName, code, protectedCode, args.stdout ? "stdout" : path.join(config.output, fileName), protectedEntry.processing);
    assertSizeBudgets(manifest, config);
    writeManifest(manifestPath, manifest);
    return;
  }

  const files = collectFiles(config.input, config.output, config.extensions, config.exclude, config.include, config.markupExtensions);
  const assets = config.copyAssets ? collectAssets(config.input, config.output, files, config.assetExclude) : [];
  const protection = buildProtectionItems(config, files);
  const request = buildRequestFromItems(config, protection.items);

  const summary = addProtectionSummary({
    endpoint: config.endpoint,
    projectName: config.projectName,
    preset: config.preset,
    input: config.input,
    output: config.output,
    files: files.map((file) => file.relative),
    assets: assets.map((file) => file.relative),
    options: Object.keys(config.options).filter((key) => key !== "reservedNames")
  }, protection);

  if (args.dryRun) {
    if (args.json) process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
    else {
      process.stdout.write(`Would protect ${files.length} file(s).\n`);
      for (const file of files) process.stdout.write(`- ${file.relative}\n`);
      process.stdout.write(`API item(s): ${summary.processing.apiItems}\n`);
      for (const entry of summary.processing.transformedFiles) {
        process.stdout.write(`Transform ${entry.fileName}: ${entry.type}, ${entry.apiItems} API item(s), ${entry.preservedParts} preserved part(s)\n`);
      }
    }
    return;
  }

  assertReady(config);
  if (!files.length) {
    throw new Error("No matching input files found.");
  }

  const result = await protectItems(config, request.Items);

  writeResults(files, result, protection.transforms);
  copyAssets(assets);
  const manifestPath = config.manifest || null;
  const manifest = buildProtectionManifest(config, files, assets, result, protection.transforms);
  assertSizeBudgets(manifest, config);
  writeManifest(manifestPath, manifest);

  const done = { ...summary, type: result.Type, written: files.map((file) => file.target), copied: assets.map((file) => file.target), manifest: manifestPath };
  if (args.json) process.stdout.write(`${JSON.stringify(done, null, 2)}\n`);
  else process.stdout.write(`Protected ${files.length} file(s) and copied ${assets.length} asset(s) into ${config.output}\n`);
}

if (require.main === module) {
  main().catch((error) => {
    process.stderr.write(`jso-protector: ${error.message}\n`);
    process.exitCode = 1;
  });
}

module.exports = {
  DEFAULT_ENDPOINT,
  OPTION_REFERENCE,
  PRESET_OPTIONS,
  buildMigrationMapSummary,
  buildMigrationNextCommands,
  buildMigrationReportSummary,
  buildProtectionManifest,
  buildRequest,
  buildRequestFromItems,
  buildProtectionItems,
  buildProtectionItemsFromInputItems,
  describeProtectionTransforms,
  addProtectionSummary,
  buildCodeProtectionPlan,
  buildHtmlProtectionPlan,
  buildItemsManifest,
  buildStdinManifest,
  assertSizeBudgets,
  checkSizeBudgets,
  collectAssets,
  collectFiles,
  copyAssets,
  createExampleConfig,
  getPackageMetadata,
  getRedactedConfig,
  globLikeMatch,
  hasConditionalMarkers,
  validateConditionalMarkers,
  findMarkedHtmlScripts,
  validateMarkedHtmlScripts,
  hasMarkedHtmlScriptAttributes,
  hasHtmlProtectionMarkers,
  formatSourceLocation,
  splitConditionalCode,
  composeProtectionOutput,
  composeProtectionItemOutput,
  isIncluded,
  isExcluded,
  listJsConfuserMigrationMap,
  listJavascriptObfuscatorMigrationMap,
  explainJsConfuserCompatibilityOption,
  explainCompatibilityOption,
  listOptions,
  listPresets,
  mergeConfig,
  migrateJsConfuserConfig,
  migrateJavascriptObfuscatorConfig,
  normalizeExtensions,
  normalizeDomainLockList,
  translateJsConfuserConfigOptions,
  translateJavascriptObfuscatorConfigOptions,
  parseArgs,
  parsePositiveNumber,
  parseOptionOverrides,
  parseOptionValue,
  presetFromJavascriptObfuscatorPreset,
  postJson,
  protectCode,
  protectCodeDetailed,
  protectItems,
  readConfig,
  readEnv,
  readStdin,
  resolveEnv,
  buildReleasePlan,
  runReleaseCheck,
  runDoctor,
  validateProtectionConfig,
  writeMigrationReport,
  writeJsConfuserMigrationMap,
  writeMigrationMap,
  writeJsConfuserCompatibilityExplanation,
  writeCompatibilityExplanation,
  writeLocalOnlyGuidance,
  readManifest,
  writeValidationReport,
  writeReleaseCheckReport,
  verifyManifestOutputs,
  writeManifestVerificationReport,
  writeVersion,
  sha256,
  writeResults,
  writeResolvedConfig,
  writeManifest,
  main
};
