package de.gecheckt.pdf.umbenenner.adapter.out.configuration;

import java.util.Properties;
import java.util.function.Function;

import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;
import de.gecheckt.pdf.umbenenner.application.config.provider.MultiProviderConfiguration;
import de.gecheckt.pdf.umbenenner.application.config.provider.ProviderConfiguration;

/**
 * Parses the multi-provider configuration schema from a {@link Properties} object.
 * <p>
 * Recognises the following property keys:
 * <pre>
 *   ai.provider.active                         – required; must be "openai-compatible" or "claude"
 *   ai.provider.openai-compatible.baseUrl      – required for active OpenAI-compatible provider
 *   ai.provider.openai-compatible.model        – required for active OpenAI-compatible provider
 *   ai.provider.openai-compatible.timeoutSeconds
 *   ai.provider.openai-compatible.apiKey
 *   ai.provider.claude.baseUrl                 – optional; defaults to https://api.anthropic.com
 *   ai.provider.claude.model                   – required for active Claude provider
 *   ai.provider.claude.timeoutSeconds
 *   ai.provider.claude.apiKey
 * </pre>
 *
 * <h2>Environment-variable precedence for API keys</h2>
 * <ul>
 *   <li>{@code OPENAI_COMPATIBLE_API_KEY} overrides {@code ai.provider.openai-compatible.apiKey}</li>
 *   <li>{@code ANTHROPIC_API_KEY} overrides {@code ai.provider.claude.apiKey}</li>
 * </ul>
 * Each environment variable is applied only to its own provider family; the variables
 * of different families are never mixed.
 *
 * <h2>Error handling</h2>
 * <ul>
 *   <li>If {@code ai.provider.active} is absent or blank, a {@link ConfigurationLoadingException}
 *       is thrown.</li>
 *   <li>If {@code ai.provider.active} holds an unrecognised value, a
 *       {@link ConfigurationLoadingException} is thrown.</li>
 *   <li>If a {@code timeoutSeconds} property is present but not a valid integer, a
 *       {@link ConfigurationLoadingException} is thrown.</li>
 *   <li>Missing optional fields result in {@code null} (String) or {@code 0} (int) stored in
 *       the returned record; the validator enforces required fields for the active provider.</li>
 * </ul>
 *
 * <p>The returned {@link MultiProviderConfiguration} is not yet validated. Use
 * {@link MultiProviderConfigurationValidator} after parsing.
 */
public class MultiProviderConfigurationParser {

    /** Property key selecting the active provider family. */
    static final String PROP_ACTIVE_PROVIDER = "ai.provider.active";

    static final String PROP_OPENAI_BASE_URL = "ai.provider.openai-compatible.baseUrl";
    static final String PROP_OPENAI_MODEL = "ai.provider.openai-compatible.model";
    static final String PROP_OPENAI_TIMEOUT = "ai.provider.openai-compatible.timeoutSeconds";
    static final String PROP_OPENAI_API_KEY = "ai.provider.openai-compatible.apiKey";

    static final String PROP_CLAUDE_BASE_URL = "ai.provider.claude.baseUrl";
    static final String PROP_CLAUDE_MODEL = "ai.provider.claude.model";
    static final String PROP_CLAUDE_TIMEOUT = "ai.provider.claude.timeoutSeconds";
    static final String PROP_CLAUDE_API_KEY = "ai.provider.claude.apiKey";

    /** Environment variable for the OpenAI-compatible provider API key. */
    static final String ENV_OPENAI_API_KEY = "OPENAI_COMPATIBLE_API_KEY";

    /**
     * Legacy environment variable for the OpenAI-compatible provider API key.
     * <p>
     * Accepted as a fallback when {@code OPENAI_COMPATIBLE_API_KEY} is not set.
     * Existing installations that set this variable continue to work without change.
     * New installations should prefer {@code OPENAI_COMPATIBLE_API_KEY}.
     */
    static final String ENV_LEGACY_OPENAI_API_KEY = "PDF_UMBENENNER_API_KEY";

    /** Environment variable for the Anthropic Claude provider API key. */
    static final String ENV_CLAUDE_API_KEY = "ANTHROPIC_API_KEY";

    /** Default base URL for the Anthropic Claude provider when not explicitly configured. */
    static final String CLAUDE_DEFAULT_BASE_URL = "https://api.anthropic.com";

    private final Function<String, String> environmentLookup;

    /**
     * Creates a parser that uses the real system environment for API key resolution.
     */
    public MultiProviderConfigurationParser() {
        this(System::getenv);
    }

    /**
     * Creates a parser with a custom environment lookup function.
     * <p>
     * This constructor is intended for testing to allow deterministic control over
     * environment variable values without modifying the real process environment.
     *
     * @param environmentLookup a function that maps environment variable names to their values;
     *                          must not be {@code null}
     */
    public MultiProviderConfigurationParser(Function<String, String> environmentLookup) {
        this.environmentLookup = environmentLookup;
    }

