#!/usr/bin/env bash
export THDir=/home/user/ほしゅつーる/
#CurrentThreadNum=1778038443 #偽番でエラー起こす用
#CurrentThreadURL="https://mevius.5ch.io/test/read.cgi/internet/1778926030/"
#CurrentDatURL="https://mevius.5ch.io/internet/dat/1778926030"

# ADB/phone preparation is disabled; the safe path is Playwright-only.

set -e
trap 'printf "ERROR: %s\nLine:%s\ncommand: %s\n" "$0" "$LINENO" "$BASH_COMMAND" > "${THDir}/LastErr"' ERR
# Start each run with a clean error marker; ERR trap rewrites it on failure.
: > "${THDir}/LastErr"

if [ -z "$1" ]; then
    HPMode="1"
else
    HPMode="$1"
fi

if [ "$HPMode" != "HtP" ]; then
    PW_Time=$(( LastUnix + 3285 ))

    while [ "$(date +%s)" -lt "$PW_Time" ]; do
        echo "待機中... あと $(( PW_Time - $(date +%s) )) 秒"
        sleep 1
    done
fi

if [ "$HPMode" = "HtP" ]; then
    PostMsg="ほしゅ対象発見！"
else
    PostMsg="ききかんりー"
fi

case "$HPMode" in

1|HtP)

PortFile="${THDir}/.last_hosyu_port"
LauncherPidFile="${THDir}/.last_hosyu_launcher_pid"

if [ -f "$LauncherPidFile" ]; then
    LAST_LAUNCHER_PID=$(cat "$LauncherPidFile" 2>/dev/null || :)
    if [ -n "$LAST_LAUNCHER_PID" ]; then
        kill "$LAST_LAUNCHER_PID" 2>/dev/null || :
        sleep 0.2
        if kill -0 "$LAST_LAUNCHER_PID" 2>/dev/null; then
            kill -9 "$LAST_LAUNCHER_PID" 2>/dev/null || :
        fi
    fi
    rm -f "$LauncherPidFile"
fi

if [ -f "$PortFile" ]; then
    LAST_PORT=$(cat "$PortFile")
    if [ -n "$LAST_PORT" ]; then
        pkill -f "remote-debugging-port=$LAST_PORT" || :
    fi
    rm -f "$PortFile"
fi

RunStamp="$(date +%Y%m%d_%H%M%S)_$$"
PythonMainLog="${THDir}/python_main_${RunStamp}.log"

export CurrentThreadURL
export CurrentDatURL
export CurrentThreadNum
export PostMsg
export THDir
export HPMode
export LastUnix

echo "before pythonrun:$(date "+%Y-%m-%d %H:%M:%S")" >>"$THDir/Log2"

