#!/usr/bin/env bash
set -Eeuo pipefail

readonly EXPECTED_SHA256="57b5e4f123c505b151ce1abdb157f5e31a54492812b39cc46d3a17245a325fc0"
readonly INPUT="${1:-/work/NB6VAC-MAIN-R4.0.45d}"
readonly OUTPUT="${2:-/work/NB6VAC-MAIN-R4.0.45d-rooted-generic-pd-webui}"
readonly SALT="d0u6y147S41t4nDp3pp3r"

if [[ ! -f "$INPUT" ]]; then
    echo "ERROR: firmware not found: $INPUT" >&2
    exit 1
fi

IFS= read -r PASSWORD
if [[ -z "$PASSWORD" ]]; then
    echo "ERROR: the root password supplied on stdin is empty" >&2
    exit 1
fi

ACTUAL_SHA256="$(sha256sum "$INPUT" | awk '{print $1}')"
if [[ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]]; then
    echo "ERROR: SHA-256 mismatch for $INPUT" >&2
    echo "expected: $EXPECTED_SHA256" >&2
    echo "actual:   $ACTUAL_SHA256" >&2
    exit 1
fi

TMP_DIR="$(mktemp -d)"
trap 'rm -rf -- "$TMP_DIR"' EXIT

NO_WFI="$TMP_DIR/original-no-wfi.bin"
JFFS2="$TMP_DIR/jffs2.bin"
UBI="$TMP_DIR/original.ubi"
ROOTFS_OUT="$TMP_DIR/rootfs"
UBIFS_IMAGE="$TMP_DIR/rootfs.ubifs"
NEW_UBI="$TMP_DIR/rootfs.ubi"
NEW_NO_WFI="$TMP_DIR/rooted-no-wfi.bin"
UBINIZE_CFG="$TMP_DIR/ubinize.cfg"

echo "[1/8] Verifying official firmware: $ACTUAL_SHA256"
/opt/nb6vac/extract-wfi/wfi-tag-extract -i "$INPUT" -o "$NO_WFI"

echo "[2/8] Splitting authenticated JFFS2/kernel and UBI root filesystem"
CUT_SECTION="$(binwalk "$NO_WFI" | awk '/UBI erase count header/ {print $1; exit}')"
if [[ ! "$CUT_SECTION" =~ ^[0-9]+$ ]] || (( CUT_SECTION == 0 )); then
    echo "ERROR: could not locate the UBI image" >&2
    exit 1
fi
dd if="$NO_WFI" of="$JFFS2" bs=1 count="$CUT_SECTION" status=none
dd if="$NO_WFI" of="$UBI" bs=1 skip="$CUT_SECTION" status=none

echo "[3/8] Extracting UBIFS in userspace"
ubireader_extract_files --keep-permissions -o "$ROOTFS_OUT" "$UBI" >/dev/null
SHADOW="$(find "$ROOTFS_OUT" -type f -path '*/etc/shadow' -print -quit)"
if [[ -z "$SHADOW" ]]; then
    echo "ERROR: etc/shadow was not found in the extracted image" >&2
    exit 1
fi
ROOTFS="${SHADOW%/etc/shadow}"

echo "[4/8] Replacing the root password hash"
HASH="$(openssl passwd -6 -salt "$SALT" "$PASSWORD")"
sed -i "s#^root:[^:]*:#root:${HASH}:#" "$SHADOW"
if ! grep -Fqx "root:${HASH}:14550:0:99999:7:::" "$SHADOW"; then
    echo "ERROR: root password hash replacement failed" >&2
    exit 1
fi

