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.StandardCopyOption;
import java.util.Properties;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import de.gecheckt.pdf.umbenenner.adapter.out.bootstrap.validation.InvalidStartConfigurationException;
import de.gecheckt.pdf.umbenenner.application.config.provider.MultiProviderConfiguration;

/**
 * Detects and migrates a legacy flat-key configuration file to the multi-provider schema.
 *
 * <h2>Legacy form</h2>
 * A configuration file is considered legacy if it contains at least one of the flat property
 * keys ({@code api.baseUrl}, {@code api.model}, {@code api.timeoutSeconds}, {@code api.key})
 * and does <em>not</em> already contain {@code ai.provider.active}.
 *
 * <h2>Migration procedure</h2>
 * <ol>
 *   <li>Detect legacy form; if absent, return immediately without any I/O side effect.</li>
 *   <li>Create a {@code .bak} backup of the original file before any changes. If a {@code .bak}
 *       file already exists, a numbered suffix is appended ({@code .bak.1}, {@code .bak.2}, …).
 *       Existing backups are never overwritten.</li>
 *   <li>Rewrite the file:
 *     <ul>
 *       <li>{@code api.baseUrl} → {@code ai.provider.openai-compatible.baseUrl}</li>
 *       <li>{@code api.model} → {@code ai.provider.openai-compatible.model}</li>
 *       <li>{@code api.timeoutSeconds} → {@code ai.provider.openai-compatible.timeoutSeconds}</li>
 *       <li>{@code api.key} → {@code ai.provider.openai-compatible.apiKey}</li>
 *       <li>{@code ai.provider.active=openai-compatible} is appended.</li>
 *       <li>A commented placeholder section for the Claude provider is appended.</li>
 *       <li>All other keys are carried over unchanged in stable order.</li>
 *     </ul>
 *   </li>
 *   <li>Write the migrated content via a temporary file ({@code <file>.tmp}) followed by an
 *       atomic move/rename. The original file is never partially overwritten.</li>
 *   <li>Reload the migrated file and validate it with {@link MultiProviderConfigurationParser}
 *       and {@link MultiProviderConfigurationValidator}. If validation fails, a
 *       {@link ConfigurationLoadingException} is thrown; the {@code .bak} is preserved.</li>
 * </ol>
 */
public class LegacyConfigurationMigrator {

    private static final Logger LOG = LogManager.getLogger(LegacyConfigurationMigrator.class);

    /** Legacy flat key for base URL, replaced during migration. */
    static final String LEGACY_BASE_URL = "api.baseUrl";

    /** Legacy flat key for model name, replaced during migration. */
    static final String LEGACY_MODEL = "api.model";

    /** Legacy flat key for timeout, replaced during migration. */
    static final String LEGACY_TIMEOUT = "api.timeoutSeconds";

    /** Legacy flat key for API key, replaced during migration. */
    static final String LEGACY_API_KEY = "api.key";

    private static final String[][] LEGACY_KEY_MAPPINGS = {
        {LEGACY_BASE_URL,  "ai.provider.openai-compatible.baseUrl"},
        {LEGACY_MODEL,     "ai.provider.openai-compatible.model"},
        {LEGACY_TIMEOUT,   "ai.provider.openai-compatible.timeoutSeconds"},
        {LEGACY_API_KEY,   "ai.provider.openai-compatible.apiKey"},
    };

    private final MultiProviderConfigurationParser parser;
    private final MultiProviderConfigurationValidator validator;

    /**
     * Creates a migrator backed by default parser and validator instances.
     */
    public LegacyConfigurationMigrator() {
        this(new MultiProviderConfigurationParser(), new MultiProviderConfigurationValidator());
    }

