package com.clanstop.healing.locator;

import com.clanstop.healing.config.FrameworkConfig;
import com.clanstop.healing.driver.HealingContext;
import com.clanstop.healing.hooks.Screenshots;
import com.clanstop.healing.persistence.ElementStore;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The heart of the framework. {@link #find} tries the primary locator; on success it records
 * a fingerprint and locator history, on failure it heals from stored data and the live DOM.
 */
public final class LocatorResolver {

    private static final Logger log = LoggerFactory.getLogger(LocatorResolver.class);

    /** Keys whose fingerprint has already been refreshed this run (sample once → cheap happy path). */
    private static final Set<String> fingerprintedThisRun = ConcurrentHashMap.newKeySet();

    private final WebDriver driver;
    private final ElementStore store;
    private final FingerprintExtractor extractor;
    private final SimilarityScorer scorer;
    private final CandidateGenerator candidates;
    private final FrameworkConfig cfg;

    public LocatorResolver(WebDriver driver, ElementStore store) {
        this.driver = driver;
        this.store = store;
        this.cfg = FrameworkConfig.get();
        this.extractor = new FingerprintExtractor(driver);
        this.scorer = new SimilarityScorer(cfg.volatileIdRegex());
        this.candidates = new CandidateGenerator(store);
    }

    public WebElement find(String logicalKey, By primary) {
        // --- Happy path ---
        List<WebElement> matches = driver.findElements(primary);
        if (!matches.isEmpty()) {
            WebElement el = matches.get(0);
            if (matches.size() > 1) {
                log.warn("Primary locator {} matched {} elements for '{}'; using the first. "
                        + "Consider tightening it.", primary, matches.size(), logicalKey);
            }
            recordSuccess(logicalKey, primary, el);
            return el;
        }

        // --- Heal ---
        log.warn("Primary locator {} found nothing for '{}'; attempting to heal.", primary, logicalKey);
        HealResult healed = heal(logicalKey, primary);
        if (healed != null) {
            log.info("Healed '{}' via {} ({}={}) score={} in {}ms — changed: {}",
                    logicalKey, healed.strategy(), healed.byType(), healed.byValue(),
                    String.format("%.3f", healed.score()), healed.elapsedMs(), healed.changes());
            store.recordHeal(currentStepId(), logicalKey, Locators.typeOf(primary) + "=" + Locators.valueOf(primary),
                    healed.byType(), healed.byValue(), healed.score(), healed.strategy(),
                    healed.changes(), healed.screenshotPath());
            // Promote the winner so next run tries it first.
            store.recordLocator(logicalKey, healed.byType(), healed.byValue(), "HEAL");
            return healed.element();
        }

        throw new NoSuchElementException(
                "Self-healing failed for '" + logicalKey + "' (primary " + primary + " matched nothing "
                        + "and no candidate scored above threshold " + cfg.threshold() + ")");
    }

    private void recordSuccess(String logicalKey, By primary, WebElement el) {
        store.recordLocator(logicalKey, Locators.typeOf(primary), Locators.valueOf(primary), "PRIMARY");
        // Refresh the stored fingerprint at most once per key per run.
        if (fingerprintedThisRun.add(logicalKey)) {
            try {
                Fingerprint fp = extractor.extract(el);
                if (fp != null) {
                    store.saveFingerprint(logicalKey, fp, HealingContext.runId());
                }
            } catch (Exception e) {
                log.debug("fingerprint capture skipped for {}: {}", logicalKey, e.getMessage());
            }
        }
    }

    private HealResult heal(String logicalKey, By failedPrimary) {
        long start = System.currentTimeMillis();
        Fingerprint stored = store.loadFingerprint(logicalKey);
        if (stored == null) {
            log.warn("No stored fingerprint for '{}' — cannot heal (first run?).", logicalKey);
            return null;
        }

        double bestScore = 0;
        WebElement bestEl = null;
        Fingerprint bestFp = null;
        String bestType = null, bestValue = null, bestStrategy = null;

        // 1) Targeted candidates: history + fingerprint-derived locators.
        for (CandidateGenerator.Candidate cand : candidates.generate(logicalKey, stored, failedPrimary)) {
            List<WebElement> found;
            try {
                found = driver.findElements(cand.by());
            } catch (Exception badSelector) {
                continue;
            }
            if (found.isEmpty()) {
                continue;
            }
            // A targeted candidate usually resolves to one element; cap scoring at a few.
            List<WebElement> sample = found.size() > 5 ? found.subList(0, 5) : found;
            List<Fingerprint> fps = extractor.extractAll(sample);
            for (int i = 0; i < fps.size(); i++) {
                double s = scorer.score(stored, fps.get(i));
                if (s > bestScore) {
                    bestScore = s;
                    bestEl = sample.get(i);
                    bestFp = fps.get(i);
                    bestType = cand.byType();
                    bestValue = cand.byValue();
                    bestStrategy = cand.strategy();
                }
            }
            if (bestScore >= 1.0) {
                break; // perfect match, stop early
            }
        }

        // 2) Broad DOM-scan fallback if nothing targeted cleared the bar.
        if (bestScore < cfg.threshold()) {
            By tagBy = By.tagName(notBlank(stored.tag()) ? stored.tag() : "*");
            List<WebElement> all = driver.findElements(tagBy);
            if (all.size() > cfg.domScanLimit()) {
                log.warn("DOM scan for '{}' capped at {} of {} <{}> elements.",
                        logicalKey, cfg.domScanLimit(), all.size(), stored.tag());
                all = all.subList(0, cfg.domScanLimit());
            }
            List<Fingerprint> fps = extractor.extractAll(all);
            for (int i = 0; i < fps.size(); i++) {
                double s = scorer.score(stored, fps.get(i));
                if (s > bestScore) {
                    bestScore = s;
                    bestEl = all.get(i);
                    // Persist the winner as a concrete css path so it becomes reusable history.
                    Fingerprint hit = fps.get(i);
                    bestFp = hit;
                    if (notBlank(hit.cssPath())) {
                        bestType = "cssSelector";
                        bestValue = hit.cssPath();
                    } else {
                        bestType = "xpath";
                        bestValue = hit.xpath();
                    }
                    bestStrategy = "DOM_SCAN";
                }
            }
        }

        long elapsed = System.currentTimeMillis() - start;
        if (bestEl != null && bestScore >= cfg.threshold()) {
            String changes = FingerprintDiff.describe(stored, bestFp);
            String shot = healScreenshot(logicalKey, bestEl);
            return new HealResult(bestEl, bestType, bestValue, bestScore, bestStrategy, changes, shot, elapsed);
        }
        log.warn("Best candidate for '{}' scored {} (< threshold {}) — giving up.",
                logicalKey, String.format("%.3f", bestScore), cfg.threshold());
        return null;
    }

    /** Highlight the healed element and screenshot the page, so the audit shows what was matched. */
    private String healScreenshot(String logicalKey, WebElement el) {
        if (!cfg.screenshotOnHeal()) {
            return null;
        }
        String prevOutline = null;
        try {
            JavascriptExecutor js = (JavascriptExecutor) driver;
            prevOutline = String.valueOf(js.executeScript(
                    "var e=arguments[0];var o=e.style.outline;"
                    + "e.style.outline='3px solid #e11d48';e.style.outlineOffset='2px';"
                    + "e.scrollIntoView({block:'center',inline:'center'});return o;", el));
            String path = Screenshots.capture(driver, cfg.screenshotDir(), "heal_" + logicalKey);
            js.executeScript("arguments[0].style.outline=arguments[1];", el,
                    prevOutline == null || "null".equals(prevOutline) ? "" : prevOutline);
            return path;
        } catch (Exception e) {
            log.debug("heal screenshot skipped for {}: {}", logicalKey, e.getMessage());
            return null;
        }
    }

    private static Long currentStepId() {
        long id = HealingContext.currentStepId();
        return id < 0 ? null : id;
    }

    private static boolean notBlank(String s) {
        return s != null && !s.isBlank();
    }
}
