#!/usr/bin/env python3
# ---------------------------------------------------------------------------
# service-index.html — the "What We Do" index page.
#
# Adi 2026-08-31: "saya ingin membuat halaman Service Index ... dengan header,
# hero, ctatab dan footer yang sesuai seperti halaman halaman sebelumnya. dan
# tidak perlu sidebar."
#
# The shell is settled, so none of it is designed here. It is EXECUTED out of
# the files that already build it — the same four calls your-team.html makes:
#   header  HNS["_header"]()    footer  HNS["_footer"]()
#   hero    HNS["_hero_css"]() + the option sheet's A1() builder
#   CTA tab HNS["_consult"]()
#
# What IS new is the index itself, and it is built from two crawled sources
# rather than written:
#   team_menu.json  — the LIVE site's menu: the real service tree and the real
#                     URLs. This decides what exists.
#   home_v1.html    — the mega menus: how the rebuild GROUPS the injury claims,
#                     and each area's paragraph and icon. This decides how it
#                     reads.
# Every name in the grouping is resolved against the live tree, and the build
# fails on any that does not resolve. That is what stops the page promising a
# page the site does not have — see NOT-PAGES below.
# ---------------------------------------------------------------------------
import io, json, os, re, sys

OUT     = "/srv/apps/leaguelaw/wp-content/prototype/service-index.html"
HGEN    = "/srv/apps/leaguelaw/prototyping/gen_inner_replica_hero.py"
HOME_V1 = "/srv/apps/leaguelaw/wp-content/prototype/home_v1.html"
HERE    = os.path.dirname(os.path.abspath(__file__))

_hsrc = io.open(HGEN, encoding="utf-8").read()
HNS = {"__name__": "gen_inner_replica_hero", "__file__": HGEN}
exec(compile(_hsrc, HGEN, "exec"), HNS)
CSS = HNS["CSS"]
assert HNS["SCOPE"] == ".shero"

MENU  = json.load(io.open(os.path.join(HERE, "team_menu.json"), encoding="utf-8"))
INTRO = io.open(os.path.join(HERE, "services_intro_body.html"), encoding="utf-8").read().strip()
assert INTRO.startswith("<h2>") and "collaboratively" in INTRO


# ---------------------------------------------------------------------------
# THE LIVE TREE — what actually exists
# ---------------------------------------------------------------------------
def live_tree():
    """{live top-level name: (href, [(name, href, depth), ...])} under What We Do."""
    top = next(i for i, m in enumerate(MENU) if m["href"] == "/our-services/")
    end = next((i for i in range(top + 1, len(MENU)) if MENU[i]["depth"] == 0), len(MENU))
    kids, order = {}, []
    cur = None
    for m in MENU[top + 1:end]:
        if m["depth"] == 1:
            cur = m["text"]
            order.append(cur)
            kids[cur] = (m["href"], [])
        else:
            kids[cur][1].append((m["text"], m["href"], m["depth"]))
    assert order == ["Injury Claims", "Estates", "Business Law", "Real Estate", "Marine Law"], order
    return kids, order


# ---------------------------------------------------------------------------
# home_v1's mega menus — the grouping, the paragraph, the icon
# ---------------------------------------------------------------------------
def megas():
    h = io.open(HOME_V1, encoding="utf-8").read()
    head = h[h.index("<!-- 1 · header"):h.index("<!-- 2 · triage")]
    idx = [m.start() for m in re.finditer(r'<div class="mi"[^>]*><a href="[^"]*">', head)] + [len(head)]
    out = []
    for k in range(len(idx) - 1):
        blk = head[idx[k]:idx[k + 1]]
        name = re.search(r'<a href="[^"]*">([^<]+)</a>', blk).group(1)
        if name == "Your Team":
            continue
        cols = re.findall(r'<div class="mcol">(.*?)</div>\s*(?=<div class="mcol">|<div class="rail">)',
                          blk, re.S)
        groups = []
        for c in cols:
            gh = re.search(r'<div class="mh">([^<]*)</div>', c)
            names = [t for _, t in re.findall(r'<a href="([^"]*)">([^<]*)</a>', c)]
            groups.append((gh.group(1) if gh else "", names))
        rail = re.search(r'<div class="rail">\s*<img src="([^"]+)".*?<div class="rt ([ot])">.*?<p>(.*?)</p>',
                         blk, re.S)
        assert rail, "the %s mega has no rail" % name
        out.append({"name": name, "icon": rail.group(1), "tone": rail.group(2),
                    "copy": rail.group(3).strip(), "groups": groups})
    assert [a["name"] for a in out] == ["Injuries", "Estates", "Business", "Marine", "Real Estate"], \
        [a["name"] for a in out]
    return {a["name"]: a for a in out}