    /**
     * Creates a migrator with injected parser and validator.
     * <p>
     * Intended for testing, where a controlled (e.g. always-failing) validator can be supplied
     * to verify that the {@code .bak} backup is preserved when post-migration validation fails.
     *
     * @param parser    parser used to re-read the migrated file; must not be {@code null}
     * @param validator validator used to verify the migrated file; must not be {@code null}
     */
    public LegacyConfigurationMigrator(MultiProviderConfigurationParser parser,
                                        MultiProviderConfigurationValidator validator) {
        this.parser = parser;
        this.validator = validator;
    }

    /**
     * Migrates the configuration file at {@code configFilePath} if it is in legacy form.
     * <p>
     * If the file does not contain legacy flat keys or already contains
     * {@code ai.provider.active}, this method returns immediately without any I/O side effect.
     *
     * @param configFilePath path to the configuration file; must exist and be readable
     * @throws ConfigurationLoadingException if the file cannot be read, the backup cannot be
     *         created, the migrated file cannot be written, or post-migration validation fails
     */
    public void migrateIfLegacy(Path configFilePath) {
        String originalContent = readFile(configFilePath);
        Properties props = parsePropertiesFromContent(originalContent);

        if (!isLegacyForm(props)) {
            return;
        }

        LOG.info("Legacy configuration format detected. Migrating: {}", configFilePath);

        createBakBackup(configFilePath, originalContent);

        String migratedContent = generateMigratedContent(originalContent);
        writeAtomically(configFilePath, migratedContent);

        LOG.info("Configuration file migrated to multi-provider schema: {}", configFilePath);

        validateMigratedFile(configFilePath);
    }

    /**
     * Returns {@code true} if the given properties are in legacy form.
     * <p>
     * A properties set is considered legacy when it contains at least one of the four
     * flat legacy keys and does not already contain {@code ai.provider.active}.
     *
     * @param props the parsed properties to inspect; must not be {@code null}
     * @return {@code true} if migration is required, {@code false} otherwise
     */
    boolean isLegacyForm(Properties props) {
        boolean hasLegacyKey = props.containsKey(LEGACY_BASE_URL)
                || props.containsKey(LEGACY_MODEL)
                || props.containsKey(LEGACY_TIMEOUT)
                || props.containsKey(LEGACY_API_KEY);
        boolean hasNewKey = props.containsKey(MultiProviderConfigurationParser.PROP_ACTIVE_PROVIDER);
        return hasLegacyKey && !hasNewKey;
    }

    /**
     * Creates a backup of the original file before overwriting it.
     * <p>
     * If {@code <file>.bak} does not yet exist, it is written directly. Otherwise,
     * numbered suffixes ({@code .bak.1}, {@code .bak.2}, …) are tried in ascending order
     * until a free slot is found. Existing backups are never overwritten.
     */
    private void createBakBackup(Path configFilePath, String content) {
        Path bakPath = configFilePath.resolveSibling(configFilePath.getFileName() + ".bak");
        if (!Files.exists(bakPath)) {
            writeFile(bakPath, content);
            LOG.info("Backup created: {}", bakPath);
            return;
        }
        for (int i = 1; ; i++) {
            Path numbered = configFilePath.resolveSibling(configFilePath.getFileName() + ".bak." + i);
            if (!Files.exists(numbered)) {
                writeFile(numbered, content);
                LOG.info("Backup created: {}", numbered);
                return;
            }
        }
    }

    /**
     * Produces the migrated file content from the given original content string.
     * <p>
     * Each line is inspected: lines that define a legacy key are rewritten with the
     * corresponding new namespaced key; all other lines (comments, blank lines, other keys)
     * pass through unchanged. After all original lines, a {@code ai.provider.active} entry
     * and a commented Claude-provider placeholder block are appended.
     *
     * @param originalContent the raw original file content; must not be {@code null}
     * @return the migrated content ready to be written to disk
     */
    String generateMigratedContent(String originalContent) {
        String[] lines = originalContent.split("\\r?\\n", -1);
        StringBuilder sb = new StringBuilder();
        for (String line : lines) {
            sb.append(transformLine(line)).append("\n");
        }
        sb.append("\n");
        sb.append("# Aktiver KI-Provider: openai-compatible oder claude\n");
        sb.append("ai.provider.active=openai-compatible\n");
        sb.append("\n");
        sb.append("# Anthropic Claude-Provider (nur benoetigt wenn ai.provider.active=claude)\n");
        sb.append("# ai.provider.claude.model=\n");
        sb.append("# ai.provider.claude.timeoutSeconds=\n");
        sb.append("# ai.provider.claude.apiKey=\n");
        return sb.toString();
    }

