package de.gecheckt.pdf.umbenenner.bootstrap.adapter;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

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

import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationFileWriter;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationSaveResult;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.GuiConfigurationWriteException;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiConfigurationValues;
import de.gecheckt.pdf.umbenenner.adapter.in.gui.editor.GuiProviderConfigurationState;
import de.gecheckt.pdf.umbenenner.application.config.provider.AiProviderFamily;

/**
 * Writes a normalized {@code .properties} file from the current GUI editor values.
 * <p>
 * This adapter implements the {@link GuiConfigurationFileWriter} port and is wired by
 * Bootstrap. It performs two main responsibilities:
 * <ol>
 *   <li>Creates a {@code .bak} backup of any existing file before overwriting it, using
 *       the same rotation schema as the legacy configuration migrator:
 *       {@code <filename>.bak}, and on collision {@code .bak.1}, {@code .bak.2}, …
 *       Existing backups are never overwritten.</li>
 *   <li>Writes the editor values as a canonically ordered, grouped and commented
 *       {@code .properties} file via a temporary file and an atomic rename, so the
 *       existing file is never partially overwritten.</li>
 * </ol>
 * <p>
 * API-key preservation logic (detecting empty fields with a non-empty baseline) is handled
 * by the caller (workspace) before invoking this writer. The writer simply serializes the
 * values it receives.
 *
 * <h2>Normalized output order</h2>
 * <pre>{@code
 * # Provider
 * ai.provider.active=...
 * # Claude
 * ai.provider.claude.*
 * # OpenAI-kompatibel
 * ai.provider.openai-compatible.*
 * # Pfade
 * source.folder=...
 * target.folder=...
 * sqlite.file=...
 * # Verarbeitung
 * max.retries.transient=...
 * max.pages=...
 * max.text.characters=...
 * max.title.length=...
 * prompt.template.file=...
 * # Logging
 * log.ai.sensitive=...
 * log.directory=...
 * log.level=...
 * # Laufzeit
 * runtime.lock.file=...
 * }</pre>
 */
public final class GuiConfigurationPropertiesWriter implements GuiConfigurationFileWriter {

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

    /**
     * Creates a new properties writer.
     */
    public GuiConfigurationPropertiesWriter() {
        // intentionally empty – no initialization required
    }

    /**
     * Writes the editor values to the target path as a normalized {@code .properties} file.
     * <p>
     * When the target file already exists, a backup is created before the file is overwritten.
     * The write is performed via a temporary file followed by an atomic rename.
     *
     * <p><strong>Threading contract:</strong> This method performs blocking file-system I/O
     * ({@link java.nio.file.Files#exists}, backup copy, directory creation, file write, atomic
     * move). It must be invoked from a background worker thread. It must never be called from
     * the JavaFX Application Thread.
     *
     * @param values     the current editor values; must not be {@code null}
     * @param targetPath the file to write; must not be {@code null}
     * @return the save result containing the written path
     * @throws GuiConfigurationWriteException if the file cannot be written
     */
    @Override
    public GuiConfigurationSaveResult write(GuiConfigurationValues values, Path targetPath) {
        if (Files.exists(targetPath)) {
            createBakBackup(targetPath);
        }

        String content = buildPropertiesContent(values);
        writeAtomically(targetPath, content);

        LOG.info("Konfigurationsdatei geschrieben: {}", targetPath.toAbsolutePath());
        return GuiConfigurationSaveResult.saved(targetPath);
    }

    /**
     * Creates a rotating backup of the file at the given path.
     * <p>
     * The first backup uses the suffix {@code .bak}. When that file already exists,
     * numbered suffixes are tried in ascending order ({@code .bak.1}, {@code .bak.2}, …)
     * until a free slot is found. Existing backups are never overwritten.
     *
     * @param targetPath the file to back up; must exist
     * @throws GuiConfigurationWriteException if the backup cannot be created
     */
    void createBakBackup(Path targetPath) {
        Path bakPath = targetPath.resolveSibling(targetPath.getFileName() + ".bak");
        if (!Files.exists(bakPath)) {
            copyFile(targetPath, bakPath);
            LOG.info("Sicherungskopie erstellt: {}", bakPath);
            return;
        }
        final int MAX_BACKUP_ATTEMPTS = 1000;
        for (int i = 1; i <= MAX_BACKUP_ATTEMPTS; i++) {
            Path numbered = targetPath.resolveSibling(targetPath.getFileName() + ".bak." + i);
            if (!Files.exists(numbered)) {
                copyFile(targetPath, numbered);
                LOG.info("Sicherungskopie erstellt: {}", numbered);
                return;
            }
        }
        throw new GuiConfigurationWriteException(
                "Maximale Anzahl an .bak-Sicherungen (1000) für '" + targetPath + "' erreicht");
    }

