BareGit
"""Constrained JavaScript builders for live DOM operations."""

from __future__ import annotations

import json


def _literal(value: object) -> str:
    return json.dumps(value, ensure_ascii=False, separators=(",", ":"))


def _wrapped(body: str) -> str:
    return (
        "(() => { try { " + body
        + " } catch (error) { return JSON.stringify({ok:false,error:{"
        + "code:'page_script_failed',message:String(error && "
        + "error.message || error)}}); } })()"
    )


def _wrappedAsync(body: str) -> str:
    return (
        "(async () => { 'use strict'; try { " + body
        + " } catch (error) { return JSON.stringify({ok:false,error:{"
        + "code:'javascript_error',message:String(error && "
        + "error.message || error)}}); } })()"
    )


def runJavaScript(script: str, *, statement_body: bool = False) -> str:
    """Build an async evaluator for caller-supplied page JavaScript."""
    if statement_body:
        operation = f"(async () => {{\n{script}\n}})()"
    else:
        operation = f"(\n{script}\n)"
    body = f"""
const result = await {operation};
const resultType = result === null ? 'null' : typeof result;
let serializable = result;
if (resultType === 'undefined') serializable = null;
else if (resultType === 'bigint') serializable = String(result);
else if (resultType === 'symbol' || resultType === 'function') {{
    serializable = String(result);
}} else if (resultType === 'number' && !Number.isFinite(result)) {{
    serializable = String(result);
}}
return JSON.stringify({{ok:true,value:{{
    result:serializable,
    result_type:resultType
}}}});
"""
    return _wrappedAsync(body)


COMMON_HELPERS = r"""
const clean = value => String(value || '').replace(/\s+/g, ' ').trim();
const labelled = element => {
    const direct = element.getAttribute('aria-label');
    if (direct) return clean(direct);
    const ids = (element.getAttribute('aria-labelledby') || '')
        .split(/\s+/).filter(Boolean);
    const names = ids.map(id => document.getElementById(id))
        .filter(Boolean).map(item => clean(item.textContent)).filter(Boolean);
    if (names.length) return names.join(' ');
    if (element.labels && element.labels.length) {
        return clean(Array.from(element.labels)
            .map(item => item.textContent).join(' '));
    }
    return clean(element.getAttribute('alt') ||
        element.getAttribute('title') ||
        element.getAttribute('placeholder') || element.textContent);
};
const pathFor = element => {
    if (element.id) return '#' + CSS.escape(element.id);
    const parts = [];
    let current = element;
    while (current && current.nodeType === Node.ELEMENT_NODE) {
        let part = current.localName;
        if (current.id) {
            parts.unshift('#' + CSS.escape(current.id));
            break;
        }
        const parent = current.parentElement;
        if (parent) {
            const peers = Array.from(parent.children)
                .filter(item => item.localName === current.localName);
            if (peers.length > 1) {
                part += ':nth-of-type(' + (peers.indexOf(current) + 1) + ')';
            }
        }
        parts.unshift(part);
        current = parent;
    }
    return parts.join(' > ');
};
const safeHref = element => {
    if (!element.href) return '';
    try {
        const url = new URL(element.href, document.baseURI);
        url.username = '';
        url.password = '';
        return url.href;
    } catch (_) { return ''; }
};
const describe = element => ({
    selector: pathFor(element),
    tag_name: element.localName || '',
    role: element.getAttribute('role') || null,
    input_type: element instanceof HTMLInputElement ? element.type : null,
    accessible_name: labelled(element),
    href: safeHref(element)
});
"""