# home_v1's name for an area -> the live menu's name for the same area.
# THREE naming schemes are in play and none of them agree: the rebuild's nav
# says Injuries / Estates / Business / Marine / Real Estate, the live menu says
# Injury Claims / Estates / Business Law / Real Estate / Marine Law, and the
# live services page's own tiles say INJURIES / Estate Law / Business Law /
# Marine Law / Real Estate Law. The map is written out so the disagreement is
# visible instead of being smoothed over by a slug.
LIVE_NAME = {"Injuries": "Injury Claims", "Estates": "Estates", "Business": "Business Law",
             "Marine": "Marine Law", "Real Estate": "Real Estate"}


def areas():
    kids, order = live_tree()
    mg = megas()
    # ORDER: the live menu's, because this is the live site's index page.
    # home_v1's nav puts Marine before Real Estate. That disagreement is old —
    # it is flag 4 of the practice-areas round, still nobody's decision.
    rev = {v: k for k, v in LIVE_NAME.items()}
    out = []
    notpages = []
    for live in order:
        ours = rev[live]
        a = dict(mg[ours])
        a["href"], flat = kids[live]
        byname = {re.sub(r"&#0?38;", "&", n): (n, hr) for n, hr, _ in flat}
        groups = []
        for gh, names in a["groups"]:
            items = []
            for n in names:
                key = re.sub(r"&amp;", "&", n)
                if key in byname:
                    items.append((n, byname[key][1]))
                else:
                    notpages.append((ours, n))
            if items:
                groups.append((gh, items))
        # A GROUP HEADING CAN BE A PAGE. home_v1's mega uses "Estate Disputes"
        # as a column LABEL, but on the live site that is a real page with the
        # four claims beneath it. On a menu a label is fine; on an index page
        # it would be the one heading in the section that cannot be reached.
        # So a heading that resolves to a live page not already listed becomes
        # a link and counts as one of that area's pages.
        used = {hr for _, items in groups for _, hr in items}
        linked = []
        for gh, items in groups:
            key = re.sub(r"&amp;", "&", gh)
            hr = byname[key][1] if key in byname and byname[key][1] not in used else None
            linked.append((gh, hr, items))
        groups = linked
        a["groups"] = groups
        a["count"] = sum(len(i) for _, _, i in groups) + sum(1 for _, hr, _ in groups if hr)
        assert a["count"] == len(flat) or not groups, \
            "%s: accounted for %d of %d live pages" % (ours, a["count"], len(flat))
        out.append(a)
    # NOT-PAGES. home_v1's mega names topics under Business and Marine that the
    # live site has no page for. Rendering them would be the padding the
    # practice-areas round already ruled against ("the two empty cards shown
    # honestly rather than padded"), so they are dropped from the page and
    # reported here instead. The count is asserted: if a page appears for one
    # of them, this build fails and the index gains it.
    assert sorted(set(a for a, _ in notpages)) == ["Business", "Marine"], notpages
    assert len(notpages) == 6, "%d topics without pages, not the 6 measured" % len(notpages)
    assert [a["count"] for a in out] == [13, 7, 0, 2, 0], [a["count"] for a in out]
    return out, notpages


AREAS, NOTPAGES = areas()

# ---------------------------------------------------------------------------
HERO_PHOTO  = "/wp-content/uploads/law-1.jpg"
HERO_CRUMBS = ('<nav class="crumbs %s"><a href="/">Home</a><i></i>'
               '<span class="here">What We Do</span></nav>')
