package com.petshop.backend;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.IntFunction;
import java.util.function.LongConsumer;
import java.util.function.LongFunction;

final class PortCleanupSupport {

    private final IntFunction<Optional<Long>> pidLookup;
    private final LongFunction<Optional<String>> commandLineLookup;
    private final LongConsumer terminator;

    PortCleanupSupport() {
        this(PortCleanupSupport::findListeningPid, PortCleanupSupport::findCommandLine, PortCleanupSupport::terminateProcess);
    }

    PortCleanupSupport(IntFunction<Optional<Long>> pidLookup, LongFunction<Optional<String>> commandLineLookup, LongConsumer terminator) {
        this.pidLookup = pidLookup;
        this.commandLineLookup = commandLineLookup;
        this.terminator = terminator;
    }

    boolean cleanupIdentifiableProcessOnPort(int port) {
        Optional<Long> pid = pidLookup.apply(port);
        if (pid.isEmpty()) {
            return false;
        }

        Optional<String> commandLine = commandLineLookup.apply(pid.get());
        if (commandLine.isEmpty() || !isIdentifiableBackendCommand(commandLine.get())) {
            return false;
        }

        terminator.accept(pid.get());
        return true;
    }

    private boolean isIdentifiableBackendCommand(String commandLine) {
        String normalized = commandLine.toLowerCase(Locale.ROOT);
        return normalized.contains("com.petshop.backend.backendapplication")
                || normalized.contains("com.petshop.backend.devstackapplication")
                || normalized.contains("exec:java@dev-stack")
                || normalized.contains("group-2-threaded-project-petshop/backend")
                || normalized.contains("/backend/target/classes")
                || normalized.contains("com.petshop:backend");
    }

    private static Optional<Long> findListeningPid(int port) {
        ProcessBuilder builder = buildPortLookupCommand(port);
        builder.redirectErrorStream(true);
        try {
            Process process = builder.start();
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line = firstNonBlankLine(reader);
                int exitCode = process.waitFor();
                if (exitCode != 0 || line == null || line.isBlank()) {
                    return Optional.empty();
                }
                return Optional.of(parsePid(line));
            }
        } catch (IOException | InterruptedException | NumberFormatException ex) {
            if (ex instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            return Optional.empty();
        }
    }

    private static Optional<String> findCommandLine(long pid) {
        return ProcessHandle.of(pid)
                .flatMap(handle -> handle.info().commandLine());
    }

    private static void terminateProcess(long pid) {
        ProcessHandle.of(pid).ifPresent(handle -> {
            handle.destroy();
            try {
                handle.onExit().get(5, TimeUnit.SECONDS);
            } catch (TimeoutException ex) {
                handle.destroyForcibly();
            } catch (Exception ex) {
                handle.destroyForcibly();
            }
        });
    }

    private static ProcessBuilder buildPortLookupCommand(int port) {
        String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
        if (os.contains("win")) {
            return new ProcessBuilder("cmd", "/c", "netstat -ano -p tcp | findstr LISTENING | findstr :" + port);
        }
        return new ProcessBuilder("lsof", "-tiTCP:" + port, "-sTCP:LISTEN");
    }

    private static String firstNonBlankLine(BufferedReader reader) throws IOException {
        String line;
        while ((line = reader.readLine()) != null) {
            if (!line.isBlank()) {
                return line.trim();
            }
        }
        return null;
    }

    private static long parsePid(String line) {
        String trimmed = line.trim();
        int lastSpace = trimmed.lastIndexOf(' ');
        String pidToken = lastSpace >= 0 ? trimmed.substring(lastSpace + 1) : trimmed;
        return Long.parseLong(pidToken);
    }
}
