/*******************************************************************************
 * Copyright (C) 2025, Leo Galambos
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 
 * 3. All advertising materials mentioning features or use of this software must
 *    display the following acknowledgement:
 *    This product includes software developed by the Egothor project.
 * 
 * 4. Neither the name of the copyright holder nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 ******************************************************************************/
package zeroecho;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import javax.crypto.SecretKey;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;

import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spi.AsymmetricKeyBuilder;
import zeroecho.core.spi.SymmetricKeyBuilder;
import zeroecho.core.storage.KeyringStore;

/**
 * Command-line utility for managing key material in a text-based keyring store.
 *
 * <h2>Overview</h2> The {@code KeyStoreManagement} subcommand provides
 * lifecycle operations on key material stored in
 * {@link zeroecho.core.storage.KeyringStore}. It supports listing algorithms
 * and aliases, generating key pairs or symmetric keys, exporting/importing
 * versioned text snippets, and overwriting existing entries.
 * 
 * The keystore is a plain-text file with aliases mapped to keys. Import/export
 * uses line-oriented snippets with format-version headers, suitable for
 * exchanging public keys or migrating key material between systems.
 *
 * <h2>Usage</h2> Invoked as: <pre>{@code
 * ZeroEcho -K [options]
 * }</pre>
 *
 * <h2>Modes</h2> Exactly one action must be chosen:
 * <ul>
 * <li>{@code --list-algorithms} - list catalog algorithms and whether they
 * support symmetric/asymmetric builders.</li>
 * <li>{@code --list-aliases} - list aliases present in the keystore.</li>
 * <li>{@code --generate} - generate a new key pair or secret and store under
 * the given alias.</li>
 * <li>{@code --export} - export one or more aliases as a versioned
 * snippet.</li>
 * <li>{@code --import} - import a versioned snippet into the keystore.</li>
 * </ul>
 *
 * <h2>General options</h2>
 * <ul>
 * <li>{@code -k | --keystore <file>} - path to keystore file (required).</li>
 * <li>{@code --overwrite} - overwrite existing aliases on conflict.</li>
 * </ul>
 *
 * <h2>Generate options</h2>
 * <ul>
 * <li>{@code --alg <id>} - algorithm id (e.g., RSA, Ed25519, AES, Frodo).</li>
 * <li>{@code --alias <name>} - base alias; for asymmetric, both public and
 * private entries will be created.</li>
 * <li>{@code --kind sym|asym} - force symmetric or asymmetric if the algorithm
 * supports both (optional).</li>
 * <li>{@code --pub-suffix <sfx>} - suffix for public alias (default:
 * .pub).</li>
 * <li>{@code --prv-suffix <sfx>} - suffix for private alias (default:
 * .prv).</li>
 * </ul>
 *
 * <h2>Export options</h2>
 * <ul>
 * <li>{@code --aliases a,b,c} - comma-separated list of aliases to export
 * (default: all).</li>
 * <li>{@code --out <file|-} - output file path (default: "-" for stdout).</li>
 * </ul>
 *
 * <h2>Import options</h2>
 * <ul>
 * <li>{@code --in <file|-} - input file path (default: "-" for stdin).</li>
 * <li>{@code --overwrite} - allow replacing existing aliases when
 * importing.</li>
 * </ul>
 *
 * <h2>Examples</h2> <pre>{@code
 * # List available algorithms
 * ZeroEcho -K --list-algorithms
 *
 * # Generate a new RSA key pair and store as alice.pub / alice.prv
 * ZeroEcho -K --generate --alg RSA --alias alice --keystore keys.txt
 *
 * # Generate a new AES secret key and store as "backup-key"
 * ZeroEcho -K --generate --alg AES --alias backup-key --kind sym --keystore keys.txt
 *
 * # List aliases in the keystore
 * ZeroEcho -K --list-aliases --keystore keys.txt
 *
 * # Export selected aliases to a file
 * ZeroEcho -K --export --aliases alice.pub,alice.prv --out alice-keys.txt --keystore keys.txt
 *
 * # Import aliases from stdin, overwriting if necessary
 * ZeroEcho -K --import --in - --overwrite --keystore keys.txt < alice-keys.txt
 * }</pre>
 *
 * <h2>Exit codes</h2>
 * <ul>
 * <li>0 - operation succeeded</li>
 * <li>non-zero - error occurred (parse error, I/O failure, or invalid
 * arguments)</li>
 * </ul>
 *
 * @since 1.0
 */