    /**
     * Parses the multi-provider configuration from the given properties.
     * <p>
     * The Claude default base URL ({@code https://api.anthropic.com}) is applied when
     * {@code ai.provider.claude.baseUrl} is absent. API keys are resolved with environment
     * variable precedence. The resulting configuration is not yet validated; call
     * {@link MultiProviderConfigurationValidator#validate(MultiProviderConfiguration)} afterward.
     *
     * @param props the properties to parse; must not be {@code null}
     * @return the parsed (but not yet validated) multi-provider configuration
     * @throws ConfigurationLoadingException if {@code ai.provider.active} is absent, blank,
     *         or holds an unrecognised value, or if any present timeout property is not a
     *         valid integer
     */
    public MultiProviderConfiguration parse(Properties props) {
        AiProviderFamily activeFamily = parseActiveProvider(props);
        ProviderConfiguration openAiConfig = parseOpenAiCompatibleConfig(props);
        ProviderConfiguration claudeConfig = parseClaudeConfig(props);
        return new MultiProviderConfiguration(activeFamily, openAiConfig, claudeConfig);
    }

    private AiProviderFamily parseActiveProvider(Properties props) {
        String raw = props.getProperty(PROP_ACTIVE_PROVIDER);
        if (raw == null || raw.isBlank()) {
            throw new ConfigurationLoadingException(
                    "Required property missing or blank: " + PROP_ACTIVE_PROVIDER
                    + ". Valid values: openai-compatible, claude");
        }
        String trimmed = raw.trim();
        return AiProviderFamily.fromIdentifier(trimmed).orElseThrow(() ->
                new ConfigurationLoadingException(
                        "Unknown provider identifier for " + PROP_ACTIVE_PROVIDER + ": '" + trimmed
                        + "'. Valid values: openai-compatible, claude"));
    }

    private ProviderConfiguration parseOpenAiCompatibleConfig(Properties props) {
        String model = getOptionalString(props, PROP_OPENAI_MODEL);
        int timeout = parseTimeoutSeconds(props, PROP_OPENAI_TIMEOUT);
        String baseUrl = getOptionalString(props, PROP_OPENAI_BASE_URL);
        String apiKey = resolveOpenAiApiKey(props);
        return new ProviderConfiguration(model, timeout, baseUrl, apiKey);
    }

    private ProviderConfiguration parseClaudeConfig(Properties props) {
        String model = getOptionalString(props, PROP_CLAUDE_MODEL);
        int timeout = parseTimeoutSeconds(props, PROP_CLAUDE_TIMEOUT);
        String baseUrl = getStringOrDefault(props, PROP_CLAUDE_BASE_URL, CLAUDE_DEFAULT_BASE_URL);
        String apiKey = resolveApiKey(props, PROP_CLAUDE_API_KEY, ENV_CLAUDE_API_KEY);
        return new ProviderConfiguration(model, timeout, baseUrl, apiKey);
    }

    /**
     * Returns the trimmed property value, or {@code null} if absent or blank.
     */
    private String getOptionalString(Properties props, String key) {
        String value = props.getProperty(key);
        return (value == null || value.isBlank()) ? null : value.trim();
    }

    /**
     * Returns the trimmed property value, or the {@code defaultValue} if absent or blank.
     */
    private String getStringOrDefault(Properties props, String key, String defaultValue) {
        String value = props.getProperty(key);
        return (value == null || value.isBlank()) ? defaultValue : value.trim();
    }

    /**
     * Parses a timeout property as a positive integer.
     * <p>
     * Returns {@code 0} when the property is absent or blank (indicating "not configured").
     * Throws {@link ConfigurationLoadingException} when the property is present but not
     * parseable as an integer.
     */
    private int parseTimeoutSeconds(Properties props, String key) {
        String value = props.getProperty(key);
        if (value == null || value.isBlank()) {
            return 0;
        }
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException e) {
            throw new ConfigurationLoadingException(
                    "Invalid integer value for property " + key + ": '" + value.trim() + "'", e);
        }
    }

    /**
     * Resolves the effective API key for the OpenAI-compatible provider.
     * <p>
     * Resolution order:
     * <ol>
     *   <li>{@code OPENAI_COMPATIBLE_API_KEY} environment variable</li>
     *   <li>{@code PDF_UMBENENNER_API_KEY} environment variable (legacy fallback;
     *       accepted for backward compatibility with existing installations)</li>
     *   <li>{@code ai.provider.openai-compatible.apiKey} property</li>
     * </ol>
     *
     * @param props the configuration properties
     * @return the resolved API key; never {@code null}, but may be blank
     */
    private String resolveOpenAiApiKey(Properties props) {
        String primary = environmentLookup.apply(ENV_OPENAI_API_KEY);
        if (primary != null && !primary.isBlank()) {
            return primary.trim();
        }
        String legacy = environmentLookup.apply(ENV_LEGACY_OPENAI_API_KEY);
        if (legacy != null && !legacy.isBlank()) {
            return legacy.trim();
        }
        String propsValue = props.getProperty(PROP_OPENAI_API_KEY);
        return (propsValue != null) ? propsValue.trim() : "";
    }

    /**
     * Resolves the effective API key for a provider family.
     * <p>
     * The environment variable value takes precedence over the properties value.
     * If the environment variable is absent or blank, the properties value is used.
     * If both are absent or blank, an empty string is returned (the validator will
     * reject this for the active provider).
     *
     * @param props       the configuration properties
     * @param propertyKey the property key for the API key of this provider family
     * @param envVarName  the environment variable name for this provider family
     * @return the resolved API key; never {@code null}, but may be blank
     */
    private String resolveApiKey(Properties props, String propertyKey, String envVarName) {
        String envValue = environmentLookup.apply(envVarName);
        if (envValue != null && !envValue.isBlank()) {
            return envValue.trim();
        }
        String propsValue = props.getProperty(propertyKey);
        return (propsValue != null) ? propsValue.trim() : "";
    }
}