echo "[5/8] Applying DHCP, WAN bridge, WebUI and remote-management patches"
for firmware_patch in /opt/nb6vac/firmware-patches/*.patch; do
    patch --batch --forward -d "$ROOTFS" -p1 <"$firmware_patch"
done
cp -a /opt/nb6vac/firmware-files/. "$ROOTFS/"
chmod 0755 "$ROOTFS/usr/sbin/uireset"

# Local account data belongs to the writable data partition and must never be
# captured in a distributable firmware image.  A missing file is intentional:
# sipconfigd creates it only after the administrator saves the WebUI form.
if [[ -e "$ROOTFS/etc/config/voip-local.xml" || -L "$ROOTFS/etc/config/voip-local.xml" ]]; then
    echo "ERROR: a device-specific SIP configuration was included in the firmware rootfs" >&2
    exit 1
fi
if [[ -e "$ROOTFS/etc/config/ipv6-pd-length" || -L "$ROOTFS/etc/config/ipv6-pd-length" ]]; then
    echo "ERROR: a device-specific DHCPv6-PD preference was included in the firmware rootfs" >&2
    exit 1
fi

# The vendor FastCGI hides and rejects /network/wan whenever Ethernet is
# reported as "ftth", and /networkv6/wan when the generic native-IPv6
# profile is active. Keep the real access profiles intact and redirect only
# those hidden-menu keys to unused, same-sized HDF keys.
python3 - "$ROOTFS/www/fcgiroot/fastcgi" <<'PY'
import pathlib
import sys

path = pathlib.Path(sys.argv[1])
data = path.read_bytes()
menu_keys = (
    (b"MenuHidden.network.wan", b"MenuShown_.network.wan"),
    (b"MenuHidden.networkv6.wan", b"MenuShown_.networkv6.wan"),
)
for old, new in menu_keys:
    if data.count(old) != 1 or len(old) != len(new):
        raise SystemExit("unexpected FastCGI WAN-menu signature")
    data = data.replace(old, new)

# The WebUI factory reset only asks NBD to erase fields from its own schema.
# Replace its diagnostic command with a local wrapper which also deletes the
# SIP and DHCPv6-PD files before the existing NBD erase and reboot continue.
old_command = b"diaglog event uireset"
new_command = b"/usr/sbin/uireset"
if data.count(old_command) != 1 or len(new_command) >= len(old_command):
    raise SystemExit("unexpected FastCGI WebUI-reset signature")
new_command += b"\0" * (len(old_command) - len(new_command))
data = data.replace(old_command, new_command)
data = bytearray(data)

# The MIPS `sw zero,0(s0)` instructions at 0x402ef0 and 0x40302c null the
# IPv4 and IPv6 WAN handlers for those profiles. Replacing only these two
# instructions with NOP leaves every other FTTH/DSL WebUI condition intact.
handler_guard_offsets = (0x2EF0, 0x302C)
for handler_guard_offset in handler_guard_offsets:
    if data[handler_guard_offset:handler_guard_offset + 4] != b"\xae\x00\x00\x00":
        raise SystemExit("unexpected FastCGI WAN-handler guard signature")
    data[handler_guard_offset:handler_guard_offset + 4] = b"\x00\x00\x00\x00"
path.write_bytes(data)
PY

grep -Fq '[ -r /etc/config/ipv6-pd-length ]' "$ROOTFS/etc/init.d/odhcp6c"
grep -Fq 'exec odhcp6c -N none -P "${pd_length}" -F' "$ROOTFS/etc/init.d/odhcp6c"
grep -Fq 'local pd_length="0"' "$ROOTFS/etc/init.d/odhcp6c"
grep -Fq 'local lan_net=$(ipv6_subnet_calc "${prefix_iapd}" 0 64)' "$ROOTFS/etc/dhcp/ipv6.script"
grep -Fq "ip -6 route show table all | grep -q '^default '" "$ROOTFS/etc/dhcp/ipv6.script"
grep -Fq 'No NIS-domain match; accept the lease on the active access link.' "$ROOTFS/etc/dhcp/wan.script"
grep -Fq 'for infra in adsl ftth' "$ROOTFS/etc/dhcp/wan.script"
grep -Fq 'elif [ "$(status get ftth_link)" = "up" ]; then' "$ROOTFS/etc/dhcp/ipv6.script"
grep -Fq 'generic_native_fallback' "$ROOTFS/etc/init.d/ipv6"
grep -Fq 'forcing router mode before topology setup' "$ROOTFS/etc/init.d/phy"
grep -Fq 'status set net_mode_bridge on' "$ROOTFS/etc/init.d/boot-terminated"
grep -Fq 'name(item) == "fiber"' "$ROOTFS/www/fcgiroot/tpl/menu.html"
grep -Fq 'id="net_mode_bridge"' "$ROOTFS/www/fcgiroot/tpl/network/wan.html"
grep -Fq 'name="ethernet-wan"' "$ROOTFS/etc/default/topology-bridge.xml"
grep -Fq 'name="xdsl-wan"' "$ROOTFS/etc/default/topology-bridge.xml"
grep -Fq 'remote autoconfiguration disabled' "$ROOTFS/usr/sbin/defaultroute"
grep -Fq 'local_voip_config=/etc/config/voip-local.xml' "$ROOTFS/etc/init.d/voip"
grep -Fq '/usr/sbin/voip-sip-filter "$services_proxy"' "$ROOTFS/etc/init.d/voip"
grep -Fq 'launcher status is therefore not authoritative' "$ROOTFS/usr/sbin/voip-local-apply"
grep -Fq '$HTTP["url"] =~ "^/(sipconfig|pdconfig)$"' "$ROOTFS/etc/lighttpd/_base.conf"
grep -Fq 'id="form_sip_local"' "$ROOTFS/www/fcgiroot/tpl/voip/config.html"
grep -Fq 'url: "/sipconfig"' "$ROOTFS/www/docroot/js/voip/config.js"
grep -Fq 'id="form_ipv6_pd_local"' "$ROOTFS/www/fcgiroot/tpl/networkv6/wan.html"
grep -Fq '<!-- nb6vac-local-ipv6-pd:end -->' "$ROOTFS/www/fcgiroot/tpl/networkv6/wan.html"
grep -Fq 'url: "/pdconfig"' "$ROOTFS/www/docroot/js/networkv6/wan.js"
if grep -Fq 'id="form_ipv6_pd_local"' "$ROOTFS/www/fcgiroot/tpl/networkv6.html"; then
    echo "ERROR: DHCPv6-PD form is still present on the IPv6 overview page" >&2
    exit 1
fi
test -x "$ROOTFS/usr/sbin/sipconfigd"
test -x "$ROOTFS/etc/init.d/sipconfigd"
grep -aFq 'status set voip_up up' "$ROOTFS/usr/sbin/sipconfigd"
grep -aFq '/etc/config/ipv6-pd-length' "$ROOTFS/usr/sbin/sipconfigd"
grep -aFq '/factory-reset' "$ROOTFS/usr/sbin/sipconfigd"
grep -aFq '/usr/bin/nbctl auth_key' "$ROOTFS/usr/sbin/sipconfigd"
grep -aFq '/usr/bin/serialization --show' "$ROOTFS/usr/sbin/sipconfigd"
grep -aFq 'auth_key=' "$ROOTFS/usr/sbin/sipconfigd"
grep -aFq '/proc/productid' "$ROOTFS/usr/sbin/sipconfigd"
/opt/nb6vac/sipconfigd/check-no-embedded-key.sh /opt/nb6vac/sipconfigd/sipconfigd.c
test -x "$ROOTFS/usr/sbin/uireset"
test -x "$ROOTFS/usr/sbin/voip-local-apply"
test -x "$ROOTFS/usr/sbin/voip-sip-filter"
grep -Fq 'local_apply_now' "$ROOTFS/usr/sbin/voip-local-apply"
grep -Fq 'http://127.0.0.1:8088/factory-reset' "$ROOTFS/usr/sbin/uireset"
grep -Fq 'local settings reset failed' "$ROOTFS/usr/sbin/uireset"
grep -Fq 'rm -f /etc/config/*xml /etc/config/ipv6-pd-length' "$ROOTFS/usr/sbin/reset-to-factory"
python3 - "$ROOTFS/usr/share/nbd/user.xml" "$ROOTFS/etc/default/reset.xml" <<'PY'
import sys
import xml.etree.ElementTree as ET

for filename in sys.argv[1:]:
    root = ET.parse(filename).getroot()
    path = "./user/ipv6/enable" if root.tag == "default" else "./ipv6/enable"
    value = root.findtext(path)
    if value != "on":
        raise SystemExit(f"IPv6 is not enabled by default in {filename}")
PY
if grep -Fq 'autoconf start' "$ROOTFS/usr/sbin/defaultroute"; then
    echo "ERROR: remote autoconfiguration can still be started by defaultroute" >&2
    exit 1
fi
grep -aFq 'MenuShown_.network.wan' "$ROOTFS/www/fcgiroot/fastcgi"
if grep -aFq 'MenuHidden.network.wan' "$ROOTFS/www/fcgiroot/fastcgi"; then
    echo "ERROR: FastCGI still hides the Ethernet WAN page" >&2
    exit 1
fi
grep -aFq 'MenuShown_.networkv6.wan' "$ROOTFS/www/fcgiroot/fastcgi"
if grep -aFq 'MenuHidden.networkv6.wan' "$ROOTFS/www/fcgiroot/fastcgi"; then
    echo "ERROR: FastCGI still hides the IPv6 WAN page" >&2
    exit 1
fi
grep -aFq '/usr/sbin/uireset' "$ROOTFS/www/fcgiroot/fastcgi"
if grep -aFq 'diaglog event uireset' "$ROOTFS/www/fcgiroot/fastcgi"; then
    echo "ERROR: FastCGI WebUI reset does not clear local settings" >&2
    exit 1
fi
python3 - "$ROOTFS/www/fcgiroot/fastcgi" <<'PY'
import pathlib
import sys
data = pathlib.Path(sys.argv[1]).read_bytes()
for offset in (0x2EF0, 0x302C):
    assert data[offset:offset + 4] == b"\0\0\0\0"
PY
sh -n "$ROOTFS/etc/init.d/odhcp6c" "$ROOTFS/etc/init.d/ipv6" \
    "$ROOTFS/etc/dhcp/ipv6.script" "$ROOTFS/etc/dhcp/wan.script" \
    "$ROOTFS/etc/init.d/phy" "$ROOTFS/etc/init.d/boot-terminated" "$ROOTFS/etc/init.d/guest" \
    "$ROOTFS/etc/init.d/hotspot" "$ROOTFS/etc/init.d/ont" \
    "$ROOTFS/etc/init.d/voip" "$ROOTFS/etc/init.d/sipconfigd" \
    "$ROOTFS/etc/init.d/miniupnpd" \
    "$ROOTFS/usr/sbin/defaultroute" "$ROOTFS/usr/sbin/reset-to-factory" \
    "$ROOTFS/usr/sbin/uireset"
python3 - "$ROOTFS/etc/default/topology-bridge.xml" "$ROOTFS/etc/default/topology-rtk.xml" <<'PY'
import re
import sys
import xml.etree.ElementTree as ET
bridge = ET.parse(sys.argv[1])
bridge_names = {node.get("ifname") for node in bridge.iter("interface") if node.get("ifname")}
# The vendor RTK file itself is not strict XML: its hotspot IFB element has two
# `interface` attributes. Extracting only ifname attributes is deliberate and
# still validates every interface consumed by our well-formed bridge topology.
stock_names = set(re.findall(r'\bifname="([^"]+)"', open(sys.argv[2]).read()))
missing = bridge_names - stock_names
if missing:
    raise SystemExit(f"bridge topology uses interfaces absent from RTK topology: {sorted(missing)}")
PY
python3 - "$ROOTFS" <<'PY'
import pathlib
import re
import sys

root = pathlib.Path(sys.argv[1])
phy = (root / "etc/init.d/phy").read_text()
touch = phy.index("touch /tmp/bcmsw")
guard = phy.index("forcing router mode before topology setup")
if guard < touch:
    raise SystemExit("BCM router guard runs before switch detection")

orders = {}
for link in (root / "etc/rc.d").glob("S[0-9][0-9]*"):
    match = re.match(r"S([0-9]+)(.+)", link.name)
    if match:
        orders[match.group(2)] = int(match.group(1))

phy_order = orders.get("phy")
consumers = []
for name, order in orders.items():
    script = root / "etc/init.d" / name
    if script.exists() and "topology${mode}.xml" in script.read_text(errors="ignore"):
        consumers.append((order, name))
if phy_order is None or not consumers or phy_order >= min(consumers)[0]:
    raise SystemExit(f"BCM router guard does not precede topology consumers: phy={phy_order}, consumers={consumers}")
PY

echo "[6/8] Rebuilding UBIFS and UBI"
mkfs.ubifs -m 2048 -e 126976 -c 131072 -r "$ROOTFS" \
    -o "$UBIFS_IMAGE" --compr=zlib >/dev/null

cat >"$UBINIZE_CFG" <<EOF
[ubifs]
mode=ubi
image=$UBIFS_IMAGE
vol_id=0
vol_type=dynamic
vol_name=rootfs_ubifs
vol_flags=autoresize
EOF

IMAGE_SEQ="$(ubireader_display_info "$UBI" | awk -F': ' '/Sequence Num:/ {print $2; exit}')"
UBINIZE_ARGS=(-O 2048 -p 128KiB -m 2048 -s 2048 -o "$NEW_UBI")
if [[ "$IMAGE_SEQ" =~ ^[0-9]+$ ]]; then
    UBINIZE_ARGS+=(--image-seq="$IMAGE_SEQ")
fi
ubinize "${UBINIZE_ARGS[@]}" "$UBINIZE_CFG" >/dev/null

echo "[7/8] Reassembling the firmware and generating its WFI tag"
cp "$JFFS2" "$NEW_NO_WFI"
dd if="$NEW_UBI" of="$NEW_NO_WFI" bs=1 seek="$CUT_SECTION" conv=notrunc status=none
MAX_IMAGE_SIZE=$((259 * 128 * 1024))
NEW_IMAGE_SIZE="$(stat -c '%s' "$NEW_NO_WFI")"
if (( NEW_IMAGE_SIZE > MAX_IMAGE_SIZE )); then
    echo "ERROR: rebuilt image exceeds the documented 259-PEB partition" >&2
    exit 1
fi
/opt/nb6vac/make-wfi/wfi-tag-mk -i "$NEW_NO_WFI" -o "$OUTPUT"

echo "[8/8] Verifying generated image"
VERIFY_NO_WFI="$TMP_DIR/verify-no-wfi.bin"
/opt/nb6vac/extract-wfi/wfi-tag-extract -i "$OUTPUT" -o "$VERIFY_NO_WFI" >/dev/null
VERIFY_CUT="$(binwalk "$VERIFY_NO_WFI" | awk '/UBI erase count header/ {print $1; exit}')"
if [[ "$VERIFY_CUT" != "$CUT_SECTION" ]]; then
    echo "ERROR: authenticated prefix size changed" >&2
    exit 1
fi
cmp -n "$CUT_SECTION" "$NO_WFI" "$VERIFY_NO_WFI"
dd if="$VERIFY_NO_WFI" of="$TMP_DIR/verify.ubi" bs=1 skip="$VERIFY_CUT" status=none
ubireader_extract_files --keep-permissions -o "$TMP_DIR/verify-rootfs" "$TMP_DIR/verify.ubi" >/dev/null
VERIFY_SHADOW="$(find "$TMP_DIR/verify-rootfs" -type f -path '*/etc/shadow' -print -quit)"
grep -Fqx "root:${HASH}:14550:0:99999:7:::" "$VERIFY_SHADOW"