HERO_TITLE  = '<h1 class="htitle">What We Do</h1>'
# the live page header carries no sub-line at all. A1 has a slot for one, and
# the five areas are the most useful thing that can go in it: the hero then
# says what the page contains before a pixel of the index has scrolled.
HERO_SUB    = " · ".join(a["name"] for a in AREAS)


def hero_css():
    return HNS["_hero_css"]() + (
        "/* law-1.jpg is 5.42:1 in a 1.11:1 panel: `cover` crops the WIDTH, so\n"
        "   this is the only control over what the panel shows. 70% is the\n"
        "   statue; the 50% default is the blur behind it. */\n"
        ".shero .ph img{object-position:70% 50%}\n")


def hero_markup():
    r2 = io.open(HNS["R2GEN"], encoding="utf-8").read()
    ns = {}
    exec(compile(r2[r2.index("PHOTO = "):r2.index('HEAD = """')], HNS["R2GEN"], "exec"), ns)
    _hex = ns["hexicon"]
    ns["PHOTO"], ns["CRUMBS"], ns["TITLE"] = HERO_PHOTO, HERO_CRUMBS, HERO_TITLE
    ns["hexicon"] = lambda size="m", tone="w": _hex(size, tone).replace("fa-car", "fa-balance-scale")
    html = ns["A1"]()
    html = '<section class="shero">' + html[html.index("</header>") + len("</header>"):]
    old = '<p class="hsub">Injuries</p>'
    assert html.count(old) == 1
    html = html.replace(old, '<p class="hsub">%s</p>' % HERO_SUB)
    assert "fa-balance-scale" in html and "fa-car" not in html
    assert html.count("<h1") == 1 and "What We Do" in html
    return "\n<!-- 3 · hero — A1 -->\n" + HNS["_rename_markup"](html) + "\n"


# ---------------------------------------------------------------------------
INTRO_CSS = r"""
/* ---------- intro — centred, no hexagon, same as the team page ----------
   762px measure held rather than widened: centred text gets harder to read
   as the measure grows, because every line starts at a different x. */
.icontext{padding:14px 0 0}
.icontext .tc{max-width:762px;margin:0 auto;text-align:center}
.icontext h2{font-family:Raleway,"Helvetica Neue",Arial,sans-serif;font-size:26px;line-height:39px;
             font-weight:700;color:#000;margin-bottom:14px}
.icontext p{margin-bottom:0}
.cwrap.solo{display:block}
.cwrap.solo .primary{width:100%;padding-bottom:0}
"""

