from __future__ import annotations

"""Security and release-safety preflight for packaged NexaOffice builds.

This is not an antivirus scanner. It checks package configuration and runtime
source patterns that often increase false positives with Windows Defender,
SmartScreen, Avast, and other security products.
"""

import argparse
import json
import re
from datetime import datetime
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "data"
SPEC = ROOT / "nexaoffice_worksuite.spec"
MANIFEST = ROOT / "nexaoffice.exe.manifest"
VERSION_INFO = ROOT / "version_info.txt"
CONSTANTS = ROOT / "core" / "constants.py"
RUNTIME_PATHS = [ROOT / "main.py", ROOT / "app.py", ROOT / "core", ROOT / "pages"]


def _read(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return ""


def _current_version() -> str:
    text = _read(CONSTANTS)
    m = re.search(r'APP_VERSION\s*=\s*"(v\d{4}\.\d{2}\.\d{2}\.\d{2,3})"', text)
    return m.group(1) if m else "unknown"


def _runtime_files() -> list[Path]:
    files: list[Path] = []
    for item in RUNTIME_PATHS:
        if item.is_file() and item.suffix == ".py":
            files.append(item)
        elif item.is_dir():
            files.extend(sorted(p for p in item.rglob("*.py") if "__pycache__" not in p.parts))
    return files


def _strip_comments(text: str) -> str:
    lines = []
    for line in text.splitlines():
        stripped = line.strip()
        if stripped.startswith("#"):
            continue
        lines.append(line)
    return "\n".join(lines)


def _scan_runtime_patterns() -> dict:
    patterns = {
        "startup_persistence": [
            r"CurrentVersion\\\\Run",
            r"\bwinreg\b.*\bRun\b",
            r"\bschtasks\b",
            r"Task Scheduler",
            r"shell:startup",
        ],
        "self_update_or_downloader": [r"urlretrieve", r"curl\s+", r"wget\s+", r"Invoke-WebRequest", r"Start-BitsTransfer"],
        "remote_code_execution_review": [r"exec\(", r"eval\(", r"powershell", r"cmd\.exe", r"subprocess\.Popen"],
        "credential_exposure_logs": [r"api_token.*print", r"Authorization.*print", r"password.*print"],
        "local_listener_review": [r"socketserver", r"HTTPServer", r"Flask", r"FastAPI", r"uvicorn", r"listen\("],
    }
    hits: dict[str, list[str]] = {name: [] for name in patterns}
    for path in _runtime_files():
        rel = str(path.relative_to(ROOT))
        text = _strip_comments(_read(path))
        for name, exprs in patterns.items():
            for expr in exprs:
                if re.search(expr, text, flags=re.IGNORECASE):
                    hits[name].append(f"{rel}:{expr}")
    return hits


def _build_flags() -> dict:
    spec = _read(SPEC)
    manifest = _read(MANIFEST)
    return {
        "onedir_collect": "COLLECT(" in spec and "onefile=True" not in spec,
        "upx_disabled": "upx=False" in spec,
        "python_archive_disabled_noarchive": "noarchive=True" in spec,
        "console_window_disabled": "console=False" in spec,
        "no_admin_privilege": "requireAdministrator" not in manifest,
        "version_info_present": VERSION_INFO.exists(),
        "icon_present": (ROOT / "assets" / "nexaoffice.ico").exists(),
        "manifest_present": MANIFEST.exists(),
    }


def _network_policy() -> dict:
    network_py = _read(ROOT / "core" / "network.py")
    storage_py = _read(ROOT / "core" / "storage.py")
    app_py = _read(ROOT / "app.py")
    settings = storage_py + "\n" + app_py
    return {
        "offline_first_default": '"offline_mode": True' in settings or "'offline_mode': True" in settings,
        "server_sync_disabled_default": '"enabled": False' in settings or "'enabled': False" in settings,
        "https_normalization": '"https://" + url' in network_py,
        "http_restricted_to_localhost": "is_loopback_host" in network_py and "server_sync_status_insecure_http" in network_py,
        "network_audit_enabled": "audit_network_event" in network_py and "network_audit.json" in network_py,
        "api_token_not_logged": "sanitize_url_for_log" in network_py and "Authorization" in network_py,
        "no_hidden_updater_declared": "no hidden updater" in _read(ROOT / "tools" / "package_trust_report.py").lower(),
    }


def build_report() -> dict:
    build_flags = _build_flags()
    runtime_hits = _scan_runtime_patterns()
    network_policy = _network_policy()

    critical: list[str] = []
    warnings: list[str] = []

    required_build = {
        "onedir_collect": "Build should stay in PyInstaller onedir/COLLECT mode.",
        "upx_disabled": "UPX compression must remain disabled.",
        "python_archive_disabled_noarchive": "noarchive=True should remain enabled for a transparent package layout.",
        "console_window_disabled": "Windowed release should keep console=False.",
        "no_admin_privilege": "The app should not request administrator privilege.",
        "version_info_present": "version_info.txt should be embedded in the EXE.",
        "icon_present": "Application icon should be embedded.",
        "manifest_present": "Windows manifest should be embedded.",
    }
    for key, message in required_build.items():
        if not build_flags.get(key):
            critical.append(message)

    for group in ("startup_persistence", "self_update_or_downloader"):
        if runtime_hits.get(group):
            critical.append(f"Runtime source contains suspicious pattern group: {group}")
    if runtime_hits.get("credential_exposure_logs"):
        critical.append("Runtime source may log sensitive credentials/tokens.")
    if runtime_hits.get("remote_code_execution_review"):
        warnings.append("Runtime contains review-only execution/open-folder patterns; current known uses are local folder openers and a guarded spreadsheet formula evaluator.")
    if runtime_hits.get("local_listener_review"):
        warnings.append("Runtime contains a local listener pattern; current known use is loopback-only single-instance window focusing.")

    required_network = {
        "offline_first_default": "Network mode should remain offline-first by default.",
        "server_sync_disabled_default": "Server sync should remain disabled by default.",
        "https_normalization": "Bare server URLs should default to HTTPS.",
        "http_restricted_to_localhost": "Plain HTTP should be restricted to localhost/local test endpoints.",
        "network_audit_enabled": "Manual network tests should write a local audit event without secrets.",
        "api_token_not_logged": "Network logging must avoid tokens and Authorization headers.",
    }
    for key, message in required_network.items():
        if not network_policy.get(key):
            critical.append(message)

    if not (ROOT / "dist" / "NexaOffice_WorkSuite" / "NexaOffice_WorkSuite.exe").exists():
        warnings.append("EXE is not built yet; Defender/SmartScreen reputation cannot be measured from the source package alone.")
    warnings.append("Unsigned Windows apps can still trigger SmartScreen/unknown-publisher prompts after public distribution; EV/OV code signing is the official mitigation.")

    report = {
        "ok": not critical,
        "version": _current_version(),
        "created_at": datetime.now().isoformat(timespec="seconds"),
        "purpose": "Preflight release-safety check for Windows Defender, SmartScreen, Avast, and similar generic detections.",
        "build_flags": build_flags,
        "network_policy": network_policy,
        "runtime_pattern_hits": runtime_hits,
        "critical": critical,
        "warnings": warnings,
        "notes": [
            "This tool does not replace Microsoft Defender or third-party antivirus scans.",
            "It reduces obvious false-positive triggers before packaging.",
            "For public releases, submit hashes and sample builds to security vendors if a generic detection appears.",
        ],
    }
    return report


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--write-report", action="store_true", help="write data/security_preflight_latest.json")
    args = parser.parse_args()
    report = build_report()
    if args.write_report:
        DATA.mkdir(parents=True, exist_ok=True)
        (DATA / "security_preflight_latest.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps(report, ensure_ascii=False, indent=2))
    return 0 if report["ok"] else 1


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