VERIFY_ROOTFS="${VERIFY_SHADOW%/etc/shadow}"
if [[ -e "$VERIFY_ROOTFS/etc/config/voip-local.xml" || -L "$VERIFY_ROOTFS/etc/config/voip-local.xml" ]]; then
    echo "ERROR: generated firmware contains a device-specific SIP configuration" >&2
    exit 1
fi
if [[ -e "$VERIFY_ROOTFS/etc/config/ipv6-pd-length" || -L "$VERIFY_ROOTFS/etc/config/ipv6-pd-length" ]]; then
    echo "ERROR: generated firmware contains a device-specific DHCPv6-PD preference" >&2
    exit 1
fi
(cd "$ROOTFS" && find . -type f ! -path ./etc/shadow -print0 | sort -z | xargs -0 sha256sum) \
    >"$TMP_DIR/original-files.sha256"
(cd "$VERIFY_ROOTFS" && find . -type f ! -path ./etc/shadow -print0 | sort -z | xargs -0 sha256sum) \
    >"$TMP_DIR/verified-files.sha256"
cmp "$TMP_DIR/original-files.sha256" "$TMP_DIR/verified-files.sha256"

(cd "$ROOTFS" && find . -type l -printf '%p -> %l\n' | sort) >"$TMP_DIR/original-links"
(cd "$VERIFY_ROOTFS" && find . -type l -printf '%p -> %l\n' | sort) >"$TMP_DIR/verified-links"
cmp "$TMP_DIR/original-links" "$TMP_DIR/verified-links"

(cd "$ROOTFS" && find . ! -path ./etc/shadow -printf '%y %m %U %G %p -> %l\n' | sort) \
    >"$TMP_DIR/original-metadata"
(cd "$VERIFY_ROOTFS" && find . ! -path ./etc/shadow -printf '%y %m %U %G %p -> %l\n' | sort) \
    >"$TMP_DIR/verified-metadata"
cmp "$TMP_DIR/original-metadata" "$TMP_DIR/verified-metadata"

if [[ -n "${HOST_UID:-}" && -n "${HOST_GID:-}" ]]; then
    chown "$HOST_UID:$HOST_GID" "$OUTPUT"
fi

echo "Created: $OUTPUT"
sha256sum "$OUTPUT"
