"""Capture the REAL portal screens shown on /how-it-works.

Every window on that page is a static HTML copy of a portal page as the demo member "Ryan"
(member@propertylab.test) sees it — the live Vue code rendering the live database — with every script,
token and link removed (snaplib.py). This script makes those copies, plus a manifest of where each
highlighted card sits, so the page can scroll each window to the card a beat talks about.

    python3 tools/how-it-works/capture.py            # desktop (1440) and phone (390) copies
    python3 tools/how-it-works/capture.py --only dash_d,ai_d

Needs Playwright (Python) with Chrome, and runs on the server: it signs Ryan in through a one-time magic
link written to the cache with `php artisan tinker`. Output: public/cinematic/how/ (s/ pages, a/ images,
css/ stylesheets, manifest.json). Re-run it after a portal screen changes; the page picks the new copies up.
"""
import argparse, asyncio, json, os, shutil, subprocess, sys

sys.path.insert(0, os.path.dirname(__file__))
from snaplib import Snap
from playwright.async_api import async_playwright

ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
OUT = os.path.join(ROOT, 'public', 'cinematic', 'how')
PUB = '/cinematic/how'
BASE = 'https://wk.propertylab.com.my'
EMAIL = 'member@propertylab.test'
UNIT = '/property/my-properties/76aa9738-4f3b-40a6-8979-6d7e70db2c83'
REQUEST = '/property-concierge/244d0d19-de27-4ecf-b438-858bbb935e20'
CHAT = '/property/ai-advisor?c=2f3ed752-0192-438e-b6ba-438568bba038'
PROJECT = '/my/projects/binastra-cochrane'

# name, url, signed in?, clicks (button/tab text, in order), marks {key: [phrases]}
# A mark is the box around its phrases: a plain phrase counts as its whole card, "=phrase" as the text alone.
SCREENS = [
    ('dash', '/dashboard', True, [], {'kpi': ['Portfolio value', 'Monthly cash flow'], 'unit': ['My Portfolio'], 'todo': ['Action Centre', 'Wealth goal']}),
    ('wp', '/wealth-planning', True, [], {'stand': ['=Where you stand', 'What you want', 'Properties in this plan'], 'next': ['Next best move', '=Book my Property Deal Day']}),
    ('wpbank', '/wealth-planning', True, ["The bank's view"], {'bank': ['=The three questions behind your plan', 'What the properties build', 'What a bank would approve']}),
    ('apguest', PROJECT, False, [], {'tabs': ['Tenure', 'Sale status']}),
    ('apinv', PROJECT, True, ['Investment Analysis'], {'inv': ['Deep Market Analysis']}),
    ('apfin', PROJECT, True, ['Financial Projection'], {'fin': ['Financial Projection · Loan & Equity']}),
    ('lh', '/property/academy', True, [], {'tools': ['=Which tool do you want to learn first?', '01 / Plan', '03 / Apply']}),
    ('lhroad', '/property/academy', True, ['DMAIC Road'], {'road': ['The DMAIC Road', '=8 stops to think buying a property through.', 'Next Round']}),
    ('lhel', '/property/academy', True, ['e-Learning'], {'el': ['=Your courses', '=Unlock more', 'Monthly Zoom Call Coaching Replay']}),
    ('aichat', CHAT, True, [], {'q': ["=My plan says I'm RM 2,559 a month short of my RM 6,000 target at 45"], 'a': ["=Let's break down where you stand", '=What are your options to close']}),
    ('aiip', '/property/ai-advisor', True, ['Investment Prompt'], {'ip': ['The Master Prompt Playbook']}),
    ('ll', '/property/landlord-management', True, [], {'props': ['=My properties', 'Maxim Risen'], 'antserv': ['PropertyLab and Antserv work together after the keys.']}),
    ('llunit', UNIT, True, ['Renovation steps'], {'steps': ['Key collection', 'Renovation']}),
    ('llreq', REQUEST, True, [], {'req': ['Property details', 'Timeline & messages']}),
]
WIDTHS = {'d': (1440, 900), 'm': (390, 844)}

