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

import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.UnaryOperator;

import de.gecheckt.pdf.umbenenner.application.config.provider.MultiProviderConfiguration;
import de.gecheckt.pdf.umbenenner.application.config.startup.StartConfiguration;
import de.gecheckt.pdf.umbenenner.application.port.out.ConfigurationPort;

/**
 * Properties-based implementation of {@link ConfigurationPort}.
 * <p>
 * Loads configuration from {@code config/application.properties} as the primary source.
 * The multi-provider AI configuration is parsed via {@link MultiProviderConfigurationParser}
 * and validated via {@link MultiProviderConfigurationValidator}. Environment variables
 * for API keys are resolved by the parser with provider-specific precedence rules:
 * {@code OPENAI_COMPATIBLE_API_KEY} for the OpenAI-compatible family and
 * {@code ANTHROPIC_API_KEY} for the Anthropic Claude family.
 */
public class PropertiesConfigurationPortAdapter implements ConfigurationPort {

    private static final String DEFAULT_CONFIG_FILE_PATH = "config/application.properties";

    private final Function<String, String> environmentLookup;
    private final Path configFilePath;

    /**
     * Creates the adapter with default system environment lookup and default config file path.
     */
    public PropertiesConfigurationPortAdapter() {
        this(System::getenv, Paths.get(DEFAULT_CONFIG_FILE_PATH));
    }

    /**
     * Creates the adapter with a custom environment lookup function.
     * <p>
     * This constructor is primarily intended for testing purposes to allow deterministic
     * control over environment variable values without modifying the real process environment.
     *
     * @param environmentLookup a function that looks up environment variables by name
     */
    public PropertiesConfigurationPortAdapter(UnaryOperator<String> environmentLookup) {
        this(environmentLookup, Paths.get(DEFAULT_CONFIG_FILE_PATH));
    }

    /**
     * Creates the adapter with a custom config file path.
     * <p>
     * This constructor is primarily intended for testing purposes to allow loading
     * configuration from arbitrary temporary files.
     *
     * @param configFilePath the path to the configuration properties file
     */
    public PropertiesConfigurationPortAdapter(Path configFilePath) {
        this(System::getenv, configFilePath);
    }

    /**
     * Creates the adapter with custom environment lookup and config file path.
     * <p>
     * This constructor is primarily intended for testing purposes.
     *
     * @param environmentLookup a function that looks up environment variables by name
     * @param configFilePath the path to the configuration properties file
     */
    public PropertiesConfigurationPortAdapter(UnaryOperator<String> environmentLookup, Path configFilePath) {
        this.environmentLookup = environmentLookup;
        this.configFilePath = configFilePath;
    }

    @Override
    public StartConfiguration loadConfiguration() {
        Properties props = loadPropertiesFile();
        MultiProviderConfiguration multiProviderConfig = parseAndValidateProviders(props);
        boolean logAiSensitive = parseAiContentSensitivity(props);
        return buildStartConfiguration(props, multiProviderConfig, logAiSensitive);
    }

    private Properties loadPropertiesFile() {
        Properties props = new Properties();
        try {
            if (!Files.exists(configFilePath)) {
                throw new java.io.FileNotFoundException("Config file not found: " + configFilePath);
            }
            String content = Files.readString(configFilePath, StandardCharsets.UTF_8);
            String escapedContent = escapeBackslashes(content);
            props.load(new StringReader(escapedContent));
        } catch (IOException e) {
            throw new ConfigurationLoadingException("Failed to load configuration from " + configFilePath, e);
        }
        return props;
    }

    /**
     * Parses and validates the multi-provider AI configuration from the given properties.
     * <p>
     * Uses {@link MultiProviderConfigurationParser} for parsing and
     * {@link MultiProviderConfigurationValidator} for validation. Throws on any
     * configuration error before returning.
     */
    private MultiProviderConfiguration parseAndValidateProviders(Properties props) {
        MultiProviderConfigurationParser parser = new MultiProviderConfigurationParser(environmentLookup);
        MultiProviderConfiguration config = parser.parse(props);
        new MultiProviderConfigurationValidator().validate(config);
        return config;
    }