def snapshot(max_elements: int, max_text_chars: int) -> str:
    """Build a compact visibility and actionable-element snapshot script."""
    body = COMMON_HELPERS + rf"""
const visible = element => {{
    if (element instanceof HTMLInputElement && element.type === 'file') {{
        return true;
    }}
    const style = getComputedStyle(element);
    return style.display !== 'none' && style.visibility !== 'hidden' &&
        style.visibility !== 'collapse' && element.getClientRects().length > 0;
}};
const roles = new Set(['button','link','textbox','checkbox','radio',
    'combobox','listbox','option','menuitem','switch','tab','slider']);
const candidates = Array.from(document.querySelectorAll(
    'a[href],button,input,textarea,select,[role],[contenteditable]'));
const elements = [];
for (const element of candidates) {{
    if (elements.length >= {max_elements}) break;
    const role = element.getAttribute('role');
    if (!visible(element) || (role && !roles.has(role) &&
        !element.matches('a[href],button,input,textarea,select,' +
            '[contenteditable]'))) continue;
    const item = describe(element);
    item.value = element instanceof HTMLInputElement &&
        element.type === 'password' ? null :
        ('value' in element ? String(element.value) : null);
    item.disabled = Boolean(element.disabled ||
        element.getAttribute('aria-disabled') === 'true');
    item.readonly = Boolean(element.readOnly ||
        element.getAttribute('aria-readonly') === 'true');
    item.checked = 'checked' in element ? Boolean(element.checked) : null;
    item.selected = 'selected' in element ? Boolean(element.selected) : null;
    item.accept = element instanceof HTMLInputElement &&
        element.type === 'file' ? element.accept : null;
    item.multiple = element instanceof HTMLInputElement &&
        element.type === 'file' ? element.multiple : null;
    elements.push(item);
}}
const rawText = clean(document.body ? document.body.innerText : '');
const truncated = rawText.length > {max_text_chars};
const text = rawText.slice(0, {max_text_chars});
return JSON.stringify({{ok:true,value:{{
    title: document.title,
    url: location.href,
    text,
    text_truncated: truncated,
    elements
}}}});
"""
    return _wrapped(body)


def describeElement(selector: str) -> str:
    """Build a script that returns current identity fields for a selector."""
    body = COMMON_HELPERS + f"""
let element;
try {{ element = document.querySelector({_literal(selector)}); }}
catch (_) {{ return JSON.stringify({{ok:false,error:{{
    code:'invalid_selector',message:'The stored selector is invalid.'
}}}}); }}
if (!element) return JSON.stringify({{ok:false,error:{{
    code:'element_not_found',message:'The selector no longer resolves.'
}}}});
return JSON.stringify({{ok:true,value:describe(element)}});
"""
    return _wrapped(body)


def click(selector: str) -> str:
    """Build a stale-safe DOM click script for a validated selector."""
    body = f"""
const element = document.querySelector({_literal(selector)});
if (!element) return JSON.stringify({{ok:false,error:{{
    code:'element_not_found',message:'The selector no longer resolves.'
}}}});
if (element.disabled || element.getAttribute('aria-disabled') === 'true') {{
    return JSON.stringify({{ok:false,error:{{
        code:'element_not_interactable',message:'The element is disabled.'
    }}}});
}}
element.scrollIntoView({{block:'nearest',inline:'nearest'}});
if (typeof element.focus === 'function') element.focus();
element.click();
return JSON.stringify({{ok:true,value:{{synthetic:true,url:location.href}}}});
"""
    return _wrapped(body)


def fill(selector: str, value: str, submit: bool) -> str:
    """Build a native-setter text replacement script."""
    body = f"""
const element = document.querySelector({_literal(selector)});
if (!element) return JSON.stringify({{ok:false,error:{{
    code:'element_not_found',message:'The selector no longer resolves.'
}}}});
const value = {_literal(value)};
element.focus();
if (element instanceof HTMLInputElement ||
    element instanceof HTMLTextAreaElement) {{
    if (element.disabled || element.readOnly) return JSON.stringify({{
        ok:false,error:{{code:'element_not_interactable',
        message:'The element is disabled or readonly.'}}
    }});
    const proto = element instanceof HTMLInputElement ?
        HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
    const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
    setter.call(element, value);
    element.dispatchEvent(new Event('input', {{bubbles:true}}));
    element.dispatchEvent(new Event('change', {{bubbles:true}}));
}} else if (element.isContentEditable) {{
    const selection = getSelection();
    const range = document.createRange();
    range.selectNodeContents(element);
    selection.removeAllRanges();
    selection.addRange(range);
    document.execCommand('insertText', false, value);
    element.dispatchEvent(new InputEvent('input', {{
        bubbles:true,inputType:'insertText',data:value
    }}));
}} else {{
    return JSON.stringify({{ok:false,error:{{
        code:'unsupported_element',message:'Element is not text-editable.'
    }}}});
}}
if ({_literal(submit)}) {{
    const form = element.form || element.closest('form');
    if (form) form.requestSubmit();
}}
return JSON.stringify({{ok:true,value:{{synthetic:true,
    submitted:{_literal(submit)}}}}});
"""
    return _wrapped(body)


