import de.vertico.starface.module.core.model.VariableType;
import de.vertico.starface.module.core.model.Visibility;
import de.vertico.starface.module.core.runtime.IBaseExecutable;
import de.vertico.starface.module.core.runtime.IRuntimeEnvironment;
import de.vertico.starface.module.core.runtime.annotations.Function;
import de.vertico.starface.module.core.runtime.annotations.InputVar;
import de.vertico.starface.module.core.runtime.annotations.OutputVar;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

/**
 * BlacklistBlock - STARFACE Custom Block
 *
 * Prüft bei jedem eingehenden Anruf die Anrufernummer gegen eine konfigurierbare
 * Blacklist und führt eine wählbare Aktion aus (auflegen, besetzt, umleiten, Ansage).
 * Anonyme Anrufe (unterdrückte Rufnummer) werden separat behandelt.
 *
 * Das Modul muss als Typ "Call-Processing" mit Aktivierung "bei allen eingehenden
 * Anrufen" konfiguriert werden, damit der Block im Anruf-Kontext läuft.
 *
 * Für STARFACE 8.x, 9.x, 10.x (Java 21)
 */
@Function(
    visibility = Visibility.Public,
    modifiesCalls = true,
    description = "Prüft eingehende Anrufe gegen eine Blacklist und behandelt anonyme Anrufe"
)
public class BlacklistBlock implements IBaseExecutable {

    // Voll qualifizierte Namen der STARFACE-internen Blöcke (per Reflection genutzt,
    // damit der Block über STARFACE-Versionen hinweg robust bleibt).
    private static final String CN_AGI_RUNTIME =
        "de.vertico.starface.module.core.runtime.IAGIRuntimeEnvironment";
    private static final String CN_GET_CALLER =
        "de.vertico.starface.module.core.runtime.functions.callHandling.call.GetCaller2";
    private static final String CN_HANGUP =
        "de.vertico.starface.module.core.runtime.functions.callHandling.call.Hangup";
    private static final String CN_BUSY =
        "de.vertico.starface.module.core.runtime.functions.callHandling.call.Busy";
    private static final String CN_CALL_NUMBER =
        "de.vertico.starface.module.core.runtime.functions.callHandling.call.CallPhonenumber2";
    private static final String CN_ANSWER =
        "de.vertico.starface.module.core.runtime.functions.callHandling.call.Answer";
    private static final String CN_PLAYBACK =
        "de.vertico.starface.module.core.runtime.functions.callHandling.audio.PlaybackResourceFile2";

    // ============================================================
    // INPUT VARIABLEN - werden im Module Designer konfiguriert
    // ============================================================

    @InputVar(
        label = "Blacklist-Einträge",
        description = "Eine Nummer pro Zeile (oder durch Komma getrennt). Pro Zeile optional eine "
            + "eigene Aktion und ein Ziel im Format: nummer;AKTION;ziel\n"
            + "Beispiele:\n"
            + "+49301234567            -> Standard-Aktion\n"
            + "0190;BUSY               -> dieser Eintrag immer besetzt\n"
            + "004989;REDIRECT;100     -> auf Nebenstelle 100 umleiten\n"
            + "+4972112345;ALLOW       -> Ausnahme (durchlassen)\n"
            + "AKTIONEN: HANGUP, BUSY, REDIRECT, ANNOUNCE, ALLOW. "
            + "Zeilen mit '#' am Anfang sind Kommentare.",
        type = VariableType.STRING,
        guiHints = { "multiline" }
    )
    public String blacklistEntries = "";

    @InputVar(
        label = "Standard-Aktion (Blacklist)",
        description = "Aktion für Blacklist-Einträge ohne eigene Aktion. "
            + "HANGUP = auflegen, BUSY = besetzt, REDIRECT = umleiten, ANNOUNCE = Ansage + auflegen",
        type = VariableType.STRING,
        possibleValues = { "HANGUP", "BUSY", "REDIRECT", "ANNOUNCE" }
    )
    public String defaultAction = "HANGUP";

    @InputVar(
        label = "Standard-Umleitungsziel",
        description = "Zielrufnummer/Nebenstelle für REDIRECT-Einträge ohne eigenes Ziel (z.B. 100 oder +4972199999)",
        type = VariableType.STRING
    )
    public String defaultRedirectTarget = "";

    @InputVar(
        label = "Ansage-Datei",
        description = "Name der hochgeladenen Sound-Ressource für die Aktion ANNOUNCE (ohne Pfad). "
            + "Die Datei muss als Ressource im Modul hinterlegt sein.",
        type = VariableType.STRING
    )
    public String announcementFile = "";

