
import { Page } from 'playwright';
import fs from 'fs';
import path from 'path';
import levenshtein from 'js-levenshtein';

type Step = () => Promise<void>;
const fallbackFile = path.join(__dirname, 'fallback-selectors.json');
const historyFile = path.join(__dirname, 'selector-history.json');

function loadFallbacks(): Record<string, string> {
    if (fs.existsSync(fallbackFile)) {
        return JSON.parse(fs.readFileSync(fallbackFile, 'utf-8'));
    }
    return {};
}

function saveFallback(original: string, healed: string) {
    const data = loadFallbacks();
    data[original] = healed;
    fs.writeFileSync(fallbackFile, JSON.stringify(data, null, 2));
}

function updateSelectorHistory(original: string, healed: string) {
    let history: Record<string, string[]> = {};
    if (fs.existsSync(historyFile)) {
        history = JSON.parse(fs.readFileSync(historyFile, 'utf-8'));
    }
    if (!history[original]) {
        history[original] = [];
    }
    if (!history[original].includes(healed)) {
        history[original].push(healed);
    }
    fs.writeFileSync(historyFile, JSON.stringify(history, null, 2));
}

async function findSimilarSelector(page: Page, failedSelector: string): Promise<string | null> {
    const failedText = await page.locator(failedSelector).textContent().catch(() => '');
    if (!failedText) return null;

    const elements = await page.$$('body *');
    let bestMatch: { selector: string, score: number } = { selector: '', score: Infinity };

    for (const el of elements) {
        const text = await el.textContent();
        if (!text) continue;

        const distance = levenshtein(failedText.trim(), text.trim());
        if (distance < bestMatch.score && distance < 10) {
            const tag = await el.evaluate(el => el.tagName.toLowerCase());
            bestMatch = {
                selector: `${tag}:has-text("${text.trim()}")`,
                score: distance
            };
        }
    }

    if (bestMatch.selector) {
        saveFallback(failedSelector, bestMatch.selector);
        updateSelectorHistory(failedSelector, bestMatch.selector);
        return bestMatch.selector;
    }

    return null;
}

export function Healenium(failedSelector: string) {
    return function (
        _target: Object,
        _propertyKey: string,
        descriptor: TypedPropertyDescriptor<Step>
    ): void {
        const originalMethod = descriptor.value!;
        descriptor.value = async function (...args: any[]) {
            const page: Page = (this as any).page || args.find(arg => typeof arg === 'object' && 'click' in arg);
            if (!page) throw new Error('No Page object found for healing logic');

            try {
                await (originalMethod as any).apply(this, args);
            } catch (e: any) {
                console.warn('[Healenium] Attempting to heal from error:', e.message);

                const fallbacks = loadFallbacks();
                if (fallbacks[failedSelector]) {
                    console.info(`[Healenium] Retrying with healed selector: ${fallbacks[failedSelector]}`);
                    await page.click(fallbacks[failedSelector]);
                    updateSelectorHistory(failedSelector, fallbacks[failedSelector]);
                    return;
                }

                const healedSelector = await findSimilarSelector(page, failedSelector);
                if (healedSelector) {
                    console.info(`[Healenium] Found and stored healed selector: ${healedSelector}`);
                    await page.click(healedSelector);
                } else {
                    throw e;
                }
            }
        };
    };
}
