package de.frigosped.dc.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import org.jsoup.Jsoup;

import java.io.InputStream;
import java.net.InetAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * Lädt externe Referenz-Dokumente sicher herunter und extrahiert deren Text.
 *
 * Sicherheit (SSRF): Es werden ausschließlich öffentlich auflösbare Domains
 * akzeptiert. Der Hostname wird per DNS aufgelöst und jede resultierende IP
 * gegen private/Loopback/Link-Local/CGNAT/ULA-Bereiche geprüft. Redirects
 * werden manuell verfolgt und bei jedem Hop neu validiert. Größe und Timeout
 * sind begrenzt.
 *
 * Text-Extraktion:
 *   - PDF/DOCX/ODT/Plain → {@link DocumentProcessingService#extractText}
 *   - text/html          → jsoup → Text
 */
@ApplicationScoped
public class ReferenceDownloadService {

    private static final Logger LOG = Logger.getLogger(ReferenceDownloadService.class);
    private static final int MAX_REDIRECTS = 5;

    @Inject
    DocumentProcessingService documentProcessingService;

    @ConfigProperty(name = "dc.references.enabled", defaultValue = "true")
    boolean enabled;

    @ConfigProperty(name = "dc.references.max-size-bytes", defaultValue = "15000000")
    long maxSizeBytes;

    @ConfigProperty(name = "dc.references.timeout", defaultValue = "30s")
    Duration timeout;

    @ConfigProperty(name = "dc.references.allowed-content-types",
            defaultValue = "application/pdf,"
                    + "application/vnd.openxmlformats-officedocument.wordprocessingml.document,"
                    + "application/msword,"
                    + "application/vnd.oasis.opendocument.text,"
                    + "text/plain,text/markdown,text/html")
    String allowedContentTypes;

    /** Ergebnis eines erfolgreichen Referenz-Downloads. */
    public record DownloadedReference(
            byte[] bytes, String mimeType, String text, String filename, String sourceUrl) {}

    public boolean isEnabled() {
        return enabled;
    }

    /**
     * Lädt eine URL herunter und extrahiert den Text. Bei jedem Fehler (ungültige
     * URL, SSRF-Blockade, Timeout, zu groß, nicht erlaubter Typ, leerer Text) wird
     * {@link Optional#empty()} zurückgegeben – die Verarbeitung läuft weiter.
     */
    public Optional<DownloadedReference> fetch(String rawUrl, Long projectId) {
        if (!enabled) return Optional.empty();

        String url = normalize(rawUrl);
        if (url == null) {
            LOG.debugf("Referenz übersprungen – ungültige URL: %s", rawUrl);
            return Optional.empty();
        }

        try {
            HttpClient client = HttpClient.newBuilder()
                    .followRedirects(HttpClient.Redirect.NEVER)
                    .connectTimeout(timeout)
                    .build();

            URI current = URI.create(url);
            HttpResponse<InputStream> response = null;

            for (int hop = 0; hop <= MAX_REDIRECTS; hop++) {
                if (!isPublicHost(current.getHost())) {
                    LOG.warnf("Referenz blockiert (SSRF-Schutz): %s", current);
                    return Optional.empty();
                }

                HttpRequest request = HttpRequest.newBuilder(current)
                        .timeout(timeout)
                        .header("User-Agent", "Frigosped-DocumentCheck/1.0")
                        .GET()
                        .build();

                response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
                int status = response.statusCode();

                if (status >= 300 && status < 400) {
                    Optional<String> loc = response.headers().firstValue("location");
                    if (loc.isEmpty()) break;
                    current = current.resolve(loc.get());   // relativ oder absolut
                    response.body().close();
                    continue;
                }
                break;
            }

            if (response == null || response.statusCode() != 200) {
                LOG.debugf("Referenz übersprungen – HTTP %s: %s",
                        response == null ? "?" : response.statusCode(), url);
                return Optional.empty();
            }

            String contentType = response.headers().firstValue("content-type")
                    .map(ct -> ct.split(";")[0].trim().toLowerCase())
                    .orElse("application/octet-stream");

            if (!isAllowedType(contentType)) {
                LOG.debugf("Referenz übersprungen – Content-Type %s nicht erlaubt: %s",
                        contentType, url);
                response.body().close();
                return Optional.empty();
            }

            byte[] bytes = readLimited(response.body(), maxSizeBytes);
            if (bytes == null) {
                LOG.warnf("Referenz übersprungen – größer als %d Bytes: %s", maxSizeBytes, url);
                return Optional.empty();
            }

            String filename = deriveFilename(current, contentType);
            String text = extract(bytes, contentType, filename, current.toString(), projectId);

            if (text == null || text.isBlank()) {
                LOG.debugf("Referenz übersprungen – kein Text extrahierbar: %s", url);
                return Optional.empty();
            }

            LOG.infof("Referenz geladen: %s (%s, %d Bytes, %d Zeichen)",
                    url, contentType, bytes.length, text.length());
            return Optional.of(new DownloadedReference(bytes, contentType, text, filename, url));

        } catch (Exception e) {
            LOG.debugf("Referenz-Download fehlgeschlagen (%s): %s", url, e.getMessage());
            return Optional.empty();
        }
    }

    // =========================================================================
    // Text-Extraktion
    // =========================================================================

    private String extract(byte[] bytes, String contentType, String filename,
                           String url, Long projectId) {
        if (contentType.equals("text/html")) {
            String html = new String(bytes, StandardCharsets.UTF_8);
            return Jsoup.parse(html, url).text();
        }
        return documentProcessingService.extractText(bytes, contentType, filename, projectId);
    }

    // =========================================================================
    // SSRF-Schutz
    // =========================================================================

    /**
     * true, wenn der Host öffentlich auflösbar ist und KEINE der aufgelösten
     * IP-Adressen in einem privaten/internen Bereich liegt.
     */
    boolean isPublicHost(String host) {
        if (host == null || host.isBlank()) return false;
        try {
            InetAddress[] addresses = InetAddress.getAllByName(host);
            if (addresses.length == 0) return false;
            for (InetAddress addr : addresses) {
                if (!isPublicAddress(addr)) return false;
            }
            return true;
        } catch (Exception e) {
            return false;   // nicht auflösbar → blockieren
        }
    }

    private boolean isPublicAddress(InetAddress addr) {
        if (addr.isLoopbackAddress() || addr.isAnyLocalAddress()
                || addr.isLinkLocalAddress() || addr.isSiteLocalAddress()
                || addr.isMulticastAddress()) {
            return false;
        }
        byte[] b = addr.getAddress();
        if (b.length == 4) {
            int o0 = b[0] & 0xFF, o1 = b[1] & 0xFF;
            if (o0 == 0)   return false;                         // 0.0.0.0/8
            if (o0 == 127) return false;                         // Loopback (Sicherheit)
            if (o0 == 100 && o1 >= 64 && o1 <= 127) return false; // CGNAT 100.64.0.0/10
            if (o0 == 169 && o1 == 254) return false;            // Link-Local
        } else if (b.length == 16) {
            if ((b[0] & 0xFE) == 0xFC) return false;             // IPv6 ULA fc00::/7
        }
        return true;
    }

    // =========================================================================
    // Hilfsmethoden
    // =========================================================================

    /** Normalisiert eine vom LLM gelieferte URL (trimmt Markdown/Satzzeichen). */
    private String normalize(String raw) {
        if (raw == null) return null;
        String u = raw.trim();
        if (u.startsWith("<") && u.endsWith(">")) u = u.substring(1, u.length() - 1).trim();
        // typische nachgestellte Satzzeichen entfernen
        while (!u.isEmpty() && ").,;\"'".indexOf(u.charAt(u.length() - 1)) >= 0) {
            u = u.substring(0, u.length() - 1);
        }
        if (!u.regionMatches(true, 0, "http://", 0, 7)
                && !u.regionMatches(true, 0, "https://", 0, 8)) {
            return null;
        }
        return u;
    }

    /** Liest den Stream bis zum Limit; gibt null zurück wenn das Limit überschritten wird. */
    private byte[] readLimited(InputStream in, long limit) throws Exception {
        try (in) {
            java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
            byte[] buf = new byte[8192];
            long total = 0;
            int n;
            while ((n = in.read(buf)) != -1) {
                total += n;
                if (total > limit) return null;
                out.write(buf, 0, n);
            }
            return out.toByteArray();
        }
    }

    private boolean isAllowedType(String contentType) {
        List<String> allowed = Arrays.stream(allowedContentTypes.split(","))
                .map(String::trim).map(String::toLowerCase).toList();
        return allowed.contains(contentType);
    }

    private String deriveFilename(URI uri, String contentType) {
        String path = uri.getPath();
        String name = null;
        if (path != null && !path.isBlank()) {
            int slash = path.lastIndexOf('/');
            String last = slash >= 0 ? path.substring(slash + 1) : path;
            if (!last.isBlank()) name = last;
        }
        if (name == null || name.isBlank()) {
            name = (uri.getHost() != null ? uri.getHost() : "referenz") + extensionFor(contentType);
        } else if (!name.contains(".")) {
            name = name + extensionFor(contentType);
        }
        // Dateinamen säubern
        return name.replaceAll("[^A-Za-z0-9_äöüÄÖÜß.-]", "_");
    }

    private String extensionFor(String contentType) {
        return switch (contentType) {
            case "application/pdf" -> ".pdf";
            case "application/vnd.openxmlformats-officedocument.wordprocessingml.document" -> ".docx";
            case "application/msword" -> ".doc";
            case "application/vnd.oasis.opendocument.text" -> ".odt";
            case "text/html" -> ".html";
            case "text/markdown" -> ".md";
            default -> ".txt";
        };
    }
}