public final class KeyStoreManagement { // NOPMD

    private final static String STD_IN_OUT = "-";

    // ---------------------------------------------------------------------
    // Option constants (centralized for maintainability)
    // ---------------------------------------------------------------------

    private static final Option KEYSTORE_OPTION = Option.builder("k").longOpt("keystore").hasArg().argName("file")
            .desc("Path to keyring store").build();

    private static final Option LIST_ALGORITHMS_OPTION = Option.builder().longOpt("list-algorithms")
            .desc("List catalog algorithms with symmetric/asymmetric support").build();

    private static final Option LIST_ALIASES_OPTION = Option.builder().longOpt("list-aliases")
            .desc("List aliases present in the keyring").build();

    private static final Option GENERATE_OPTION = Option.builder().longOpt("generate")
            .desc("Generate a keypair or a secret").build();

    private static final Option ALG_OPTION = Option.builder().longOpt("alg").hasArg().argName("id")
            .desc("Algorithm id (e.g., RSA, Ed25519, AES, Frodo)").build();

    private static final Option ALIAS_OPTION = Option.builder().longOpt("alias").hasArg().argName("name")
            .desc("Alias base; for asymmetric, two entries will be written").build();

    private static final Option KIND_OPTION = Option.builder().longOpt("kind").hasArg().argName("sym|asym")
            .desc("Force symmetric or asymmetric when algorithm supports both").build();

    private static final Option PUB_SUFFIX_OPTION = Option.builder().longOpt("pub-suffix").hasArg().argName("sfx")
            .desc("Suffix for public alias (default .pub)").build();

    private static final Option PRV_SUFFIX_OPTION = Option.builder().longOpt("prv-suffix").hasArg().argName("sfx")
            .desc("Suffix for private alias (default .prv)").build();

    private static final Option OVERWRITE_OPTION = Option.builder().longOpt("overwrite")
            .desc("Overwrite existing aliases on conflict").build();

    private static final Option EXPORT_OPTION = Option.builder().longOpt("export")
            .desc("Export selected aliases as a versioned text snippet").build();

    private static final Option IMPORT_OPTION = Option.builder().longOpt("import")
            .desc("Import a versioned text snippet into the keyring").build();

    private static final Option ALIASES_OPTION = Option.builder().longOpt("aliases").hasArg().argName("a,b,c")
            .desc("Comma-separated aliases to export; empty means all").build();

    private static final Option OUTFILE_OPTION = Option.builder().longOpt("out").hasArg().argName("file|-")
            .desc("Output file for export (default '-' for stdout)").build();

    private static final Option INFILE_OPTION = Option.builder().longOpt("in").hasArg().argName("file|-")
            .desc("Input file for import (default '-' for stdin)").build();

    /** Prevents instantiation. */
    private KeyStoreManagement() {
    }

    // ---------------------------------------------------------------------
    // Entry point - lets exceptions bubble to the caller (ZeroEcho)
    // ---------------------------------------------------------------------

