package com.clanstop.healing.locator;

import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Builds {@link Fingerprint}s from live elements. A single {@link JavascriptExecutor}
 * call extracts every element in a batch, so even the broad DOM-scan fallback costs
 * exactly one round-trip.
 */
public final class FingerprintExtractor {

    // Extracts an array of elements (arguments[0]) into an array of plain objects.
    private static final String SCRIPT = """
        const nodes = arguments[0];
        function cssPath(el){
          if(!el || el.nodeType!==1) return '';
          const parts=[];
          while(el && el.nodeType===1 && parts.length<12){
            let sel=el.nodeName.toLowerCase();
            if(el.id){ parts.unshift(sel+'#'+CSS.escape(el.id)); break; }
            let nth=1, sib=el;
            while((sib=sib.previousElementSibling)!=null){ if(sib.nodeName===el.nodeName) nth++; }
            parts.unshift(sel+':nth-of-type('+nth+')');
            el=el.parentElement;
          }
          return parts.join(' > ');
        }
        function xpath(el){
          if(!el || el.nodeType!==1) return '';
          const parts=[];
          while(el && el.nodeType===1){
            let nth=1, sib=el;
            while((sib=sib.previousElementSibling)!=null){ if(sib.nodeName===el.nodeName) nth++; }
            parts.unshift(el.nodeName.toLowerCase()+'['+nth+']');
            el=el.parentElement;
          }
          return '/'+parts.join('/');
        }
        function one(el){
          const attrs={};
          for(const a of el.attributes){ attrs[a.name]=a.value; }
          let cls=[];
          const cn=el.className;
          if(typeof cn==='string'){ cls=cn.split(/\\s+/).filter(Boolean); }
          else if(cn && cn.baseVal){ cls=cn.baseVal.split(/\\s+/).filter(Boolean); }
          let depth=0, p=el; while(p){ depth++; p=p.parentElement; }
          const r=el.getBoundingClientRect();
          return {
            tag: el.tagName.toLowerCase(),
            id: el.id || '',
            name: el.getAttribute('name') || '',
            classes: cls,
            text: (el.textContent||'').trim().slice(0,200),
            attrs: attrs,
            cssPath: cssPath(el),
            xpath: xpath(el),
            cx: r.left + r.width/2,
            cy: r.top + r.height/2,
            depth: depth
          };
        }
        return nodes.map(one);
        """;

    private final WebDriver driver;

    public FingerprintExtractor(WebDriver driver) {
        this.driver = driver;
    }

    public Fingerprint extract(WebElement element) {
        List<Fingerprint> fps = extractAll(List.of(element));
        return fps.isEmpty() ? null : fps.get(0);
    }

    @SuppressWarnings("unchecked")
    public List<Fingerprint> extractAll(List<WebElement> elements) {
        List<Fingerprint> out = new ArrayList<>();
        if (elements.isEmpty()) {
            return out;
        }
        Object raw = ((JavascriptExecutor) driver).executeScript(SCRIPT, elements);
        if (!(raw instanceof List<?> rows)) {
            return out;
        }
        for (Object row : rows) {
            if (row instanceof Map<?, ?> m) {
                out.add(fromMap((Map<String, Object>) m));
            }
        }
        return out;
    }

    private static Fingerprint fromMap(Map<String, Object> m) {
        Map<String, String> attrs = new LinkedHashMap<>();
        Object rawAttrs = m.get("attrs");
        if (rawAttrs instanceof Map<?, ?> am) {
            for (Map.Entry<?, ?> e : am.entrySet()) {
                if (Fingerprint.TRACKED_ATTRS.contains(String.valueOf(e.getKey()))) {
                    attrs.put(String.valueOf(e.getKey()), String.valueOf(e.getValue()));
                }
            }
        }
        List<String> classes = new ArrayList<>();
        if (m.get("classes") instanceof List<?> cl) {
            for (Object c : cl) {
                classes.add(String.valueOf(c));
            }
        }
        return new Fingerprint(
                str(m.get("tag")),
                str(m.get("id")),
                str(m.get("name")),
                classes,
                str(m.get("text")),
                attrs,
                str(m.get("cssPath")),
                str(m.get("xpath")),
                dbl(m.get("cx")),
                dbl(m.get("cy")),
                (int) Math.round(dbl(m.get("depth")))
        );
    }

    private static String str(Object o) { return o == null ? "" : o.toString(); }

    private static double dbl(Object o) {
        if (o instanceof Number n) {
            return n.doubleValue();
        }
        try {
            return o == null ? 0 : Double.parseDouble(o.toString());
        } catch (NumberFormatException e) {
            return 0;
        }
    }
}
