#!/usr/bin/env node
'use strict';

const childProcess = require('node:child_process');
const fs = require('node:fs');
const http = require('node:http');
const path = require('node:path');
const readline = require('node:readline/promises');
const { stdin, stdout } = require('node:process');
const { chromium } = require('playwright-core');

const PROJECT_ROOT = path.resolve(__dirname, '..');
const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_PORT = 8788;
const DEFAULT_API_BASE = 'https://api.atuchat.com/';
const DEFAULT_URL =
    '/aslan/recharge-activity/index.html?app_code=aslan&codex_webview=1';
const DEFAULT_CHROME_PATHS = [
    '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
    '/Applications/Chromium.app/Contents/MacOS/Chromium',
    '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
];

function parseArgs(argv) {
    const options = {
        account: '',
        password: '',
        token: '',
        url: '',
        apiBase: DEFAULT_API_BASE,
        host: DEFAULT_HOST,
        port: DEFAULT_PORT,
        origin: 'ATYOU',
        originChild: '',
        lang: 'en',
        chrome: '',
        headless: false,
        devtools: false,
        disableWebSecurity: true,
        navigationPrompt: true,
        closeAfter: 0,
        screenshot: '',
    };
    const aliases = {
        '-a': 'account',
        '--account': 'account',
        '-p': 'password',
        '--password': 'password',
        '--pwd': 'password',
        '--token': 'token',
        '-u': 'url',
        '--url': 'url',
        '--api-base': 'apiBase',
        '--host': 'host',
        '--port': 'port',
        '--origin': 'origin',
        '--origin-child': 'originChild',
        '--lang': 'lang',
        '--chrome': 'chrome',
        '--close-after': 'closeAfter',
        '--screenshot': 'screenshot',
    };

    for (let i = 0; i < argv.length; i += 1) {
        const raw = argv[i];
        const [flag, inlineValue] = raw.split(/=(.*)/s);
        if (flag === '--help' || flag === '-h') {
            options.help = true;
            continue;
        }
        if (flag === '--headless') {
            options.headless = true;
            continue;
        }
        if (flag === '--devtools') {
            options.devtools = true;
            continue;
        }
        if (flag === '--no-disable-web-security') {
            options.disableWebSecurity = false;
            continue;
        }
        if (flag === '--no-nav-prompt') {
            options.navigationPrompt = false;
            continue;
        }
        const key = aliases[flag];
        if (!key) {
            throw new Error(`Unknown argument: ${raw}`);
        }
        const value = inlineValue !== undefined ? inlineValue : argv[++i];
        if (value === undefined) throw new Error(`Missing value for ${raw}`);
        options[key] =
            key === 'port' || key === 'closeAfter' ? Number(value) : value;
    }
    options.originChild = options.originChild || options.origin;
    return options;
}

function usage() {
    return [
        'Usage:',
        '  npm run aslan:webview',
        '  npm run aslan:webview -- --account 6666 --password 123456 --url /aslan/recharge-activity/index.html?app_code=aslan',
        '',
        'Options:',
        '  -a, --account <value>        Aslan login account or special ID',
        '  -p, --password <value>       Aslan login password',
        '      --token <value>          Existing Aslan token; skips account password login',
        '  -u, --url <value>            Target H5 URL. Relative paths use the local H5 dev server.',
        '      --api-base <url>         API base, default https://api.atuchat.com/',
        '      --origin <value>         Req-Sys-Origin origin, default ATYOU',
        '      --origin-child <value>   Req-Sys-Origin originChild, default same as origin',
        '      --lang <value>           Req-Lang, default en',
        '      --chrome <path>          Chrome executable path',
        '      --headless               Run browser headless',
        '      --devtools               Open DevTools',
        '      --no-disable-web-security  Keep browser web security enabled',
        '      --no-nav-prompt          Do not prompt for more URLs after opening',
        '      --close-after <seconds>  Auto close, useful for smoke tests',
        '      --screenshot <path>      Save a screenshot after initial load',
    ].join('\n');
}

function normalizeBaseURL(value) {
    return String(value || DEFAULT_API_BASE).replace(/\/+$/, '');
}

function resolveChromePath(explicitPath) {
    if (explicitPath) return explicitPath;
    return (
        DEFAULT_CHROME_PATHS.find((candidate) => fs.existsSync(candidate)) || ''
    );
}