# Where a mark's rectangle comes from: the smallest element whose text starts with the phrase, widened to
# its card (the nearest rounded container at least 240px wide), or the phrase's own section when none.
MARK_JS = r"""
(texts) => {
  const vis = e => { const r = e.getBoundingClientRect(); return r.width > 0 && r.height > 0 };
  // lower-cased: innerText follows CSS text-transform, so an uppercase label reads "WHAT YOU WANT"
  const norm = s => (s || '').replace(/[‘’]/g, "'").replace(/\s+/g, ' ').trim().toLowerCase();
  const cardish = c => { const s = getComputedStyle(c); return parseFloat(s.borderTopLeftRadius) >= 8 && (s.backgroundColor !== 'rgba(0, 0, 0, 0)' || s.boxShadow !== 'none' || s.borderTopWidth !== '0px') };
  const card = e => { let c = e; for (let i = 0; i < 14 && c && c !== document.body; i++) { if (cardish(c) && c.getBoundingClientRect().width >= 240) return c; c = c.parentElement } return e };
  let box = null;
  for (let t of texts) {
    const bare = t.startsWith('='); if (bare) t = t.slice(1); t = norm(t);
    const cands = [...document.querySelectorAll('body *')].filter(e => vis(e) && e.children.length <= 3 && norm(e.innerText).startsWith(t));
    if (!cands.length) return { miss: t };
    cands.sort((a, b) => a.innerText.length - b.innerText.length);
    const r = (bare ? cands[0] : card(cands[0])).getBoundingClientRect();
    const b = [r.left + scrollX, r.top + scrollY, r.right + scrollX, r.bottom + scrollY];
    box = box ? [Math.min(box[0], b[0]), Math.min(box[1], b[1]), Math.max(box[2], b[2]), Math.max(box[3], b[3])] : b;
  }
  return { rect: [Math.round(box[0]), Math.round(box[1]), Math.round(box[2] - box[0]), Math.round(box[3] - box[1])] };
}
"""

# Click the smallest visible button / link / tab whose text contains the phrase (quotes normalised).
CLICK_JS = r"""
(t) => {
  const norm = s => (s || '').replace(/[\u2018\u2019]/g, "'").replace(/\s+/g, ' ').trim();
  const els = [...document.querySelectorAll('button, a, [role=tab]')].filter(e => { const r = e.getBoundingClientRect(); return r.width > 0 && norm(e.innerText).includes(t) });
  if (!els.length) return false;
  els.sort((a, b) => a.innerText.length - b.innerText.length); els[0].click(); return true;
}
"""


def login_state(path):
    hexkey = subprocess.check_output(['php', 'artisan', 'tinker', '--execute',
        f"$h=bin2hex(random_bytes(24)); Cache::put('auth:magic:'.$h, ['email'=>'{EMAIL}','portal'=>'main'], now()->addMinutes(10)); echo $h;"],
        cwd=ROOT, text=True).strip().splitlines()[-1]
    return hexkey


async def main(only):
    if not only and os.path.isdir(OUT):
        shutil.rmtree(OUT)
    os.makedirs(OUT, exist_ok=True)
    mpath = os.path.join(OUT, 'manifest.json')
    manifest = json.load(open(mpath)) if only and os.path.exists(mpath) else {}
    async with async_playwright() as p:
        b = await p.chromium.launch(executable_path='/usr/bin/google-chrome', args=['--use-angle=swiftshader', '--enable-unsafe-swiftshader'])
        for wkey, (w, h) in WIDTHS.items():
            mobile = wkey == 'm'
            opts = dict(viewport={'width': w, 'height': h}, device_scale_factor=1, is_mobile=mobile, has_touch=mobile)
            member = await b.new_context(**opts)
            pg0 = await member.new_page()
            await pg0.goto(f'{BASE}/auth/login-link/{login_state(None)}', wait_until='load'); await pg0.wait_for_timeout(1500)
            assert '/dashboard' in pg0.url, 'sign-in failed: ' + pg0.url
            guest = await b.new_context(**opts)
            for name, url, signed, clicks, marks in SCREENS:
                key = f'{name}_{wkey}'
                if only and key not in only:
                    continue
                ctx = member if signed else guest
                pg = await ctx.new_page()
                await pg.goto(BASE + url, wait_until='load'); await pg.wait_for_timeout(4500)
                for c in clicks:
                    ok = await pg.evaluate(CLICK_JS, c)
                    if not ok:
                        print('  NO CLICK', name, c)
                    await pg.wait_for_timeout(3500)
                # inner scroll boxes too: a chat opens scrolled to its last message, which hides the question
                await pg.evaluate('() => { scrollTo(0, 0); document.querySelectorAll("body *").forEach(e => { if (e.scrollTop) e.scrollTop = 0 }) }')
                await pg.wait_for_timeout(300)
                rects = {}
                for mk, texts in marks.items():
                    r = await pg.evaluate(MARK_JS, texts)
                    if 'miss' in r:
                        print('  MISS', key, mk, r['miss'])
                    else:
                        rects[mk] = r['rect']
                lib = Snap(ctx, OUT, PUB)
                info = await lib.save(pg, key, note=f'{url} as Ryan ({"signed in" if signed else "guest"}), {w}px.')
                manifest[key] = {'src': f'{PUB}/s/{key}.html', 'w': w, 'h': info['h'], 'marks': rects}
                print(key, info['h'], rects, info['bytes'], lib.log[:3])
                await pg.close()
            await member.close(); await guest.close()
        await b.close()
    json.dump(manifest, open(mpath, 'w'), indent=1)


if __name__ == '__main__':
    ap = argparse.ArgumentParser(); ap.add_argument('--only', default='')
    a = ap.parse_args()
    asyncio.run(main(set(x for x in a.only.split(',') if x)))