    /**
     * Transforms a single properties-file line, replacing a legacy key with its new equivalent.
     * <p>
     * Comment lines, blank lines, and lines defining keys other than the four legacy keys
     * are returned unchanged.
     */
    private String transformLine(String line) {
        for (String[] mapping : LEGACY_KEY_MAPPINGS) {
            String legacyKey = mapping[0];
            String newKey = mapping[1];
            if (lineDefinesKey(line, legacyKey)) {
                int keyStart = line.indexOf(legacyKey);
                return line.substring(0, keyStart) + newKey + line.substring(keyStart + legacyKey.length());
            }
        }
        return line;
    }

    /**
     * Returns {@code true} when {@code line} defines the given {@code key}.
     * <p>
     * A line defines a key if — after stripping any leading whitespace — it starts with
     * the exact key string followed by {@code =}, {@code :}, whitespace, or end-of-string.
     * Comment-introducing characters ({@code #} or {@code !}) cause an immediate {@code false}.
     */
    private boolean lineDefinesKey(String line, String key) {
        String trimmed = line.stripLeading();
        if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("!")) {
            return false;
        }
        if (!trimmed.startsWith(key)) {
            return false;
        }
        if (trimmed.length() == key.length()) {
            return true;
        }
        char next = trimmed.charAt(key.length());
        return next == '=' || next == ':' || Character.isWhitespace(next);
    }

    /**
     * Writes {@code content} to {@code target} via a temporary file and an atomic rename.
     * <p>
     * The temporary file is created as {@code <target>.tmp} in the same directory.
     * After the content is fully written, the temporary file is moved to {@code target},
     * replacing it. The original file is therefore never partially overwritten.
     */
    private void writeAtomically(Path target, String content) {
        Path tmpPath = target.resolveSibling(target.getFileName() + ".tmp");
        try {
            Files.writeString(tmpPath, content, StandardCharsets.UTF_8);
            Files.move(tmpPath, target, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            throw new ConfigurationLoadingException(
                    "Failed to write migrated configuration to " + target, e);
        }
    }

    /**
     * Re-reads the migrated file and validates it using the injected parser and validator.
     * <p>
     * A parse or validation failure is treated as a hard startup error. The {@code .bak} backup
     * created before migration is preserved in this case.
     */
    private void validateMigratedFile(Path configFilePath) {
        String content = readFile(configFilePath);
        Properties props = parsePropertiesFromContent(content);

        MultiProviderConfiguration config;
        try {
            config = parser.parse(props);
        } catch (ConfigurationLoadingException e) {
            throw new ConfigurationLoadingException(
                    "Migrated configuration failed to parse: " + e.getMessage(), e);
        }

        try {
            validator.validate(config);
        } catch (InvalidStartConfigurationException e) {
            throw new ConfigurationLoadingException(
                    "Migrated configuration failed validation (backup preserved): " + e.getMessage(), e);
        }
    }

    private String readFile(Path path) {
        try {
            return Files.readString(path, StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new ConfigurationLoadingException("Failed to read file: " + path, e);
        }
    }

    private void writeFile(Path path, String content) {
        try {
            Files.writeString(path, content, StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new ConfigurationLoadingException("Failed to write file: " + path, e);
        }
    }

    private Properties parsePropertiesFromContent(String content) {
        Properties props = new Properties();
        try {
            props.load(new StringReader(content));
        } catch (IOException e) {
            throw new ConfigurationLoadingException("Failed to parse properties content", e);
        }
        return props;
    }
}