def appendCharacter(selector: str, character: str) -> str:
    """Build one bounded synthetic typing step."""
    body = f"""
const element = document.querySelector({_literal(selector)});
if (!element) return JSON.stringify({{ok:false,error:{{
    code:'element_not_found',message:'The selector no longer resolves.'
}}}});
const character = {_literal(character)};
if (!(element instanceof HTMLInputElement) &&
    !(element instanceof HTMLTextAreaElement) && !element.isContentEditable) {{
    return JSON.stringify({{ok:false,error:{{
        code:'unsupported_element',message:'Element is not text-editable.'
    }}}});
}}
element.focus();
element.dispatchEvent(new KeyboardEvent('keydown', {{
    key:character,bubbles:true
}}));
if (element.isContentEditable) {{
    document.execCommand('insertText', false, character);
}} else {{
    const proto = element instanceof HTMLInputElement ?
        HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
    const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
    setter.call(element, element.value + character);
}}
element.dispatchEvent(new InputEvent('input', {{
    bubbles:true,inputType:'insertText',data:character
}}));
element.dispatchEvent(new KeyboardEvent('keyup', {{
    key:character,bubbles:true
}}));
return JSON.stringify({{ok:true,value:{{synthetic:true}}}});
"""
    return _wrapped(body)


def pressKey(selector: str | None, key: str) -> str:
    """Build one allowlisted synthetic key action."""
    target = (
        f"document.querySelector({_literal(selector)})"
        if selector is not None
        else "document.activeElement"
    )
    body = f"""
const element = {target};
if (!element) return JSON.stringify({{ok:false,error:{{
    code:'element_not_found',message:'No target element is available.'
}}}});
const key = {_literal(key)};
element.focus();
element.dispatchEvent(new KeyboardEvent('keydown', {{key,bubbles:true}}));
let effect = 'event';
if (key === 'Enter' && element.form) {{
    element.form.requestSubmit(); effect = 'form_submitted';
}} else if (key === 'Tab') {{
    const all = Array.from(document.querySelectorAll(
        'a[href],button,input,textarea,select,[tabindex],[contenteditable]'))
        .filter(item => !item.disabled && item.tabIndex >= 0 &&
            item.getClientRects().length > 0);
    const index = all.indexOf(element);
    if (all.length) all[(index + 1) % all.length].focus();
    effect = 'focus_moved';
}} else if ((key === 'Backspace' || key === 'Delete') &&
    (element instanceof HTMLInputElement ||
     element instanceof HTMLTextAreaElement)) {{
    const start = element.selectionStart ?? element.value.length;
    const end = element.selectionEnd ?? start;
    let next = element.value;
    if (start !== end) next = next.slice(0, start) + next.slice(end);
    else if (key === 'Backspace' && start > 0)
        next = next.slice(0, start - 1) + next.slice(end);
    else if (key === 'Delete')
        next = next.slice(0, start) + next.slice(start + 1);
    const proto = element instanceof HTMLInputElement ?
        HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
    Object.getOwnPropertyDescriptor(proto, 'value').set.call(element, next);
    element.dispatchEvent(new InputEvent('input', {{bubbles:true,
        inputType:key === 'Backspace' ? 'deleteContentBackward' :
        'deleteContentForward'}}));
    effect = 'value_changed';
}}
element.dispatchEvent(new KeyboardEvent('keyup', {{key,bubbles:true}}));
return JSON.stringify({{ok:true,value:{{synthetic:true,effect}}}});
"""
    return _wrapped(body)


def navigate(url: str) -> str:
    """Build an absolute URL assignment script."""
    return _wrapped(
        f"window.location.href = {_literal(url)}; "
        "return JSON.stringify({ok:true,value:{started:true}});"
    )


def pageState() -> str:
    """Build a script returning URL, title, and document readiness."""
    return _wrapped(
        "return JSON.stringify({ok:true,value:{url:location.href,"
        "title:document.title,ready_state:document.readyState}});"
    )


def waitCondition(
    condition: str,
    value: str,
    state: str,
) -> str:
    """Build one injection-safe observable-condition poll."""
    if condition == "text":
        expression = (
            "Boolean(document.body && "
            f"document.body.innerText.includes({_literal(value)}))"
        )
    elif condition == "url_contains":
        expression = f"location.href.includes({_literal(value)})"
    else:
        body = f"""
let present;
try {{ present = document.querySelector({_literal(value)}) !== null; }}
catch (_) {{ return JSON.stringify({{ok:false,error:{{
    code:'invalid_selector',message:'The CSS selector is invalid.'
}}}}); }}
return JSON.stringify({{ok:true,value:{{matched:
    present === {_literal(state == 'present')},present,url:location.href}}}});
"""
        return _wrapped(body)
    return _wrapped(
        f"const present = {expression}; "
        f"return JSON.stringify({{ok:true,value:{{matched:"
        f"present === {_literal(state == 'present')},present," 
        "url:location.href}});"
    )