set +e
tmpRESULT=$(python3 - << 'EOF' 2>>"$PythonMainLog"
import os
import sys
import time
import hashlib
import asyncio
import traceback
import subprocess
from pathlib import Path
from playwright.async_api import async_playwright

th_dir = os.environ.get("THDir", "/home/user/ほしゅつーる/")
log2 = Path(th_dir) / "Log2"
auto_recovery_log = Path(th_dir) / "auto_recovery.log"
submit_started = False

try:
    max_playwright_attempts = max(
        1,
        min(3, int(os.environ.get("HOSYU_PLAYWRIGHT_ATTEMPTS", "3") or "3")),
    )
except ValueError:
    max_playwright_attempts = 3

def log(msg):
    with log2.open("a", encoding="utf-8") as f:
        f.write(f"{msg}:{time.strftime('%Y-%m-%d %H:%M:%S')}\n")

def recovery_log(msg):
    with auto_recovery_log.open("a", encoding="utf-8") as f:
        f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {msg}\n")

def looks_like_dat_text(text):
    return any(line.count("<>") >= 3 for line in text.splitlines())

def is_target_closed_error(exc):
    text = repr(exc)
    return (
        exc.__class__.__name__ == "TargetClosedError"
        or "Target page, context or browser has been closed" in text
    )

async def wait_until(ts):
    last_remain = None
    while time.time() < ts:
        remain = max(0, int(ts - time.time()))
        if remain != last_remain:
            print(f"待機中... あと {remain} 秒", file=sys.stderr)
            last_remain = remain
        await asyncio.sleep(0.05)

def md5_current_dat_sync():
    current_path = Path(th_dir) / "CurrentThreadNum.dat"
    current_bytes = current_path.read_bytes()
    if not current_bytes.strip():
        raise RuntimeError("CurrentThreadNum.dat is empty")

    current_text = current_bytes.decode("utf-8", "ignore")
    if not looks_like_dat_text(current_text):
        raise RuntimeError("CurrentThreadNum.dat is not DAT-like")

    old_md5 = hashlib.md5(current_bytes).hexdigest()

    dat_url = os.environ.get("CurrentDatURL")
    if not dat_url:
        raise RuntimeError("CurrentDatURL is empty")

    result = subprocess.run(
        ["curl", "-s", "-f", "-L", dat_url],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
    )

    if result.returncode != 0:
        err = result.stderr.decode("utf-8", "ignore")
        raise RuntimeError(f"curl failed: code={result.returncode} stderr={err}")

    if not result.stdout:
        raise RuntimeError("curl returned empty DAT")

    new_text = result.stdout.decode("cp932", "ignore")
    if not new_text.strip():
        raise RuntimeError("curl returned blank DAT")

    lower_text = new_text.lower()
    if "<html" in lower_text or "404" in lower_text or "not found" in lower_text:
        raise RuntimeError("curl returned non-DAT response")
    if not looks_like_dat_text(new_text):
        raise RuntimeError("curl returned non-DAT response")

    new_bytes = new_text.encode("utf-8")
    new_md5 = hashlib.md5(new_bytes).hexdigest()

    return old_md5, new_md5

def can_check_current_dat():
    return bool(os.environ.get("CurrentDatURL")) and (Path(th_dir) / "CurrentThreadNum.dat").exists()

async def dat_changed_safely(reason):
    if not can_check_current_dat():
        recovery_log(f"{reason}: dat check skipped")
        return False

    try:
        old_md5, new_md5 = await asyncio.to_thread(md5_current_dat_sync)
    except Exception as exc:
        recovery_log(f"{reason}: dat check failed: {exc!r}")
        return False

    if old_md5 != new_md5:
        recovery_log(f"{reason}: dat changed; treating direct attempt as done")
        return True

    return False

async def close_browser_safely(browser):
    if browser is None:
        return

    try:
        await browser.close()
    except Exception as exc:
        recovery_log(f"browser close failed: {exc!r}")

async def playwright_post_once(p, attempt, thread_url, post_msg, hp_mode, last_unix):
    global submit_started
    browser = None
    submit_attempted = False

    if hp_mode != "HtP":
        # Keep Chromium closed during the scheduled wait; stale pages have been
        # closing before the submit step.
        await wait_until(last_unix + 3296)

    try:
        browser = await p.chromium.launch(
            headless=True,
            args=[
                "--no-sandbox",
                "--disable-dev-shm-usage",
                "--disable-gpu",
                "--disable-background-networking",
                "--disable-component-update",
                "--disable-sync",
                "--disable-features=MediaRouter",
                "--disable-default-apps",
                "--noerrdialogs",
            ],
        )

        context = await browser.new_context(viewport={"width": 1280, "height": 1024})
        page = await context.new_page()

        log("before python1")

        target_url = thread_url.rstrip("/") + "/l1"
        await page.goto(target_url, wait_until="domcontentloaded", timeout=30000)

        submit_btn = page.locator('input[name="submit"]').first
        await submit_btn.wait_for(state="visible", timeout=10000)

        await page.fill('input[name="FROM"]', "ほしゅ")
        await page.fill('input[name="mail"]', "sage")
        await page.fill('textarea[name="MESSAGE"]', post_msg)
        await page.wait_for_timeout(500)

        log("affter python1")

        if hp_mode != "HtP":
            pw_time = last_unix + 3296

            with log2.open("a", encoding="utf-8") as f:
                f.write(f"LastUnix:{last_unix}\n")
                f.write(f"PW_Time:{pw_time}\n")

            old_md5, new_md5 = await asyncio.to_thread(md5_current_dat_sync)

            if old_md5 != new_md5:
                return "changed"

        log("before python2")

        submit_btn = page.locator('input[name="submit"]').first
        await submit_btn.wait_for(state="visible", timeout=10000)

        try:
            submit_attempted = True
            submit_started = True
            await submit_btn.click(timeout=15000)
        except Exception as exc:
            target_closed = is_target_closed_error(exc)
            if not target_closed:
                traceback.print_exc()
            if await dat_changed_safely(f"attempt={attempt} submit click exception"):
                return "pass"
            if target_closed:
                recovery_log(f"attempt={attempt} page closed during submit click; no fallback click to avoid duplicate post")
            else:
                recovery_log(f"attempt={attempt} submit click exception; no fallback click to avoid duplicate post")
            return "fail_after_submit"

        final_btn_selector = 'input[type="submit"][value*="承諾"]'
        try:
            await page.wait_for_selector(final_btn_selector, timeout=13000)
            await page.click(final_btn_selector)
        except Exception:
            pass

        success = False
        for _ in range(26):
            visible_text = await page.inner_text("body")
            if "書きこみが終わりました" in visible_text:
                success = True
                break
            await asyncio.sleep(0.5)

        try:
            await page.screenshot(path=f"{th_dir}/screenshot.png")
        except Exception as exc:
            recovery_log(f"attempt={attempt} screenshot failed: {exc!r}")

        log("affter python2")

        if success:
            return "pass"

        if await dat_changed_safely(f"attempt={attempt} after submit"):
            return "pass"

        recovery_log(f"attempt={attempt} failed after submit; no retry to avoid duplicate post")
        return "fail_after_submit"

    except Exception as exc:
        if submit_attempted:
            target_closed = is_target_closed_error(exc)
            if not target_closed:
                traceback.print_exc()
            if await dat_changed_safely(f"attempt={attempt} exception after submit"):
                return "pass"
            if target_closed:
                recovery_log(f"attempt={attempt} page closed after submit; no retry to avoid duplicate post")
            else:
                recovery_log(f"attempt={attempt} exception after submit; no retry to avoid duplicate post")
            return "fail_after_submit"

        raise

    finally:
        await close_browser_safely(browser)

async def run():
    thread_url = os.environ.get("CurrentThreadURL")
    post_msg = os.environ.get("PostMsg")
    hp_mode = os.environ.get("HPMode", "1")
    last_unix = int(os.environ.get("LastUnix", "0") or "0")

    if not thread_url:
        raise RuntimeError("CurrentThreadURL is empty")
    if post_msg is None:
        raise RuntimeError("PostMsg is empty")

    async with async_playwright() as p:
        for attempt in range(1, max_playwright_attempts + 1):
            try:
                result = await playwright_post_once(
                    p,
                    attempt,
                    thread_url,
                    post_msg,
                    hp_mode,
                    last_unix,
                )

                print(result)
                return

            except Exception as exc:
                recovery_log(f"attempt={attempt} failed before submit: {exc!r}")

                if attempt < max_playwright_attempts:
                    recovery_log(f"attempt={attempt} retrying with fresh browser")
                    await asyncio.sleep(min(5, attempt * 2))

        recovery_log("all playwright attempts failed before submit; no adb/2chMate fallback")
        print("fail")

def _context_has_broken_pipe(context):
    exc = context.get("exception")
    if isinstance(exc, BrokenPipeError):
        return True

    future = context.get("future")
    if future is None:
        return False

    try:
        fut_exc = future.exception()
    except BaseException:
        return False

    return isinstance(fut_exc, BrokenPipeError)

def run_with_pipe_warning_filter():
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)

    def handle_loop_exception(loop, context):
        if not submit_started and _context_has_broken_pipe(context):
            return
        loop.default_exception_handler(context)

    loop.set_exception_handler(handle_loop_exception)

    try:
        loop.run_until_complete(run())
    finally:
        try:
            loop.run_until_complete(loop.shutdown_asyncgens())
        finally:
            asyncio.set_event_loop(None)
            loop.close()