# Every colour below was computed, not chosen. Contrast on white:
#   brown  #423C37  10.87:1   the service names and the area headings
#   muted  #6B6B6B   5.33:1   group labels and the "no sub-pages" line
#   teal-d #2F7186   5.50:1   the "All ..." door
#   hover  #A94A29   5.68:1
# home_v1's own label grey #9A948B measures 3.01:1 on white and 2.86:1 on its
# --paper band — it fails AA at any size it is used at. It is NOT reused here.
# Brand orange is 3.03:1 and carries no word on this page: it is the rule under
# a hovered row and nothing else. Orange marks, brown reads.
SIDX_CSS = r"""
/* ---------- 5 · the service index ---------------------------------------
   Five areas, each with every page that actually exists beneath it. An index
   page's job is completeness — it is read once, to find something — so
   nothing here is behind a tab, a hover or a panel. That is the difference
   between this and the homepage's practice-areas screen, whose job is to
   route a visitor in one glance rather than list the estate.
   1280 = 384 (the area) + 64 + 832 (its pages), the same lattice the inner
   pages use for content and sidebar. */
.sidx{padding:56px 0 64px}
.sidx .area{display:flex;gap:64px;padding:34px 0;border-top:1px solid var(--line)}
.sidx .area:first-child{border-top:0}
.sidx .ahead{width:384px;flex:none}
/* the plate is a REGULAR hexagon: 64 / 0.8660254 = 73.9. Every hexagon on
   this rebuild is on ratio or it is a bug. */
.sidx .plate{width:64px;height:73.9px;background:var(--brown);
             clip-path:polygon(50% 0,100% 25%,100% 75%,50% 100%,0 75%,0 25%);
             display:flex;align-items:center;justify-content:center;margin-bottom:18px}
/* 46px, and that is a ceiling not a preference: the icon files are 93x107
   8-bit PNGs with the brand colour baked in, so 96px is already an upscale
   and they cannot be recoloured for this dark plate. Replacing the set with
   SVG is the standing flag; until then nothing draws them larger. */
.sidx .plate img{width:46px;height:auto;display:block}
.sidx h2{font-family:Raleway,sans-serif;font-weight:300;font-size:26px;letter-spacing:.06em;
         text-transform:uppercase;color:var(--brown);margin:0 0 12px;line-height:1.2}
.sidx h2 a{color:inherit}
.sidx h2 a:hover{color:#A94A29}
.sidx .acopy{font-size:15px;line-height:25px;color:#6B6B6B;margin:0 0 18px}
.sidx .more{display:inline-block;font-family:Raleway,sans-serif;font-weight:700;font-size:12px;
            letter-spacing:.14em;text-transform:uppercase;color:#2F7186}
.sidx .more:hover{color:#A94A29}
.sidx .alist{flex:1;display:flex;gap:32px}
/* the column is 256 whatever an area's group count is: (832 - 2x32)/3. Letting
   the groups divide the 832 between them instead would give Injuries three
   256px columns and Estates two 400px ones, and the eye reads that as two
   different tables rather than one index. A fixed column also means the areas
   with less to show run out of columns rather than stretching to fill. */
.sidx .agrp{flex:0 0 256px;min-width:0}
/* an area with one group has a group label that only restates the area name,
   so the label is dropped */
.sidx .agh{font-family:Raleway,sans-serif;font-weight:700;font-size:11px;line-height:18px;
           letter-spacing:.16em;text-transform:uppercase;color:#6B6B6B;margin-bottom:6px}
.sidx .agh a{color:#2F7186}
.sidx .agh a:hover{color:#A94A29}
/* `.sidx .alist a` would ALSO have matched the group heading's link — the
   heading sits inside .alist too — and the row style won: the "Estate
   Disputes" label came out at the pages' 14px with their padding and their
   rule under it, so the one heading that is a link stopped looking like a
   heading. Same shape of mistake as the `.crumbs` collision on the team page:
   a descendant selector reaching a link it was not written for. Scoped to a
   DIRECT child of the group, which is what a page row actually is. */
.sidx .agrp > a{display:block;font-size:14px;line-height:20px;color:var(--brown);
                padding:8px 0;border-bottom:1px solid var(--line)}
.sidx .agrp > a:hover{color:#A94A29;border-bottom-color:var(--orange)}
/* an area with no pages under it says so. Padding it with the topics the
   homepage names would be inventing pages that do not exist. */
.sidx .none{font-size:15px;line-height:25px;color:#6B6B6B}
.sidx .none b{color:var(--brown);font-weight:600}
"""


def index_markup():
    rows = []
    for a in AREAS:
        if a["groups"]:
            one = " one" if len(a["groups"]) == 1 else ""
            grps = "".join(
                '<div class="agrp">%s%s</div>'
                % ("" if one else '<div class="agh">%s</div>'
                   % ('<a href="%s">%s</a>' % (ghref, gh) if ghref else gh),
                   "".join('<a href="%s">%s</a>' % (hr, n) for n, hr in items))
                for gh, ghref, items in a["groups"])
            body = '<div class="alist%s">%s</div>' % (one, grps)
        else:
            body = ('<div class="none"><b>No sub-pages.</b><br>'
                    'Everything under %s sits on the area page itself.</div>' % a["name"])
        rows.append(
            '<article class="area">'
            '<div class="ahead">'
              '<span class="plate"><img src="%s" alt=""></span>'
              '<h2><a href="%s">%s</a></h2>'
              '<p class="acopy">%s</p>'
              '<a class="more" href="%s">All %s &rarr;</a>'
            '</div>%s</article>'
            % (a["icon"], a["href"], a["name"], a["copy"], a["href"], a["name"].lower(), body))
    return ('\n<!-- 5 · the service index -->\n<section class="sidx">\n'
            '  <div class="container">\n' + "\n".join(rows) + '\n  </div>\n</section>\n')


