AiOptions.java

package org.egothor.methodatlas.ai;

import java.nio.file.Path;
import java.time.Duration;
import java.util.Objects;

/**
 * Immutable configuration describing how AI-based enrichment should be
 * performed during a {@link org.egothor.methodatlas.MethodAtlasApp} execution.
 *
 * <p>
 * This record aggregates all runtime parameters required by the AI integration
 * layer, including provider selection, model identification, authentication
 * configuration, taxonomy selection, request limits, and retry behavior.
 * </p>
 *
 * <p>
 * Instances of this record are typically constructed using the associated
 * {@link Builder} and passed to the AI subsystem when initializing an
 * {@link AiSuggestionEngine}. The configuration is immutable once constructed
 * and therefore safe to share between concurrent components.
 * </p>
 *
 * <h2>Configuration Responsibilities</h2>
 *
 * <ul>
 * <li>AI provider selection and endpoint configuration</li>
 * <li>model name resolution</li>
 * <li>API key discovery</li>
 * <li>taxonomy configuration for security classification</li>
 * <li>input size limits for class source submission</li>
 * <li>network timeout configuration</li>
 * <li>retry policy for transient AI failures</li>
 * </ul>
 *
 * <p>
 * Default values are supplied by the {@link Builder} when parameters are not
 * explicitly provided.
 * </p>
 *
 * @param enabled       whether AI enrichment is enabled
 * @param provider      AI provider used to perform analysis
 * @param modelName     provider-specific model identifier
 * @param baseUrl       base API endpoint used by the selected provider
 * @param apiKey        API key used for authentication, if provided directly
 * @param apiKeyEnv     environment variable name containing the API key
 * @param taxonomyFile  optional path to an external taxonomy definition
 * @param taxonomyMode  built-in taxonomy mode to use when no file is provided
 * @param maxClassChars maximum number of characters allowed for class source
 *                      submitted to the AI provider
 * @param timeout       request timeout applied to AI calls
 * @param maxRetries    number of retry attempts for failed AI operations
 * @param confidence    whether the AI provider should be asked to include a
 *                      confidence score for each security-relevant method
 *                      classification; requires model support and increases
 *                      token usage slightly
 *
 * @see AiSuggestionEngine
 * @see Builder
 */