if __name__ == "__main__":
    try:
        run_with_pipe_warning_filter()
    except Exception:
        traceback.print_exc()
        print("fail")
EOF
)
PY_STATUS=$?
set -e

if [ "$PY_STATUS" -ne 0 ] || [ -z "$tmpRESULT" ]; then
    tmpRESULT="fail"
fi

echo "tmpRESULT:$tmpRESULT $(date "+%Y-%m-%d %H:%M:%S")" >>"$THDir/Log2"

record_pass_without_clearing_auto_recovery_log() {
    local recovery_file="$THDir/auto_recovery.log"
    local recovered_file
    local recovered_name

    if [ ! -s "$recovery_file" ]; then
        return 0
    fi

    retain_auto_recovery_log() {
        printf "%s tmpRESULT:%s; retained existing recovery evidence; pass alone is not recovery proof\n" \
            "$(date "+%Y-%m-%d %H:%M:%S")" "$tmpRESULT" \
            >> "$recovery_file" || :
    }

    if [ ! -s "$THDir/CurrentThreadNum.dat" ]; then
        retain_auto_recovery_log
        return 0
    fi

    if [ -s "$THDir/Log" ] && grep -Eq '異常|thread-dead|thread dead|abnormal' "$THDir/Log"; then
        retain_auto_recovery_log
        return 0
    fi

    # These markers are not safe to age out based on pass alone.
    if grep -Eq 'fail_after_submit|after submit|submit click|page closed|tmpRESULT:changed|DAT changed before submit|not treated as recovered success|dat changed|dat check failed|curl returned|curl failed|CurrentThreadNum\.dat is empty|CurrentThreadNum\.dat is not DAT-like|thread-dead|thread dead|abnormal|異常' "$recovery_file"; then
        retain_auto_recovery_log
        return 0
    fi

    # Browser startup failures happen before submit; after a later clean pass,
    # keep the evidence in an archive so the live marker can recover.
    if grep -Eq 'failed before submit|retrying with fresh browser|all playwright attempts failed before submit|tmpRESULT:fail; no adb/2chMate fallback|tmp mode disabled' "$recovery_file"; then
        recovered_file="$THDir/auto_recovery_recovered_$(date +%Y%m%d_%H%M%S).log"
        recovered_name="${recovered_file##*/}"
        if mv "$recovery_file" "$recovered_file"; then
            printf "%s recovered tmpRESULT:%s; archived stale pre-submit recovery markers to %s\n" \
                "$(date "+%Y-%m-%d %H:%M:%S")" "$tmpRESULT" "$recovered_name" \
                > "$recovery_file" || :
        else
            retain_auto_recovery_log
        fi
        return 0
    fi

    retain_auto_recovery_log

    return 0
}