def entry():
    return ("""
<!-- 4 · intro -->
<div class="container cwrap solo">
  <main class="primary">
    <div class="entry">
      <div class="icontext"><div class="tc">
""" + INTRO + """
      </div></div>
    </div>
  </main>
</div>
""")


def _assert_no_collision(css, hero_html):
    hero_classes = set()
    for c in re.findall(r'class="([^"]*)"', hero_html):
        hero_classes.update(c.split())
    bad = [sel for sel, cls in HNS["_selector_classes"](css)
           if cls and cls <= hero_classes and not cls <= HNS["SHARED"]]
    assert not bad, "these page rules would reach inside the hero: %s" % bad


def build():
    hero_html = hero_markup()
    ccss, chtml, cscript = HNS["_consult"]()
    fcss, fhtml = HNS["_footer"]()
    hcss, hhtml = HNS["_header"]()
    # this page is the PARENT of the five areas in the nav, and the nav has no
    # item for it — the rebuild's header puts the areas in the bar and drops
    # "What We Do" entirely. So nothing is marked current, and the header's
    # own Injuries marker (written for an injuries page) is removed.
    was = hhtml
    hhtml = hhtml.replace(
        '<div class="mi cur"><a href="/our-services/bc-injury-claims/">Injuries</a>',
        '<div class="mi"><a href="/our-services/bc-injury-claims/">Injuries</a>', 1)
    assert hhtml != was and 'class="mi cur"' not in hhtml, "the nav still marks a current area"

    css = (CSS.replace("__HEADER_CSS__", hcss).replace("__FOOTER_CSS__", fcss)
              .replace("__CONSULT_CSS__", ccss).replace("__SIDENAV_CSS__", ""))
    assert css.count("__HERO_CSS__") == 1
    _assert_no_collision(css.replace("__HERO_CSS__", "") + INTRO_CSS + SIDX_CSS, hero_html)
    css = css.replace("__HERO_CSS__", hero_css()) + INTRO_CSS + SIDX_CSS
    assert not re.search(r"__[A-Z_]+__", css), "a stylesheet slot went unfilled"

    html = HNS["head"]()
    i, j = html.index("<style>") + 7, html.index("</style>")
    html = html[:i] + css + html[j:]
    html = html.replace("<title>League Law &mdash; Car Accident Claims</title>",
                        "<title>League Law &mdash; What We Do</title>")
    assert "<title>League Law &mdash; What We Do</title>" in html

    out = "".join([html, hhtml, hero_html, entry(), index_markup(), fhtml,
                   "\n<!-- consultation tab + modal, lifted from home_v1 -->\n",
                   chtml, cscript, "\n</body>\n</html>\n"])
    assert out.count("<h1") == 1
    assert out.count('<section class="shero">') == 1 and out.count('<section class="sidx">') == 1
    assert out.count('class="area"') == 5, "expected five practice areas"
    assert len(re.findall(r'<div class="alist[^"]*">', out)) == 3, \
        "expected three areas with pages under them"
    assert out.count('class="none"') == 2, "expected two areas with no sub-pages"
    assert '<footer class="nfoot">' in out and '<header class="nheader">' in out
    assert 'class="ctatab"' in out and "<aside" not in out
    assert 'class="sidebar"' not in out and 'class="phead' not in out
    # every service link on the page must be a real live URL, never a "#"
    links = re.findall(r'<a [^>]*?href="([^"]*)"', out[out.index('<section class="sidx">'):
                                                 out.index("</section>", out.index('<section class="sidx">'))])
    assert links and all(l.startswith("/our-services/") for l in links), \
        "a service link is not a live URL: %s" % [l for l in links if not l.startswith("/our-services/")]
    assert len(links) == 5 * 2 + 13 + 7 + 2, "expected 32 links in the index, got %d" % len(links)
    return out


if __name__ == "__main__":
    html = build()
    dest = sys.argv[1] if len(sys.argv) > 1 else OUT
    io.open(dest, "w", encoding="utf-8").write(html)
    print("wrote %s (%d bytes)" % (dest, len(html)))
    print("topics named on the homepage with no page:",
          ", ".join("%s / %s" % t for t in NOTPAGES))