    private StartConfiguration buildStartConfiguration(Properties props,
                                                        MultiProviderConfiguration multiProviderConfig,
                                                        boolean logAiSensitive) {
        return new StartConfiguration(
                Paths.get(getRequiredProperty(props, "source.folder")),
                Paths.get(getRequiredProperty(props, "target.folder")),
                Paths.get(getRequiredProperty(props, "sqlite.file")),
                multiProviderConfig,
                parseInt(getRequiredProperty(props, "max.retries.transient")),
                parseInt(getRequiredProperty(props, "max.pages")),
                parseInt(getRequiredProperty(props, "max.text.characters")),
                parseMaxTitleLength(props),
                Paths.get(getRequiredProperty(props, "prompt.template.file")),
                Paths.get(getOptionalProperty(props, "runtime.lock.file", "")),
                Paths.get(getOptionalProperty(props, "log.directory", "")),
                getOptionalProperty(props, "log.level", "INFO"),
                logAiSensitive
        );
    }

    private String escapeBackslashes(String content) {
        // Escape backslashes to prevent Java Properties from interpreting them as escape sequences.
        // This is needed because Windows paths use backslashes (e.g., C:\temp\...)
        // and Java Properties interprets \t as tab, \n as newline, etc.
        return content.replace("\\", "\\\\");
    }

    private String getRequiredProperty(Properties props, String key) {
        String value = props.getProperty(key);
        if (value == null || value.isBlank()) {
            throw new ConfigurationLoadingException("Required property missing: " + key);
        }
        return normalizePath(value.trim());
    }

    private String getOptionalProperty(Properties props, String key, String defaultValue) {
        String value = props.getProperty(key);
        return (value == null || value.isBlank()) ? defaultValue : normalizePath(value.trim());
    }

    /**
     * Normalizes a property value that represents a path.
     * Converts backslashes to forward slashes to avoid issues with Java Properties
     * escape sequence interpretation (e.g., \t becoming tab).
     * Also trims whitespace from the value.
     */
    private String normalizePath(String value) {
        return value.replace('\\', '/');
    }

    private int parseInt(String value) {
        try {
            return Integer.parseInt(value.trim());
        } catch (NumberFormatException e) {
            throw new ConfigurationLoadingException("Invalid integer value for property: " + value, e);
        }
    }

    /**
     * Parses the {@code max.title.length} property.
     * <p>
     * This property controls the maximum length of the base title portion of the generated
     * target filename. When the property is absent or blank in the properties file, the
     * default value {@value #DEFAULT_MAX_TITLE_LENGTH} is returned for backward compatibility
     * with configurations that pre-date this setting.
     * <p>
     * When the property is present but cannot be parsed as an integer, a
     * {@link ConfigurationLoadingException} is thrown. The numeric range of the parsed value
     * is enforced downstream in the start-configuration validator.
     *
     * @param props the raw loaded properties; must not be {@code null}
     * @return the parsed maximum base title length
     * @throws ConfigurationLoadingException if the property is present but cannot be parsed
     */
    private int parseMaxTitleLength(Properties props) {
        String raw = props.getProperty("max.title.length");
        if (raw == null || raw.isBlank()) {
            return DEFAULT_MAX_TITLE_LENGTH;
        }
        try {
            return Integer.parseInt(raw.trim());
        } catch (NumberFormatException e) {
            throw new ConfigurationLoadingException(
                    "Invalid integer value for property max.title.length: '" + raw + "'", e);
        }
    }

    /**
     * Default value for {@code max.title.length} when the property is missing or blank.
     * <p>
     * Chosen to preserve the previously hardcoded limit, so existing configurations without
     * the property continue to work unchanged.
     */
    private static final int DEFAULT_MAX_TITLE_LENGTH = 60;

    /**
     * Parses the {@code log.ai.sensitive} configuration property with strict validation.
     * <p>
     * This property controls whether sensitive AI-generated content (raw response, reasoning)
     * may be written to log files. It must be either the literal string "true" or "false"
     * (case-insensitive). Any other value is rejected as an invalid startup configuration.
     * <p>
     * The default value (when the property is absent) is {@code false}, which is the safe default.
     *
     * @return {@code true} if the property is explicitly set to "true", {@code false} otherwise
     * @throws ConfigurationLoadingException if the property is present but contains an invalid value
     */
    private boolean parseAiContentSensitivity(Properties props) {
        String value = props.getProperty("log.ai.sensitive");

        // If absent, return safe default
        if (value == null) {
            return false;
        }

        String trimmedValue = value.trim().toLowerCase();

        // Only accept literal "true" or "false"
        if ("true".equals(trimmedValue)) {
            return true;
        } else if ("false".equals(trimmedValue)) {
            return false;
        } else {
            // Reject any other value as invalid configuration
            throw new ConfigurationLoadingException(
                    "Invalid value for log.ai.sensitive: '" + value + "'. "
                            + "Must be either 'true' or 'false' (case-insensitive). "
                            + "Default is 'false' (sensitive content not logged).");
        }
    }
}