if [ "$tmpRESULT" = "changed" ]; then
    echo "$tmpRESULT" > "$THDir/restemp"
    printf "%s tmpRESULT:changed; DAT changed before submit; not treated as recovered success\n" \
        "$(date "+%Y-%m-%d %H:%M:%S")" \
        >> "$THDir/auto_recovery.log" || :
    echo setumei >> "$THDir/setumei"
    exit 0
fi

if [ "$HPMode" = "HtP" ]; then
    echo "$CurrentThreadNum" > "$THDir/LHtPNum"
fi

echo "$tmpRESULT" > "$THDir/restemp"

if [ "$tmpRESULT" = "fail_after_submit" ]; then
    exit 1
fi

if [ "$tmpRESULT" = "pass" ]; then
    record_pass_without_clearing_auto_recovery_log || :
    exit 0
fi

echo "$(date "+%Y-%m-%d %H:%M:%S") tmpRESULT:$tmpRESULT; no adb/2chMate fallback" >> "$THDir/auto_recovery.log"
exit 1

;;

tmp)

echo "$(date "+%Y-%m-%d %H:%M:%S") tmp mode disabled; no adb/2chMate fallback" >> "$THDir/auto_recovery.log"
echo fail > "$THDir/restemp"
exit 1

;;

esac