public record AiOptions(boolean enabled, AiProvider provider, String modelName, String baseUrl, String apiKey,
        String apiKeyEnv, Path taxonomyFile, TaxonomyMode taxonomyMode, int maxClassChars, Duration timeout,
        int maxRetries, boolean confidence) {
    /**
     * Built-in taxonomy modes used for security classification.
     *
     * <p>
     * These modes determine which internal taxonomy definition is supplied to the
     * AI provider when an external taxonomy file is not configured.
     * </p>
     *
     * <ul>
     * <li>{@link #DEFAULT} – general-purpose taxonomy suitable for human
     * readability</li>
     * <li>{@link #OPTIMIZED} – compact taxonomy optimized for AI classification
     * accuracy</li>
     * </ul>
     */
    public enum TaxonomyMode {
        /**
         * Standard taxonomy definition emphasizing clarity and documentation.
         */
        DEFAULT,
        /**
         * Reduced taxonomy optimized for improved AI classification reliability.
         */
        OPTIMIZED
    }

    /**
     * Default model identifier used when no model is explicitly configured.
     *
     * <p>
     * This constant is intentionally public so that governance processes can
     * locate and track the approved fallback model in version control without
     * searching through builder internals.
     * </p>
     */
    public static final String DEFAULT_MODEL = "qwen2.5-coder:7b";

    /**
     * Canonical constructor performing validation of configuration parameters.
     *
     * <p>
     * The constructor enforces basic invariants required for correct operation of
     * the AI integration layer. Invalid values result in an
     * {@link IllegalArgumentException}.
     * </p>
     *
     * @throws NullPointerException     if required parameters such as
     *                                  {@code provider}, {@code modelName},
     *                                  {@code timeout}, or {@code taxonomyMode} are
     *                                  {@code null}
     * @throws IllegalArgumentException if configuration values violate required
     *                                  constraints
     */
    public AiOptions {
        Objects.requireNonNull(provider, "provider");
        Objects.requireNonNull(modelName, "modelName");
        Objects.requireNonNull(timeout, "timeout");
        Objects.requireNonNull(taxonomyMode, "taxonomyMode");

        if (baseUrl == null || baseUrl.isBlank()) {
            throw new IllegalArgumentException("baseUrl must not be blank");
        }
        if (maxClassChars <= 0) {
            throw new IllegalArgumentException("maxClassChars must be > 0");
        }
        if (maxRetries < 0) {
            throw new IllegalArgumentException("maxRetries must be >= 0");
        }
    }

    /**
     * Creates a new {@link Builder} used to construct {@link AiOptions} instances.
     *
     * <p>
     * The builder supplies sensible defaults for most configuration values and
     * allows incremental customization before producing the final immutable
     * configuration record.
     * </p>
     *
     * @return new builder instance
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Resolves the effective API key used for authenticating AI provider requests.
     *
     * <p>
     * The resolution strategy is:
     * </p>
     *
     * <ol>
     * <li>If {@link #apiKey()} is defined and non-empty, it is returned.</li>
     * <li>If {@link #apiKeyEnv()} is defined, the corresponding environment
     * variable is resolved using {@link System#getenv(String)}.</li>
     * <li>If neither source yields a value, {@code null} is returned.</li>
     * </ol>
     *
     * @return resolved API key or {@code null} if none is available
     */
    public String resolvedApiKey() {
        if (apiKey != null && !apiKey.isBlank()) {
            return apiKey;
        }
        if (apiKeyEnv != null && !apiKeyEnv.isBlank()) {
            String value = System.getenv(apiKeyEnv);
            if (value != null && !value.isBlank()) {
                return value;
            }
        }
        return null;
    }

    /**
     * Mutable builder used to construct validated {@link AiOptions} instances.
     *
     * <p>
     * The builder follows the conventional staged construction pattern, allowing
     * optional parameters to be supplied before producing the final immutable
     * configuration record via {@link #build()}.
     * </p>
     *
     * <p>
     * Reasonable defaults are provided for most parameters so that only
     * provider-specific details typically need to be configured explicitly.
     * </p>
     */
    public static final class Builder {
        private boolean enabled;
        private AiProvider provider = AiProvider.AUTO;
        private String modelName = DEFAULT_MODEL;
        private String baseUrl;
        private String apiKey;
        private String apiKeyEnv;
        private Path taxonomyFile;
        private TaxonomyMode taxonomyMode = TaxonomyMode.DEFAULT;
        private int maxClassChars = 40_000;
        private Duration timeout = Duration.ofSeconds(90);
        private int maxRetries = 1;
        private boolean confidence;

        /**
         * Enables or disables AI enrichment.
         *
         * @param enabled {@code true} to enable AI integration
         * @return this builder
         */
        public Builder enabled(boolean enabled) {
            this.enabled = enabled;
            return this;
        }

        /**
         * Selects the AI provider.
         *
         * @param provider provider implementation to use
         * @return this builder
         */
        public Builder provider(AiProvider provider) {
            this.provider = provider;
            return this;
        }

        /**
         * Specifies the provider-specific model identifier.
         *
         * @param modelName name of the model to use
         * @return this builder
         */
        public Builder modelName(String modelName) {
            this.modelName = modelName;
            return this;
        }

        /**
         * Sets the base API endpoint used by the provider.
         *
         * @param baseUrl base URL of the provider API
         * @return this builder
         */
        public Builder baseUrl(String baseUrl) {
            this.baseUrl = baseUrl;
            return this;
        }

        /**
         * Sets the API key used for authentication.
         *
         * @param apiKey API key value
         * @return this builder
         */
        public Builder apiKey(String apiKey) {
            this.apiKey = apiKey;
            return this;
        }

        /**
         * Specifies the environment variable that stores the API key.
         *
         * @param apiKeyEnv environment variable name
         * @return this builder
         */
        public Builder apiKeyEnv(String apiKeyEnv) {
            this.apiKeyEnv = apiKeyEnv;
            return this;
        }

        /**
         * Specifies an external taxonomy definition file.
         *
         * @param taxonomyFile path to taxonomy definition
         * @return this builder
         */
        public Builder taxonomyFile(Path taxonomyFile) {
            this.taxonomyFile = taxonomyFile;
            return this;
        }

        /**
         * Selects the built-in taxonomy mode.
         *
         * @param taxonomyMode taxonomy variant
         * @return this builder
         */
        public Builder taxonomyMode(TaxonomyMode taxonomyMode) {
            this.taxonomyMode = taxonomyMode;
            return this;
        }

        /**
         * Sets the maximum size of class source submitted to the AI provider.
         *
         * @param maxClassChars maximum allowed character count
         * @return this builder
         */
        public Builder maxClassChars(int maxClassChars) {
            this.maxClassChars = maxClassChars;
            return this;
        }

        /**
         * Sets the timeout applied to AI requests.
         *
         * @param timeout request timeout
         * @return this builder
         */
        public Builder timeout(Duration timeout) {
            this.timeout = timeout;
            return this;
        }

        /**
         * Sets the retry limit for AI requests.
         *
         * @param maxRetries retry count
         * @return this builder
         */
        public Builder maxRetries(int maxRetries) {
            this.maxRetries = maxRetries;
            return this;
        }

        /**
         * Enables or disables AI confidence scoring.
         *
         * <p>
         * When enabled, the prompt instructs the AI provider to return a
         * {@code confidence} value for each method classification. The value is
         * included in the output as an {@code ai_confidence} column.
         * </p>
         *
         * @param confidence {@code true} to request confidence scores
         * @return this builder
         */
        public Builder confidence(boolean confidence) {
            this.confidence = confidence;
            return this;
        }

        /**
         * Builds the final immutable {@link AiOptions} configuration.
         *
         * <p>
         * If no base URL is explicitly supplied, a provider-specific default endpoint
         * is selected automatically.
         * </p>
         *
         * @return validated AI configuration
         */
        public AiOptions build() {
            AiProvider effectiveProvider = provider == null ? AiProvider.AUTO : provider;
            String effectiveBaseUrl = baseUrl;

            if (effectiveBaseUrl == null || effectiveBaseUrl.isBlank()) {
                effectiveBaseUrl = switch (effectiveProvider) {
                    case AUTO, OLLAMA -> "http://localhost:11434";
                    case OPENAI -> "https://api.openai.com";
                    case OPENROUTER -> "https://openrouter.ai/api";
                    case ANTHROPIC -> "https://api.anthropic.com";
                };
            }

            return new AiOptions(enabled, effectiveProvider, modelName, effectiveBaseUrl, apiKey, apiKeyEnv,
                    taxonomyFile, taxonomyMode, maxClassChars, timeout, maxRetries, confidence);
        }
    }
}