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

import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Prozessweiter Einzelinstanz-Schutz auf Basis eines Loopback-ServerSocket-Binds.
 * <p>
 * Beim Aufruf von {@link #acquire()} wird versucht, einen {@link ServerSocket} an die
 * Loopback-Adresse ({@code 127.0.0.1}) auf dem konfigurierten Port zu binden. Gelingt das
 * Binden, läuft noch keine weitere Instanz der Anwendung und die Guard-Instanz hält den
 * Port für die Lebensdauer des Prozesses. Schlägt das Binden mit einer
 * {@link BindException} fehl, ist bereits eine andere Instanz aktiv und es wird eine
 * {@link AnotherInstanceRunningException} geworfen.
 * <p>
 * Vorteile gegenüber dateibasiertem Locking:
 * <ul>
 *   <li>Kein veralteter Lockzustand: Das Betriebssystem gibt den Port automatisch frei,
 *       sobald der Prozess endet.</li>
 *   <li>Keine Dateisystem-Abhängigkeit: funktioniert zuverlässig auf Windows mit
 *       gemappten Laufwerken und UNC-Pfaden.</li>
 * </ul>
 * <p>
 * Die Klasse ist {@link AutoCloseable}. Im Normalfall ist ein explizites {@link #close()}
 * nicht erforderlich, da ein registrierter Shutdown-Hook den Socket beim JVM-Ende
 * automatisch schließt. Innerhalb eines {@code try}-with-resources oder an einem
 * programmatischen Endpunkt kann {@code close()} jedoch vorzeitig aufgerufen werden,
 * damit der Port sofort wieder verfügbar ist. Mehrfaches Aufrufen von {@code close()}
 * ist sicher und ohne Effekt.
 * <p>
 * Der Guard ist nicht für parallele Aufrufe ausgelegt; er soll genau einmal pro Prozess
 * aufgerufen werden.
 */
public class SingleInstanceGuard implements AutoCloseable {

    /**
     * Standardport für die Einzelinstanz-Sicherung ({@value}).
     * <p>
     * Nur auf der Loopback-Adresse gebunden und damit nicht extern sichtbar.
     */
    public static final int DEFAULT_PORT = 47832;

    private final int port;
    private final AtomicReference<ServerSocket> socket = new AtomicReference<>();
    private final AtomicBoolean closed = new AtomicBoolean(false);

    /**
     * Erstellt einen Guard, der den Standard-Port {@link #DEFAULT_PORT} verwendet.
     */
    public SingleInstanceGuard() {
        this(DEFAULT_PORT);
    }

    /**
     * Erstellt einen Guard, der den angegebenen Port verwendet.
     * <p>
     * Dieser Konstruktor ist primär für Tests gedacht, damit parallele Testläufe
     * keine Portkonflikte erzeugen.
     *
     * @param port der zu verwendende TCP-Port; muss im gültigen Bereich 0–65535 liegen
     */
    public SingleInstanceGuard(int port) {
        this.port = port;
    }

    /**
     * Aktiviert die Einzelinstanz-Sicherung.
     * <p>
     * Bindet einen {@link ServerSocket} an {@code 127.0.0.1} auf dem konfigurierten Port.
     * Registriert einen Shutdown-Hook, der den Socket beim JVM-Ende automatisch schließt,
     * damit der Port auch bei einem unerwarteten Prozessende freigegeben wird.
     *
     * @throws AnotherInstanceRunningException wenn der Port bereits belegt ist, d. h. eine
     *                                         weitere Instanz der Anwendung läuft
     * @throws IllegalStateException           wenn {@code acquire()} auf einer bereits
     *                                         geschlossenen Guard-Instanz aufgerufen wird
     */
    public void acquire() {
        if (closed.get()) {
            throw new IllegalStateException("SingleInstanceGuard wurde bereits geschlossen.");
        }
        try {
            InetAddress loopback = InetAddress.getLoopbackAddress();
            ServerSocket serverSocket = new ServerSocket(port, 1, loopback);
            this.socket.set(serverSocket);
            Runtime.getRuntime().addShutdownHook(new Thread(() -> schliesseSilent(serverSocket),
                    "single-instance-guard-shutdown"));
        } catch (BindException e) {
            throw new AnotherInstanceRunningException(
                    "Es läuft bereits eine Instanz der Anwendung (Port " + port + " belegt).", e);
        } catch (IOException e) {
            throw new AnotherInstanceRunningException(
                    "Einzelinstanz-Prüfung fehlgeschlagen: " + e.getMessage(), e);
        }
    }

    /**
     * Gibt die Einzelinstanz-Sicherung frei und schließt den gebundenen Socket.
     * <p>
     * Nach dem Schließen ist der Port sofort wieder für eine neue Instanz verfügbar.
     * Mehrfaches Aufrufen hat keinen weiteren Effekt.
     */
    @Override
    public void close() {
        if (closed.compareAndSet(false, true)) {
            schliesseSilent(socket.get());
        }
    }

    private static void schliesseSilent(ServerSocket s) {
        if (s != null && !s.isClosed()) {
            try {
                s.close();
            } catch (IOException ignored) {
                // Fehler beim Schließen werden bewusst ignoriert
            }
        }
    }
}