    @InputVar(
        label = "Aktion bei anonymen Anrufen",
        description = "Behandlung von Anrufen mit unterdrückter Rufnummer. "
            + "NONE = nichts tun (Anruf normal durchstellen), sonst HANGUP/BUSY/REDIRECT/ANNOUNCE",
        type = VariableType.STRING,
        possibleValues = { "NONE", "HANGUP", "BUSY", "REDIRECT", "ANNOUNCE" }
    )
    public String anonymousAction = "NONE";

    @InputVar(
        label = "Umleitungsziel für anonyme Anrufe",
        description = "Zielrufnummer/Nebenstelle, falls die Aktion für anonyme Anrufe REDIRECT ist",
        type = VariableType.STRING
    )
    public String anonymousRedirectTarget = "";

    @InputVar(
        label = "Ländervorwahl",
        description = "Eigene Ländervorwahl ohne führende Null/Plus (Deutschland = 49). "
            + "Wird genutzt, um nationale Nummern (0721...) und internationale (+49721...) einheitlich zu vergleichen.",
        type = VariableType.STRING
    )
    public String countryCode = "49";

    @InputVar(
        label = "Präfix-/Bereichssperre erlauben",
        description = "Wenn aktiv, sperrt ein Eintrag auch alle Nummern, die mit ihm beginnen "
            + "(z.B. '0190' sperrt den gesamten Bereich). Wenn aus, wird nur exakt verglichen.",
        type = VariableType.BOOLEAN
    )
    public Boolean prefixMatch = true;

    // ============================================================
    // OUTPUT VARIABLEN - Ergebnis der Prüfung
    // ============================================================

    @OutputVar(
        label = "Anrufernummer",
        description = "Die erkannte Anrufernummer (international normalisiert)",
        type = VariableType.STRING
    )
    public String caller = "";

    @OutputVar(
        label = "Anonym",
        description = "true, wenn der Anrufer die Rufnummer unterdrückt hat",
        type = VariableType.BOOLEAN
    )
    public Boolean isAnonymous = false;

    @OutputVar(
        label = "Geblockt",
        description = "true, wenn der Anruf durch die Blacklist oder die Anonym-Regel behandelt wurde",
        type = VariableType.BOOLEAN
    )
    public Boolean blocked = false;

    @OutputVar(
        label = "Ausgeführte Aktion",
        description = "Die tatsächlich ausgeführte Aktion (HANGUP/BUSY/REDIRECT/ANNOUNCE/ALLOW/NONE)",
        type = VariableType.STRING
    )
    public String actionTaken = "NONE";

    @OutputVar(
        label = "Status",
        description = "Status-Meldung der Prüfung",
        type = VariableType.STRING
    )
    public String statusMessage = "";

    // Runtime
    private IRuntimeEnvironment runtime;
    private org.apache.logging.log4j.Logger log;

    // Ermittelte Anrufer-Daten (statt innerer Klasse, um separate .class-Dateien zu vermeiden)
    private String callerRaw = "";
    private String callerNameVal = "";

    @Override
    public void execute(IRuntimeEnvironment runtime) throws Exception {
        this.runtime = runtime;
        this.log = runtime.getLog();

        // Sicherstellen, dass wir im Anruf-Kontext laufen (AGI-Runtime)
        Class<?> agiClass;
        try {
            agiClass = Class.forName(CN_AGI_RUNTIME);
        } catch (ClassNotFoundException e) {
            statusMessage = "Fehler: AGI-Runtime nicht verfügbar";
            log.error("Blacklist: " + statusMessage);
            return;
        }
        if (!agiClass.isInstance(runtime)) {
            statusMessage = "Übersprungen - kein Anruf-Kontext (Modul muss 'Call-Processing' / "
                + "'bei allen eingehenden Anrufen' sein)";
            log.warn("Blacklist: " + statusMessage);
            return;
        }

        // 1. Anrufer-Informationen ermitteln (füllt callerRaw, callerNameVal, caller, isAnonymous)
        if (!getCaller()) {
            statusMessage = "Anrufer konnte nicht ermittelt werden - Anruf wird durchgestellt";
            log.warn("Blacklist: " + statusMessage);
            return;
        }

        log.info("Blacklist: Eingehender Anruf - Nummer='" + callerRaw
            + "' (intl='" + caller + "'), anonym=" + isAnonymous
            + ", Name='" + callerNameVal + "'");

        // 2. Anonyme Anrufe separat behandeln
        if (Boolean.TRUE.equals(isAnonymous)) {
            String act = norm(anonymousAction);
            if (act.isEmpty() || act.equals("NONE") || act.equals("ALLOW")) {
                statusMessage = "Anonymer Anruf - keine Aktion, wird durchgestellt";
                actionTaken = "NONE";
                log.info("Blacklist: " + statusMessage);
                return;
            }
            performAction(act, anonymousRedirectTarget);
            blocked = true;
            statusMessage = "Anonymer Anruf -> " + actionTaken;
            log.info("Blacklist: " + statusMessage);
            return;
        }

        // 3. Nummer gegen Blacklist prüfen.
        // matchBlacklist() füllt matchAction/matchTarget/matchEntry, Rückgabe = Treffer ja/nein.
        if (!matchBlacklist(caller)) {
            statusMessage = "Nummer nicht in Blacklist - wird durchgestellt";
            actionTaken = "NONE";
            log.info("Blacklist: " + statusMessage);
            return;
        }

        if (matchAction.equals("ALLOW")) {
            statusMessage = "Treffer auf Ausnahme (ALLOW) - wird durchgestellt";
            actionTaken = "ALLOW";
            log.info("Blacklist: " + statusMessage + " [Regel: " + matchEntry + "]");
            return;
        }

        performAction(matchAction, !matchTarget.isEmpty() ? matchTarget : defaultRedirectTarget);
        blocked = true;
        statusMessage = "Blacklist-Treffer [" + matchEntry + "] -> " + actionTaken;
        log.info("Blacklist: " + statusMessage);
    }

