#!/usr/bin/env python3
"""Secrets-boundary regression for Wi-Fi provisioning.

Static, source-level checks that the Wi-Fi provisioning paths never log or
expose credentials. These complement the behavioural host tests: they catch a
future edit that adds a password to a log line, the Portal, or a USB response.

Checks (docs/wifi-provisioning-plan.md sections 28, 29):
  1. No ESP_LOG* call in the Wi-Fi sources formats a profile password.
  2. The Portal response builders never reference a stored password.
  3. The device/gateway token names never appear in the provisioning modules.
  4. The fixed provisioning password is defined in exactly one place.
"""
from __future__ import annotations

import pathlib
import re
import sys

REPO = pathlib.Path(__file__).resolve().parents[2]

WIFI_SOURCES = [
    REPO / "firmware/main/wifi_manager.cpp",
    REPO / "firmware/main/wifi_store_nvs.cpp",
    REPO / "boards/common/include/wifi_profiles.h",
    REPO / "boards/common/include/wifi_selection.h",
    REPO / "boards/common/include/wifi_usb_protocol.h",
    REPO / "boards/common/include/portal_html.h",
]

# Names that must never appear in the provisioning modules.
FORBIDDEN_NAMES = ["device_token", "gateway_token", "DEVICE_LAB_DEVICE_TOKEN", "api_key", "API_KEY"]

# A password may be assigned/stored, but never formatted into a log or response.
LOG_RE = re.compile(r"(ESP_LOG[A-Z]|printf|snprintf|fputs|httpd_resp_send)\s*\(")


def strip_comments(text: str) -> str:
    """Remove // and /* */ comments so documentation can discuss passwords."""
    text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
    lines = []
    for line in text.splitlines():
        # Preserve string literals; only drop a trailing // comment.
        in_string = False
        out = []
        i = 0
        while i < len(line):
            ch = line[i]
            if ch == '"' and (i == 0 or line[i - 1] != "\\"):
                in_string = not in_string
            if not in_string and ch == "/" and i + 1 < len(line) and line[i + 1] == "/":
                break
            out.append(ch)
            i += 1
        lines.append("".join(out))
    return "\n".join(lines)


def check_no_password_logging(failures: list[str]) -> None:
    for path in WIFI_SOURCES:
        if not path.exists():
            failures.append(f"missing source: {path}")
            continue
        raw = path.read_text(encoding="utf-8")
        code = strip_comments(raw)
        for line_number, line in enumerate(code.splitlines(), start=1):
            if not LOG_RE.search(line):
                continue
            lowered = line.lower()
            # A log/response line must not reference the profile password field.
            if "password" in lowered:
                # The fixed provisioning AP password is user-visible by design
                # (plan section 8) and may be shown, but a *stored* profile
                # password must not be.
                if "profile.password" in lowered or "s_candidate_password" in lowered:
                    failures.append(
                        f"{path.name}:{line_number}: log/response references a stored password: {line.strip()}"
                    )
                elif "provision" not in lowered and "PROVISIONING_AP_PASSWORD" not in line:
                    failures.append(
                        f"{path.name}:{line_number}: log/response mentions password: {line.strip()}"
                    )


def check_no_forbidden_names(failures: list[str]) -> None:
    for path in WIFI_SOURCES:
        if not path.exists():
            continue
        code = strip_comments(path.read_text(encoding="utf-8"))
        for name in FORBIDDEN_NAMES:
            if name in code:
                failures.append(f"{path.name}: references forbidden secret name {name!r}")


def check_portal_does_not_expose_password(failures: list[str]) -> None:
    path = REPO / "firmware/main/wifi_manager.cpp"
    code = strip_comments(path.read_text(encoding="utf-8"))
    # The Portal status/index builders must not read the candidate password.
    for marker in ("portal_status", "portal_network_list", "portal_root"):
        start = code.find(f"{marker}(")
        while start != -1:
            end = code.find("\n}", start)
            body = code[start:end]
            if "s_candidate_password" in body or "profile.password" in body:
                failures.append(f"portal builder {marker} touches a stored password")
            start = code.find(f"{marker}(", end)


def check_single_fixed_password_definition(failures: list[str]) -> None:
    pattern = re.compile(r'PROVISIONING_AP_PASSWORD\s*=\s*"([^"]+)"')
    definitions = []
    for path in REPO.rglob("*"):
        if not path.is_file():
            continue
        if any(part in {".git", "build", "node_modules", ".venv", "managed_components"} for part in path.parts):
            continue
        if path.suffix not in {".h", ".hpp", ".cpp", ".c"}:
            continue
        try:
            text = path.read_text(encoding="utf-8", errors="ignore")
        except OSError:
            continue
        for match in pattern.finditer(text):
            definitions.append((path, match.group(1)))
    if len(definitions) != 1:
        failures.append(
            "PROVISIONING_AP_PASSWORD must be defined exactly once; found "
            + ", ".join(f"{p.name}={v!r}" for p, v in definitions)
        )
    elif len(definitions[0][1]) < 8:
        failures.append("PROVISIONING_AP_PASSWORD is shorter than the WPA2 minimum")


def check_no_password_in_argv(failures: list[str]) -> None:
    """The Mac CLI must not accept a password on the command line."""
    cli = REPO / "tools/mac/wifi_ctl.py"
    shell = REPO / "tools/mac/device.sh"
    cli_text = cli.read_text(encoding="utf-8")
    # Only a real argparse declaration counts; the prohibition may be described
    # in prose without tripping the check.
    if re.search(r"add_argument\(\s*['\"]--password", cli_text):
        failures.append("wifi_ctl.py exposes a --password command-line flag")
    shell_text = shell.read_text(encoding="utf-8")
    if re.search(r"--password", shell_text):
        failures.append("device.sh passes --password to the helper")
    if "read -r -s" in shell_text:
        failures.append("device.sh reads the password itself instead of letting wifi_ctl prompt")


def main() -> int:
    failures: list[str] = []
    check_no_password_logging(failures)
    check_no_forbidden_names(failures)
    check_portal_does_not_expose_password(failures)
    check_single_fixed_password_definition(failures)
    check_no_password_in_argv(failures)
    if failures:
        for failure in failures:
            print(f"FAIL: {failure}", file=sys.stderr)
        return 1
    print("Wi-Fi provisioning secrets boundary: no password logs, no token exposure, "
          "Portal keeps passwords private, fixed AP password defined once PASS")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())