    /**
     * Writes the content to the target path via a temporary file and an atomic rename.
     *
     * @param target  the destination path; must not be {@code null}
     * @param content the content to write; must not be {@code null}
     * @throws GuiConfigurationWriteException if the file cannot be written
     */
    private void writeAtomically(Path target, String content) {
        Path tmpPath = target.resolveSibling(target.getFileName() + ".tmp");
        try {
            Path parentDir = target.getParent();
            if (parentDir != null) {
                Files.createDirectories(parentDir);
            }
            Files.writeString(tmpPath, content, StandardCharsets.UTF_8);
            Files.move(tmpPath, target, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            throw new GuiConfigurationWriteException(
                    "Konfigurationsdatei konnte nicht geschrieben werden: " + target, e);
        }
    }

    private void copyFile(Path source, Path destination) {
        try {
            Files.copy(source, destination);
        } catch (AccessDeniedException e) {
            throw new GuiConfigurationWriteException(
                    "Konfiguration kann nicht gespeichert werden – Scheduler läuft.", e);
        } catch (IOException e) {
            throw new GuiConfigurationWriteException(
                    "Sicherungskopie konnte nicht erstellt werden: " + destination, e);
        }
    }

    /**
     * Builds the normalized {@code .properties} file content from the given values.
     *
     * @param values the values to serialize; must not be {@code null}
     * @return the complete file content as a UTF-8 string
     */
    String buildPropertiesContent(GuiConfigurationValues values) {
        StringBuilder sb = new StringBuilder();

        appendLine(sb, "# Aktiver KI-Provider (claude oder openai-compatible)");
        appendKeyValue(sb, "ai.provider.active", values.activeProviderFamily());
        appendLine(sb, "");

        appendLine(sb, "# Provider-Konfiguration: Claude");
        GuiProviderConfigurationState claude = values.providerConfiguration(AiProviderFamily.CLAUDE);
        if (claude != null) {
            appendKeyValue(sb, "ai.provider.claude.baseUrl", claude.baseUrl());
            appendKeyValue(sb, "ai.provider.claude.model", claude.model());
            appendKeyValue(sb, "ai.provider.claude.timeoutSeconds", claude.timeoutSeconds());
            appendKeyValue(sb, "ai.provider.claude.apiKey", claude.apiKey().propertyValue());
        }
        appendLine(sb, "");

        appendLine(sb, "# Provider-Konfiguration: OpenAI-kompatibel");
        GuiProviderConfigurationState openai = values.providerConfiguration(AiProviderFamily.OPENAI_COMPATIBLE);
        if (openai != null) {
            appendKeyValue(sb, "ai.provider.openai-compatible.baseUrl", openai.baseUrl());
            appendKeyValue(sb, "ai.provider.openai-compatible.model", openai.model());
            appendKeyValue(sb, "ai.provider.openai-compatible.timeoutSeconds", openai.timeoutSeconds());
            appendKeyValue(sb, "ai.provider.openai-compatible.apiKey", openai.apiKey().propertyValue());
        }
        appendLine(sb, "");

        appendLine(sb, "# Pfade");
        appendKeyValue(sb, "source.folder", values.sourceFolder());
        appendKeyValue(sb, "target.folder", values.targetFolder());
        appendKeyValue(sb, "sqlite.file", values.sqliteFile());
        appendLine(sb, "");

        appendLine(sb, "# Verarbeitung");
        appendKeyValue(sb, "max.retries.transient", values.maxRetriesTransient());
        appendKeyValue(sb, "max.pages", values.maxPages());
        appendKeyValue(sb, "max.text.characters", values.maxTextCharacters());
        appendKeyValue(sb, "max.title.length", values.maxTitleLength());
        appendKeyValue(sb, "prompt.template.file", values.promptTemplateFile());
        appendLine(sb, "");

        appendLine(sb, "# Logging");
        appendKeyValue(sb, "log.ai.sensitive", values.logAiSensitive());
        appendKeyValue(sb, "log.directory", values.logDirectory());
        appendKeyValue(sb, "log.level", values.logLevel());
        appendLine(sb, "");

        appendLine(sb, "# Laufzeit");
        appendKeyValue(sb, "runtime.lock.file", values.runtimeLockFile());

        return sb.toString();
    }

    private static void appendLine(StringBuilder sb, String line) {
        sb.append(line).append("\n");
    }

    private static void appendKeyValue(StringBuilder sb, String key, String value) {
        sb.append(key).append("=").append(value == null ? "" : value).append("\n");
    }
}