    // ============================================================
    // Anrufer ermitteln über GetCaller2
    // ============================================================

    /** Füllt callerRaw, callerNameVal, caller (intl) und isAnonymous. Rückgabe false bei Fehler. */
    private boolean getCaller() {
        try {
            Object getCaller = newBlock(CN_GET_CALLER);
            execBlock(getCaller);

            callerNameVal = strField(getCaller, "callerName");

            Object anon = field(getCaller, "isAnonymous");
            isAnonymous = (anon instanceof Boolean) && (Boolean) anon;

            // Externe Nummer bevorzugen, dann Signalisierung, dann interne Nummer
            String ext = strField(getCaller, "callerExtNumber");
            String sig = strField(getCaller, "callerSignallingNumber");
            String intn = strField(getCaller, "callerIntNumber");

            callerRaw = !ext.isEmpty() ? ext : (!sig.isEmpty() ? sig : intn);
            caller = toIntl(callerRaw);

            // Wenn keinerlei Nummer da ist, gilt der Anruf praktisch als anonym
            if (callerRaw.isEmpty() && !isAnonymous) {
                log.debug("Blacklist: Keine Anrufernummer vorhanden - als anonym behandelt");
                isAnonymous = true;
            }
            return true;
        } catch (Exception e) {
            log.error("Blacklist: Fehler beim Ermitteln des Anrufers: " + e.getMessage(), e);
            return false;
        }
    }

    // ============================================================
    // Blacklist-Matching
    // ============================================================

    // Ergebnis des letzten Treffers (statt innerer Klasse)
    private String matchAction = "";
    private String matchTarget = "";
    private String matchEntry = "";

    /** Prüft callerIntl gegen die Einträge. Bei Treffer werden matchAction/Target/Entry gesetzt. */
    private boolean matchBlacklist(String callerIntl) {
        if (blacklistEntries == null || blacklistEntries.trim().isEmpty()) {
            return false;
        }
        if (callerIntl == null || callerIntl.isEmpty()) {
            return false;
        }

        // Trennung an Zeilenumbrüchen und Kommas
        String[] lines = blacklistEntries.split("[\\r\\n,]+");
        for (String line : lines) {
            String entry = line.trim();
            if (entry.isEmpty() || entry.startsWith("#")) {
                continue;
            }

            String[] parts = entry.split(";");
            String number = parts[0].trim();
            String action = parts.length > 1 ? norm(parts[1]) : norm(defaultAction);
            String target = parts.length > 2 ? parts[2].trim() : "";

            if (action.isEmpty()) {
                action = "HANGUP";
            }

            String entryIntl = toIntl(number);
            if (entryIntl.isEmpty()) {
                continue;
            }

            boolean hit;
            if (callerIntl.equals(entryIntl)) {
                hit = true;
            } else if (Boolean.TRUE.equals(prefixMatch)
                    && entryIntl.length() >= 3
                    && callerIntl.startsWith(entryIntl)) {
                hit = true;
            } else {
                hit = false;
            }

            if (hit) {
                matchAction = action;
                matchTarget = target;
                matchEntry = entry;
                return true;
            }
        }
        return false;
    }

    // ============================================================
    // Aktionen ausführen
    // ============================================================

