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

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;

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

import de.gecheckt.pdf.umbenenner.application.port.out.RunLockHandle;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockPort;
import de.gecheckt.pdf.umbenenner.application.port.out.RunLockUnavailableException;

/**
 * Dateibasierte Implementierung von {@link RunLockPort}.
 * <p>
 * Verwendet eine Lock-Datei, um parallele Läufe zu verhindern.
 * Beim Erwerb wird die Lock-Datei angelegt; bei der Freigabe wird sie gelöscht.
 * Existiert die Datei bereits, ist der Lock belegt.
 * <p>
 * Die Lock-Datei enthält die PID des erwerbenden Prozesses.
 * Die Freigabe ist best-effort: Ein Fehler beim Löschen wird als Warnung
 * geloggt, wirft aber keine Ausnahme.
 */
public class FilesystemRunLockPortAdapter implements RunLockPort {

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

    private final Path lockFile;

    /**
     * Erstellt einen neuen {@code FilesystemRunLockPortAdapter} für den
     * angegebenen Lock-Datei-Pfad.
     *
     * @param lockFile Pfad der Lock-Datei, die beim Erwerb angelegt und
     *                 bei der Freigabe gelöscht wird
     */
    public FilesystemRunLockPortAdapter(Path lockFile) {
        this.lockFile = lockFile;
    }

    /**
     * Erwirbt den Run-Lock durch Anlegen der Lock-Datei (blockierend).
     * <p>
     * Existiert die Lock-Datei bereits, wird eine
     * {@link RunLockUnavailableException} geworfen. Das übergeordnete
     * Verzeichnis wird bei Bedarf angelegt.
     *
     * @throws RunLockUnavailableException wenn die Lock-Datei bereits existiert
     *         oder nicht angelegt werden kann
     */
    @Override
    public void acquire() {
        if (Files.exists(lockFile)) {
            throw new RunLockUnavailableException(
                    "Run-Lock-Datei existiert bereits – eine andere Instanz könnte laufen: " + lockFile);
        }
        try {
            Path parent = lockFile.getParent();
            if (parent != null) {
                Files.createDirectories(parent);
            }
            long pid = ProcessHandle.current().pid();
            Files.writeString(lockFile, String.valueOf(pid), StandardOpenOption.CREATE_NEW);
            LOG.debug("Run-Lock erworben: {} (PID {})", lockFile, pid);
        } catch (IOException e) {
            throw new RunLockUnavailableException("Run-Lock-Datei konnte nicht angelegt werden: " + lockFile, e);
        }
    }

    /**
     * Gibt den Run-Lock durch Löschen der Lock-Datei frei.
     * <p>
     * Schlägt das Löschen fehl, wird eine Warnung geloggt; keine Ausnahme
     * wird geworfen.
     */
    @Override
    public void release() {
        try {
            boolean deleted = Files.deleteIfExists(lockFile);
            if (deleted) {
                LOG.debug("Run-Lock freigegeben: {}", lockFile);
            }
        } catch (IOException e) {
            LOG.warn("Run-Lock-Datei konnte nicht gelöscht werden: {} – manuelle Bereinigung erforderlich",
                    lockFile, e);
        }
    }

    /**
     * Versucht nicht-blockierend, den Run-Lock zu erwerben.
     * <p>
     * Existiert die Lock-Datei bereits, wird sofort {@link Optional#empty()}
     * zurückgegeben. Andernfalls wird die Datei atomar mit
     * {@link StandardOpenOption#CREATE_NEW} angelegt. Schlägt das Anlegen
     * aufgrund einer Race-Condition fehl (z.B. gleichzeitiger Erwerb durch
     * eine andere Instanz), wird ebenfalls {@link Optional#empty()} zurückgegeben.
     * <p>
     * Das zurückgegebene {@link RunLockHandle} gibt den Lock idempotent frei.
     *
     * @return Handle mit dem erworbenen Lock, oder {@link Optional#empty()}
     *         wenn der Lock nicht verfügbar ist
     */
    @Override
    public Optional<RunLockHandle> tryAcquire() {
        if (Files.exists(lockFile)) {
            LOG.debug("Run-Lock nicht verfügbar (Datei existiert): {}", lockFile);
            return Optional.empty();
        }
        try {
            Path parent = lockFile.getParent();
            if (parent != null) {
                Files.createDirectories(parent);
            }
            long pid = ProcessHandle.current().pid();
            Files.writeString(lockFile, String.valueOf(pid), StandardOpenOption.CREATE_NEW);
            LOG.debug("Run-Lock (tryAcquire) erworben: {} (PID {})", lockFile, pid);
            return Optional.of(new FilesystemRunLockHandle());
        } catch (IOException e) {
            // CREATE_NEW schlägt mit FileAlreadyExistsException fehl wenn eine
            // Race-Condition vorliegt – kein Fehler, sondern normaler Busy-Zustand
            LOG.debug("Run-Lock (tryAcquire) nicht verfügbar: {} – {}", lockFile, e.getMessage());
            return Optional.empty();
        }
    }

    /**
     * Handle für einen über {@link #tryAcquire()} erworbenen Run-Lock.
     * <p>
     * Gibt den Lock idempotent frei. Mehrfaches Aufrufen von {@link #close()}
     * hat nach dem ersten Aufruf keine Wirkung.
     */
    private class FilesystemRunLockHandle implements RunLockHandle {

        private final AtomicBoolean released = new AtomicBoolean(false);

        @Override
        public void close() {
            if (released.compareAndSet(false, true)) {
                FilesystemRunLockPortAdapter.this.release();
            }
        }
    }
}