def uploadInitialize(
    selector: str,
    key: str,
    metadata: list[dict[str, object]],
) -> str:
    """Build upload target validation and staging initialization."""
    body = f"""
const input = document.querySelector({_literal(selector)});
if (!(input instanceof HTMLInputElement) || input.type !== 'file') {{
    return JSON.stringify({{ok:false,error:{{code:'upload_target_invalid',
        message:'Target is not a file input.'}}}});
}}
if (input.disabled) return JSON.stringify({{ok:false,error:{{
    code:'upload_target_invalid',message:'The file input is disabled.'
}}}});
const metadata = {_literal(metadata)};
if (!input.multiple && metadata.length > 1) {{
    return JSON.stringify({{ok:false,error:{{code:'upload_target_invalid',
        message:'The file input does not allow multiple files.'}}}});
}}
window[{_literal(key)}] = {{selector:{_literal(selector)}, metadata,
    chunks:metadata.map(() => []), encoded:metadata.map(() => 0)}};
return JSON.stringify({{ok:true,value:{{multiple:input.multiple,
    accept:input.accept}}}});
"""
    return _wrapped(body)


def uploadAppend(key: str, file_index: int, chunk: str) -> str:
    """Build one bounded base64 upload staging append."""
    body = f"""
const stage = window[{_literal(key)}];
if (!stage) return JSON.stringify({{ok:false,error:{{
    code:'upload_transfer_failed',message:'Upload staging was lost.'
}}}});
const index = {file_index};
const chunk = {_literal(chunk)};
stage.chunks[index].push(chunk);
stage.encoded[index] += chunk.length;
return JSON.stringify({{ok:true,value:{{
    chunk_index:stage.chunks[index].length - 1,
    encoded_length:stage.encoded[index]
}}}});
"""
    return _wrapped(body)


def uploadAssign(key: str) -> str:
    """Build File construction, assignment, verification, and cleanup."""
    body = f"""
const key = {_literal(key)};
const stage = window[key];
if (!stage) return JSON.stringify({{ok:false,error:{{
    code:'upload_assignment_failed',message:'Upload staging was lost.'
}}}});
try {{
    const input = document.querySelector(stage.selector);
    if (!(input instanceof HTMLInputElement) || input.type !== 'file') {{
        return JSON.stringify({{ok:false,error:{{
            code:'upload_target_invalid',message:'File input was replaced.'
        }}}});
    }}
    const files = stage.metadata.map((meta, index) => {{
        const parts = stage.chunks[index].map(encoded => {{
            const binary = atob(encoded);
            const bytes = new Uint8Array(binary.length);
            for (let offset = 0; offset < binary.length; ++offset) {{
                bytes[offset] = binary.charCodeAt(offset);
            }}
            return bytes;
        }});
        return new File(parts, meta.name, {{type:meta.type,
            lastModified:meta.last_modified}});
    }});
    const transfer = new DataTransfer();
    for (const file of files) transfer.items.add(file);
    input.files = transfer.files;
    input.dispatchEvent(new Event('input', {{bubbles:true}}));
    input.dispatchEvent(new Event('change', {{bubbles:true}}));
    const actual = Array.from(input.files).map(file => ({{
        name:file.name,size:file.size,type:file.type
    }}));
    const valid = actual.length === stage.metadata.length &&
        actual.every((item, index) => item.name ===
            stage.metadata[index].name && item.size ===
            stage.metadata[index].size && item.type ===
            stage.metadata[index].type);
    if (!valid) return JSON.stringify({{ok:false,error:{{
        code:'upload_assignment_failed',
        message:'File input verification failed.'
    }}}});
    return JSON.stringify({{ok:true,value:{{files:actual,
        input_dispatched:true,change_dispatched:true}}}});
}} finally {{ delete window[key]; }}
"""
    return _wrapped(body)


def uploadCleanup(key: str) -> str:
    """Build best-effort deletion of upload staging state."""
    return _wrapped(
        f"delete window[{_literal(key)}]; "
        "return JSON.stringify({ok:true,value:{cleaned:true}});"
    )