    private void performAction(String action, String target) {
        try {
            switch (action) {
                case "HANGUP":
                case "REJECT":
                    execBlock(newBlock(CN_HANGUP));
                    actionTaken = "HANGUP";
                    break;

                case "BUSY":
                    execBlock(newBlock(CN_BUSY));
                    actionTaken = "BUSY";
                    break;

                case "REDIRECT":
                case "FORWARD":
                    if (target == null || target.trim().isEmpty()) {
                        log.error("Blacklist: REDIRECT ohne Ziel - lege stattdessen auf");
                        execBlock(newBlock(CN_HANGUP));
                        actionTaken = "HANGUP";
                    } else {
                        redirect(target.trim());
                        actionTaken = "REDIRECT";
                    }
                    break;

                case "ANNOUNCE":
                    announce();
                    actionTaken = "ANNOUNCE";
                    break;

                default:
                    log.warn("Blacklist: Unbekannte Aktion '" + action + "' - lege auf");
                    execBlock(newBlock(CN_HANGUP));
                    actionTaken = "HANGUP";
            }
        } catch (Exception e) {
            log.error("Blacklist: Fehler beim Ausführen der Aktion '" + action + "': " + e.getMessage(), e);
            // Im Zweifel auflegen
            try {
                execBlock(newBlock(CN_HANGUP));
                actionTaken = "HANGUP";
            } catch (Exception ignore) {
                // nichts mehr möglich
            }
        }
    }

    private void redirect(String target) throws Exception {
        Object call = newBlock(CN_CALL_NUMBER);
        setField(call, "phoneNumber", target);
        // Ursprünglichen Anrufer mitsignalisieren, damit das Ziel sieht, wer anruft
        setField(call, "callerName", callerNameVal != null ? callerNameVal : "");
        setField(call, "callerNumber", callerRaw != null ? callerRaw : "");
        setField(call, "timeout", 30);
        execBlock(call);
        log.info("Blacklist: Anruf von " + caller + " umgeleitet auf " + target);
    }

    private void announce() throws Exception {
        // Anruf annehmen, Ansage abspielen, dann auflegen
        Object answer = newBlock(CN_ANSWER);
        setField(answer, "delay", 0);
        execBlock(answer);

        if (announcementFile != null && !announcementFile.trim().isEmpty()) {
            Object playback = newBlock(CN_PLAYBACK);
            setField(playback, "defaultFile", announcementFile.trim());
            execBlock(playback);
        } else {
            log.warn("Blacklist: ANNOUNCE ohne Ansage-Datei konfiguriert");
        }

        execBlock(newBlock(CN_HANGUP));
    }

    // ============================================================
    // Reflection-Hilfen
    // ============================================================

    private Object newBlock(String className) throws Exception {
        return Class.forName(className).getDeclaredConstructor().newInstance();
    }

    /** Führt einen Call-Handling-Block mit der aktuellen AGI-Runtime aus. */
    private void execBlock(Object block) throws Exception {
        Class<?> agiClass = Class.forName(CN_AGI_RUNTIME);
        Method m = block.getClass().getMethod("execute", agiClass);
        m.invoke(block, runtime);
    }

    private void setField(Object obj, String name, Object value) {
        try {
            Field f = obj.getClass().getField(name);
            f.set(obj, value);
        } catch (NoSuchFieldException e) {
            log.debug("Blacklist: Feld '" + name + "' nicht vorhanden in " + obj.getClass().getName());
        } catch (Exception e) {
            log.warn("Blacklist: Konnte Feld '" + name + "' nicht setzen: " + e.getMessage());
        }
    }

    private Object field(Object obj, String name) {
        try {
            Field f = obj.getClass().getField(name);
            return f.get(obj);
        } catch (Exception e) {
            return null;
        }
    }

    private String strField(Object obj, String name) {
        Object v = field(obj, name);
        return v != null ? v.toString() : "";
    }

    // ============================================================
    // Nummern-Normalisierung
    // ============================================================

    /**
     * Normalisiert eine Rufnummer in eine einheitliche internationale Ziffernform.
     * Beispiele (countryCode=49):
     *   +49 721 12345  -> 4972112345
     *   0049 721 12345 -> 4972112345
     *   0721 12345     -> 4972112345
     *   100            -> 100 (interne Nebenstelle)
     */
    private String toIntl(String raw) {
        if (raw == null) {
            return "";
        }
        String r = raw.trim();
        if (r.isEmpty()) {
            return "";
        }
        boolean plus = r.startsWith("+");
        String digits = r.replaceAll("[^0-9]", "");
        if (digits.isEmpty()) {
            return "";
        }
        String cc = countryCode != null ? countryCode.replaceAll("[^0-9]", "") : "";
        if (plus) {
            return digits;
        }
        if (digits.startsWith("00")) {
            return digits.substring(2);
        }
        if (digits.startsWith("0")) {
            return cc + digits.substring(1);
        }
        return digits;
    }

    private String norm(String s) {
        return s == null ? "" : s.trim().toUpperCase();
    }
}