function normalizeTargetURL(input, host, port) {
    const value = String(input || DEFAULT_URL).trim() || DEFAULT_URL;
    if (/^https?:\/\//i.test(value)) return value;
    const cleanPath = value.startsWith('/')
        ? value
        : `/${value.replace(/^\.?\//, '')}`;
    return `http://${host}:${port}${cleanPath}`;
}

function isLocalTarget(url, host, port) {
    try {
        const parsed = new URL(url);
        return (
            parsed.hostname === host &&
            String(parsed.port || 80) === String(port)
        );
    } catch (_) {
        return false;
    }
}

function healthURL(host, port) {
    return `http://${host}:${port}/__h5_dev_server__/health`;
}

function wait(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}

function requestHealth(url) {
    return new Promise((resolve) => {
        const req = http.get(url, (res) => {
            res.resume();
            resolve(res.statusCode === 200);
        });
        req.on('error', () => resolve(false));
        req.setTimeout(800, () => {
            req.destroy();
            resolve(false);
        });
    });
}

async function ensureLocalServer(host, port) {
    const url = healthURL(host, port);
    if (await requestHealth(url)) return null;
    const server = childProcess.spawn(
        'python3',
        [
            path.join(PROJECT_ROOT, 'scripts/h5_dev_server.py'),
            '--host',
            host,
            '--port',
            String(port),
            '--directory',
            PROJECT_ROOT,
        ],
        {
            cwd: PROJECT_ROOT,
            stdio: ['ignore', 'pipe', 'pipe'],
        }
    );
    server.stdout.on('data', (chunk) =>
        process.stdout.write(`[h5-server] ${chunk}`)
    );
    server.stderr.on('data', (chunk) =>
        process.stderr.write(`[h5-server] ${chunk}`)
    );
    for (let i = 0; i < 30; i += 1) {
        if (await requestHealth(url)) return server;
        await wait(200);
    }
    server.kill();
    throw new Error(`Failed to start local H5 server: ${url}`);
}

async function askRequired(rl, label, currentValue) {
    if (currentValue) return currentValue;
    const value = (await rl.question(label)).trim();
    if (!value)
        throw new Error(`${label.replace(/[:：]\s*$/, '')} is required`);
    return value;
}

async function askPassword(rl, currentValue) {
    if (currentValue) return currentValue;
    if (!stdin.isTTY) return askRequired(rl, 'Password: ', currentValue);
    return new Promise((resolve, reject) => {
        let value = '';
        stdout.write('Password: ');
        stdin.setRawMode(true);
        stdin.resume();
        const onData = (buffer) => {
            const text = buffer.toString('utf8');
            if (text === '\u0003') {
                stdin.setRawMode(false);
                stdin.off('data', onData);
                reject(new Error('Cancelled'));
                return;
            }
            if (text === '\r' || text === '\n') {
                stdin.setRawMode(false);
                stdin.off('data', onData);
                stdout.write('\n');
                resolve(value);
                return;
            }
            if (text === '\u007f') {
                if (value.length > 0) {
                    value = value.slice(0, -1);
                    stdout.write('\b \b');
                }
                return;
            }
            value += text;
            stdout.write('*');
        };
        stdin.on('data', onData);
    });
}

function buildReqAppIntel(model, channel, imei) {
    return [
        'build=1',
        'version=1.0.0',
        `model=${model}`,
        'sysVersion=web',
        `channel=${channel}`,
        `Req-Imei=${imei}`,
    ].join(';');
}

async function login(options) {
    const imei = 'codex-aslan-webview';
    const response = await fetch(
        `${normalizeBaseURL(options.apiBase)}/auth/account/login`,
        {
            method: 'POST',
            headers: {
                'content-type': 'application/json',
                'req-lang': options.lang,
                'req-client': 'Android',
                'req-imei': imei,
                'req-app-intel': buildReqAppIntel(
                    'CodexWebViewLogin',
                    'Codex',
                    imei
                ),
                'req-sys-origin': `origin=${options.origin};originChild=${options.originChild}`,
                'req-version': 'V2',
                'req-zone': 'Asia/Shanghai',
                'user-agent': 'curl/8.7.1',
            },
            body: JSON.stringify({
                account: options.account,
                pwd: options.password,
            }),
        }
    );
    const text = await response.text();
    let payload;
    try {
        payload = text ? JSON.parse(text) : {};
    } catch (_) {
        throw new Error(
            `Login returned non-JSON response: HTTP ${response.status} ${text.slice(0, 200)}`
        );
    }
    if (
        !response.ok ||
        payload.status === false ||
        !payload.body ||
        !payload.body.token
    ) {
        throw new Error(
            `Login failed: HTTP ${response.status} ${payload.errorMsg || payload.message || text.slice(0, 200)}`
        );
    }
    return {
        token: payload.body.token,
        profile: payload.body.userProfile || {},
    };
}

function buildInitScriptPayload(options, token) {
    const imei = 'codex-aslan-webview';
    return {
        token,
        lang: options.lang,
        origin: options.origin,
        originChild: options.originChild,
        reqAppIntel: buildReqAppIntel('CodexFlutterWebView', 'Codex', imei),
    };
}

async function openWebView(options, token, firstURL) {
    const chromePath = resolveChromePath(options.chrome);
    if (!chromePath) {
        throw new Error(
            'Chrome executable not found. Pass --chrome /path/to/chrome.'
        );
    }
    const launchArgs = ['--window-size=390,844'];
    if (options.disableWebSecurity) {
        launchArgs.push('--disable-web-security');
        launchArgs.push('--disable-features=IsolateOrigins,site-per-process');
    }
    const browser = await chromium.launch({
        executablePath: chromePath,
        headless: options.headless,
        devtools: options.devtools,
        args: launchArgs,
    });
    const context = await browser.newContext({
        viewport: { width: 390, height: 844 },
        deviceScaleFactor: 3,
        isMobile: true,
        hasTouch: true,
        userAgent:
            'Mozilla/5.0 (Linux; Android 14; Aslan Flutter WebView) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.0.0 Mobile Safari/537.36',
    });
    await context.addInitScript(
        (config) => {
            window.FlutterApp = window.FlutterApp || {
                postMessage(data) {
                    console.log('[FlutterApp.postMessage]', data);
                },
            };
            window.FlutterPageControl = window.FlutterPageControl || {
                postMessage(data) {
                    console.log('[FlutterPageControl.postMessage]', data);
                },
            };
            window.app = window.app || {};
            window.app.getAccessOrigin = function getAccessOrigin() {
                return JSON.stringify({
                    Authorization: `Bearer ${config.token}`,
                    'Req-Lang': config.lang,
                    'Req-App-Intel': config.reqAppIntel,
                    'Req-Sys-Origin': `origin=${config.origin};originChild=${config.originChild}`,
                });
            };
            window.app.getAuth = function getAuth() {
                return window.app.getAccessOrigin();
            };
            window.app.sendToFlutter = function sendToFlutter(data) {
                window.FlutterApp.postMessage(data);
            };
        },
        buildInitScriptPayload(options, token)
    );

    const page = await context.newPage();
    page.on('console', (message) => {
        const type = message.type();
        if (type === 'error' || type === 'warning') {
            console.log(`[webview:${type}] ${message.text()}`);
        }
    });
    await page.goto(firstURL, {
        waitUntil: 'domcontentloaded',
        timeout: 30000,
    });
    await page
        .waitForLoadState('networkidle', { timeout: 15000 })
        .catch(() => {});
    return { browser, page };
}

async function promptNavigationLoop(rl, page, options) {
    console.log(
        '输入新的 URL 回车可跳转；输入 q 退出。相对路径会走本地 H5 服务。'
    );
    while (!page.isClosed()) {
        const next = (await rl.question('WebView URL> ')).trim();
        if (!next) continue;
        if (next.toLowerCase() === 'q' || next.toLowerCase() === 'quit') break;
        const targetURL = normalizeTargetURL(next, options.host, options.port);
        if (isLocalTarget(targetURL, options.host, options.port)) {
            await ensureLocalServer(options.host, options.port);
        }
        console.log(`Navigate: ${targetURL}`);
        await page.goto(targetURL, {
            waitUntil: 'domcontentloaded',
            timeout: 30000,
        });
        await page
            .waitForLoadState('networkidle', { timeout: 15000 })
            .catch(() => {});
    }
}

async function main() {
    const options = parseArgs(process.argv.slice(2));
    if (options.help) {
        console.log(usage());
        return;
    }
    options.account = options.account || process.env.ASLAN_ACCOUNT || '';
    options.password = options.password || process.env.ASLAN_PASSWORD || '';
    options.token = options.token || process.env.ASLAN_TOKEN || '';
    options.url = options.url || process.env.ASLAN_WEBVIEW_URL || '';

    const rl = readline.createInterface({ input: stdin, output: stdout });
    let localServer = null;
    let browser = null;
    try {
        if (!options.token) {
            options.account = await askRequired(
                rl,
                'Account: ',
                options.account
            );
            options.password = await askPassword(rl, options.password);
        }
        options.url =
            options.url ||
            (await rl.question(`URL [${DEFAULT_URL}]: `)).trim() ||
            DEFAULT_URL;

        const targetURL = normalizeTargetURL(
            options.url,
            options.host,
            options.port
        );
        if (isLocalTarget(targetURL, options.host, options.port)) {
            localServer = await ensureLocalServer(options.host, options.port);
        }

        const loginResult = options.token
            ? { token: options.token, profile: { account: 'token' } }
            : await login(options);
        const displayAccount =
            loginResult.profile.account ||
            loginResult.profile.id ||
            options.account;
        console.log(
            `${options.token ? 'Token loaded' : 'Login OK'}: ${loginResult.profile.userNickname || '-'} / ${displayAccount}`
        );
        console.log(`Open WebView: ${targetURL}`);

        const result = await openWebView(options, loginResult.token, targetURL);
        browser = result.browser;

        if (options.screenshot) {
            await result.page.screenshot({
                path: path.resolve(options.screenshot),
                fullPage: true,
            });
            console.log(
                `Screenshot saved: ${path.resolve(options.screenshot)}`
            );
        }

        if (options.closeAfter > 0) {
            await wait(options.closeAfter * 1000);
            return;
        }

        if (options.navigationPrompt && !options.headless) {
            await promptNavigationLoop(rl, result.page, options);
            return;
        }

        await result.page.waitForEvent('close').catch(() => {});
    } finally {
        rl.close();
        if (browser) await browser.close().catch(() => {});
        if (localServer) localServer.kill();
    }
}

main().catch((error) => {
    console.error(error.message || error);
    process.exit(1);
});