    /**
     * Parses arguments, executes the requested action, and returns an exit code.
     * Parser and IO exceptions are intentionally propagated for the central CLI to
     * handle.
     *
     * @param args              arguments passed by the application dispatcher
     * @param dispatcherOptions an existing {@code Options} instance used by the
     *                          dispatcher; this method only adds its own options
     * @return process exit code (0 for success; non-zero for semantic errors)
     * @throws ParseException if the parser fails
     * @throws IOException    if I/O fails
     */
    public static int main(final String[] args, final Options dispatcherOptions) throws ParseException, IOException {
        defineOptions(dispatcherOptions);
        CommandLineParser parser = new DefaultParser();
        CommandLine cmd = parser.parse(dispatcherOptions, args);

        if (cmd.hasOption(LIST_ALGORITHMS_OPTION.getLongOpt())) {
            listAlgorithms();
            return 0;
        }

        if (!cmd.hasOption(KEYSTORE_OPTION.getLongOpt())) {
            throw new ParseException("Missing required option: k/keystore");
        }

        Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt()));
        KeyringStore store = Files.exists(keyringPath) ? KeyringStore.load(keyringPath) : new KeyringStore();

        if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) {
            listAliases(store);
            return 0;
        }
        if (cmd.hasOption(GENERATE_OPTION.getLongOpt())) {
            doGenerate(store, cmd);
            store.save(keyringPath);
            return 0;
        }
        if (cmd.hasOption(EXPORT_OPTION.getLongOpt())) {
            doExportSnippet(store, cmd);
            return 0;
        }
        if (cmd.hasOption(IMPORT_OPTION.getLongOpt())) {
            doImportSnippet(store, cmd);
            store.save(keyringPath);
            return 0;
        }

        throw new IllegalArgumentException("No operation selected");
    }

    /**
     * Adds this subcommand's options to the provided {@code Options} instance.
     *
     * @param options an existing {@code Options} instance from the central
     *                dispatcher
     */
    public static void defineOptions(final Options options) {
        options.addOption(KEYSTORE_OPTION);

        OptionGroup actions = new OptionGroup();
        actions.setRequired(true);
        actions.addOption(LIST_ALGORITHMS_OPTION);
        actions.addOption(LIST_ALIASES_OPTION);
        actions.addOption(GENERATE_OPTION);
        actions.addOption(EXPORT_OPTION);
        actions.addOption(IMPORT_OPTION);
        options.addOptionGroup(actions);

        options.addOption(ALG_OPTION);
        options.addOption(ALIAS_OPTION);
        options.addOption(KIND_OPTION);
        options.addOption(PUB_SUFFIX_OPTION);
        options.addOption(PRV_SUFFIX_OPTION);
        options.addOption(OVERWRITE_OPTION);

        options.addOption(ALIASES_OPTION);
        options.addOption(OUTFILE_OPTION);
        options.addOption(INFILE_OPTION);
    }

    // ---------------------------------------------------------------------
    // Actions
    // ---------------------------------------------------------------------

    /**
     * Lists available algorithms with builder availability to stdout.
     */
    public static void listAlgorithms() {
        PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
        Set<String> ids = CryptoAlgorithms.available();
        for (String id : ids) {
            CryptoAlgorithm a = CryptoAlgorithms.require(id);
            boolean hasAsym = !a.asymmetricBuildersInfo().isEmpty();
            boolean hasSym = !a.symmetricBuildersInfo().isEmpty();
            out.printf(Locale.ROOT, "%-12s  asym:%s  sym:%s%n", id, hasAsym, hasSym);
        }
    }

    /**
     * Lists aliases present in the store to stdout.
     *
     * @param store loaded keyring store
     */
    public static void listAliases(final KeyringStore store) {
        PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
        List<String> aliases = store.aliases();
        if (aliases.isEmpty()) {
            out.println("(empty)");
            return;
        }
        for (String alias : aliases) {
            out.println(alias);
        }
    }

    /**
     * Generates a keypair or secret and stores entries under the chosen aliases.
     *
     * <p>
     * This reuses the same import-spec construction heuristic as the project's
     * dynamic tests: find plausible import-spec classes and build specs from
     * SPKI/PKCS8/RAW bytes.
     * </p>
     *
     * @param store keyring store to mutate
     * @param cmd   parsed command line
     */
    public static void doGenerate(final KeyringStore store, final CommandLine cmd) { // NOPMD
        String algId = required(cmd, ALG_OPTION, "--alg is required for --generate");
        String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate");
        String kind = cmd.getOptionValue(KIND_OPTION.getLongOpt());
        String pubSfx = cmd.getOptionValue(PUB_SUFFIX_OPTION.getLongOpt(), ".pub");
        String prvSfx = cmd.getOptionValue(PRV_SUFFIX_OPTION.getLongOpt(), ".prv");
        boolean overwrite = cmd.hasOption(OVERWRITE_OPTION.getLongOpt());

        CryptoAlgorithm alg = CryptoAlgorithms.require(algId);
        boolean canAsym = !alg.asymmetricBuildersInfo().isEmpty();
        boolean canSym = !alg.symmetricBuildersInfo().isEmpty();

        boolean doAsym = "asym".equalsIgnoreCase(kind) || (kind == null && canAsym && !canSym);
        boolean doSym = "sym".equalsIgnoreCase(kind) || (kind == null && canSym && !canAsym);

        if (!doAsym && !doSym && canAsym && canSym) {
            throw new IllegalArgumentException("Algorithm supports both; specify --kind sym|asym");
        }

        if (doAsym) {
            KeyPair kp = null;
            CryptoAlgorithm.AsymBuilderInfo used = null;
            for (CryptoAlgorithm.AsymBuilderInfo bi : alg.asymmetricBuildersInfo()) {
                if (bi.defaultKeySpec == null) {
                    continue;
                }
                @SuppressWarnings("unchecked")
                Class<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType;
                AsymmetricKeyBuilder<AlgorithmKeySpec> b = alg.asymmetricKeyBuilder(st);
                try {
                    kp = b.generateKeyPair((AlgorithmKeySpec) bi.defaultKeySpec);
                    if (kp != null) {
                        used = bi;
                        break;
                    }
                } catch (Throwable ignore) { // NOPMD
                }
            }
            if (kp == null || used == null) {
                throw new IllegalStateException("No asymmetric builder with default spec worked for " + algId);
            }

            Class<?> pubImp = null;
            Class<?> prvImp = null;
            for (CryptoAlgorithm.AsymBuilderInfo x : alg.asymmetricBuildersInfo()) {
                if (looksLikeImportSpecForPublic(x.specType)) {
                    pubImp = x.specType;
                }
                if (looksLikeImportSpecForPrivate(x.specType)) {
                    prvImp = x.specType;
                }
            }
            if (pubImp == null && prvImp == null) {
                throw new IllegalStateException("No import spec class found for " + algId + " (asymmetric)");
            }

            byte[] spki = kp.getPublic() != null ? kp.getPublic().getEncoded() : null;
            byte[] pkcs8 = kp.getPrivate() != null ? kp.getPrivate().getEncoded() : null;

            AlgorithmKeySpec pubSpec = pubImp != null ? makeImportSpec(pubImp, spki, algId, used.defaultKeySpec) : null;
            AlgorithmKeySpec prvSpec = prvImp != null ? makeImportSpec(prvImp, pkcs8, algId, used.defaultKeySpec)
                    : null;

            if (pubImp != null && pubSpec == null) {
                throw new IllegalStateException("Cannot construct public import spec for " + algId);
            }
            if (prvImp != null && prvSpec == null) {
                throw new IllegalStateException("Cannot construct private import spec for " + algId);
            }

            String pubAlias = aliasBase + pubSfx;
            String prvAlias = aliasBase + prvSfx;
            ensureWritable(store, pubAlias, overwrite);
            ensureWritable(store, prvAlias, overwrite);

            store.putPublic(pubAlias, algId, pubSpec);
            store.putPrivate(prvAlias, algId, prvSpec);

            PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
            out.printf("Generated %s -> %s, %s%n", algId, pubAlias, prvAlias);
        }

        if (doSym) {
            SecretKey sk = null;
            CryptoAlgorithm.SymBuilderInfo used = null;
            for (CryptoAlgorithm.SymBuilderInfo bi : alg.symmetricBuildersInfo()) {
                if (bi.defaultKeySpec() == null) {
                    continue;
                }
                @SuppressWarnings("unchecked")
                Class<AlgorithmKeySpec> st = (Class<AlgorithmKeySpec>) bi.specType();
                SymmetricKeyBuilder<AlgorithmKeySpec> b = alg.symmetricKeyBuilder(st);
                try {
                    sk = b.generateSecret((AlgorithmKeySpec) bi.defaultKeySpec());
                    if (sk != null) {
                        used = bi;
                        break;
                    }
                } catch (Throwable ignore) { // NOPMD
                }
            }
            if (sk == null || used == null) {
                throw new IllegalStateException("No symmetric builder with default spec worked for " + algId);
            }

            Class<?> impSym = findSymmetricImportSpecClass(alg);
            if (impSym == null) {
                throw new IllegalStateException("No symmetric import spec class for " + algId);
            }

            byte[] raw = sk.getEncoded();
            AlgorithmKeySpec secSpec = makeImportSpec(impSym, raw, algId, used.defaultKeySpec());
            if (secSpec == null) {
                throw new IllegalStateException("Cannot construct symmetric import spec for " + algId);
            }

            ensureWritable(store, aliasBase, overwrite);
            store.putSecret(aliasBase, algId, secSpec);

            PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
            out.printf("Generated %s -> %s%n", algId, aliasBase);
        }
    }

    /**
     * Exports a versioned, line-oriented snippet to stdout or a file.
     *
     * @param store source keyring store
     * @param cmd   parsed command line
     * @throws IOException if writing fails
     */
    public static void doExportSnippet(final KeyringStore store, final CommandLine cmd) throws IOException {
        List<String> aliases = parseCsv(cmd.getOptionValue(ALIASES_OPTION.getLongOpt(), ""));
        if (aliases.isEmpty()) {
            aliases = store.aliases();
        }

        String text = store.exportText(aliases);
        String outFile = cmd.getOptionValue(OUTFILE_OPTION.getLongOpt(), STD_IN_OUT);

        if (STD_IN_OUT.equals(outFile)) {
            try (PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8)) {
                out.print(text);
                if (!text.endsWith("\n")) {
                    out.println();
                }
            }
        } else {
            Files.writeString(Path.of(outFile), text, StandardCharsets.UTF_8);
            System.out.printf("Wrote snippet: %s%n", outFile);
        }
    }

    /**
     * Imports a versioned, line-oriented snippet from stdin or a file.
     *
     * @param store destination keyring store
     * @param cmd   parsed command line
     * @throws IOException if reading fails or the snippet is invalid
     */
    public static void doImportSnippet(final KeyringStore store, final CommandLine cmd) throws IOException {
        boolean overwrite = cmd.hasOption(OVERWRITE_OPTION.getLongOpt());
        String inFile = cmd.getOptionValue(INFILE_OPTION.getLongOpt(), STD_IN_OUT);

        String text;
        if (STD_IN_OUT.equals(inFile)) {
            StringBuilder sb = new StringBuilder(1024);
            try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
                String line = br.readLine();
                while (line != null) {
                    sb.append(line).append('\n');
                    line = br.readLine();
                }
            }
            text = sb.toString();
        } else {
            text = Files.readString(Path.of(inFile), StandardCharsets.UTF_8);
        }

        store.importText(text, overwrite);
        PrintWriter out = new PrintWriter(System.out, true, StandardCharsets.UTF_8); // NOPMD
        out.println("Imported snippet.");
    }

    // ---------------------------------------------------------------------
    // Helpers - EXACTLY the dynamic-test strategy
    // ---------------------------------------------------------------------

    private static boolean looksLikeImportSpecForPublic(Class<?> specType) {
        String n = specType.getSimpleName();
        return n.contains("Public") || n.endsWith("PublicKeySpec");
    }

    private static boolean looksLikeImportSpecForPrivate(Class<?> specType) {
        String n = specType.getSimpleName();
        return n.contains("Private") || n.endsWith("PrivateKeySpec");
    }

    private static Class<?> findSymmetricImportSpecClass(CryptoAlgorithm alg) {
        for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) {
            String n = x.specType().getSimpleName();
            if (n.contains("Import") || n.endsWith("KeyImportSpec") || n.endsWith("SecretSpec")) {
                return x.specType();
            }
        }
        for (CryptoAlgorithm.SymBuilderInfo x : alg.symmetricBuildersInfo()) {
            try {
                x.specType().getConstructor(byte[].class);
                return x.specType();
            } catch (NoSuchMethodException ignored) {
            }
        }
        return null;
    }

    /**
     * Constructs an AlgorithmKeySpec from encoded bytes using conventional
     * factories/ctors. Mirrors the makeImportSpec approach used in the dynamic
     * test.
     *
     * @param specType    target spec class
     * @param material    encoded bytes (SPKI, PKCS#8, or RAW)
     * @param algId       algorithm id (used for variant name heuristics)
     * @param defaultSpec the default spec used by the builder (for optional variant
     *                    hints)
     * @return constructed AlgorithmKeySpec or null if none matched
     */
    private static AlgorithmKeySpec makeImportSpec(Class<?> specType, byte[] material, String algId,
            Object defaultSpec) {
        if (material == null) {
            return null;
        }
        try {
            Method m = specType.getMethod("fromRaw", byte[].class);
            Object spec = m.invoke(null, material);
            return (AlgorithmKeySpec) spec;
        } catch (NoSuchMethodException ignored) {
        } catch (Throwable t) { // NOPMD
        }
        try {
            Method m = specType.getMethod("fromRaw", String.class, byte[].class);
            String name = deriveVariantNameForImport(algId, defaultSpec);
            Object spec = m.invoke(null, name, material);
            return (AlgorithmKeySpec) spec;
        } catch (NoSuchMethodException ignored) {
        } catch (Throwable t) { // NOPMD
        }
        try {
            Method m = specType.getMethod("of", byte[].class);
            Object spec = m.invoke(null, material);
            return (AlgorithmKeySpec) spec;
        } catch (NoSuchMethodException ignored) {
        } catch (Throwable t) { // NOPMD
        }
        try {
            Constructor<?> c = specType.getConstructor(byte[].class);
            Object spec = c.newInstance(material);
            return (AlgorithmKeySpec) spec;
        } catch (NoSuchMethodException ignored) {
        } catch (Throwable t) { // NOPMD
        }
        try {
            Constructor<?> c = specType.getConstructor(String.class);
            String b64 = Base64.getEncoder().encodeToString(material);
            Object spec = c.newInstance(b64);
            return (AlgorithmKeySpec) spec;
        } catch (NoSuchMethodException ignored) {
        } catch (Throwable t) { // NOPMD
        }
        return null;
    }

    private static String deriveVariantNameForImport(String algId, Object defaultSpec) {
        if (defaultSpec != null) {
            try {
                Method m = defaultSpec.getClass().getMethod("macName");
                Object v = m.invoke(defaultSpec);
                if (v instanceof String) {
                    return (String) v;
                }
            } catch (Throwable ignored) { // NOPMD
            }
        }
        if ("HMAC".equalsIgnoreCase(algId)) { // NOPMD
            return "HmacSHA256";
        }
        return algId;
    }

    private static String required(CommandLine cmd, Option opt, String message) {
        if (!cmd.hasOption(opt.getLongOpt())) {
            throw new IllegalArgumentException(message);
        }
        return cmd.getOptionValue(opt.getLongOpt());
    }

    private static List<String> parseCsv(String csv) {
        List<String> list = new ArrayList<>();
        if (csv == null || csv.isBlank()) {
            return list;
        }
        String[] parts = csv.split("\\s*,\\s*");
        for (String part : parts) {
            if (!part.isBlank()) {
                list.add(part);
            }
        }
        return list;
    }

    /**
     * Ensures that an alias can be written under the current collision policy.
     *
     * @param store     keyring store
     * @param alias     alias to check
     * @param overwrite whether collisions are allowed
     * @throws IllegalArgumentException if alias exists and overwrite is false
     */
    private static void ensureWritable(KeyringStore store, String alias, boolean overwrite) {
        if (store.contains(alias) && !overwrite) {
            throw new IllegalArgumentException("Alias already exists: " + alias + " (use --overwrite)");
        }
    }
}