"""NexaOffice WorkSuite self-check helper.
Run from the project root: python tools/self_check.py
"""
from __future__ import annotations

import ast
import json
import py_compile
import re
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent


def check_version_consistency():
    errors = []
    constants_path = ROOT / "core" / "constants.py"
    version_info_path = ROOT / "version_info.txt"
    constants_text = constants_path.read_text(encoding="utf-8")
    match = re.search(r'APP_VERSION\s*=\s*"v(\d{4})\.(\d{2})\.(\d{2})\.(\d{2,3})"', constants_text)
    if not match:
        return ["APP_VERSION missing or invalid format; expected vYYYY.MM.DD.NN or vYYYY.MM.DD.NNN/NNN"]
    version_plain = ".".join(match.groups())
    y, m, d, n = [int(part) for part in match.groups()]
    if version_info_path.exists():
        info = version_info_path.read_text(encoding="utf-8", errors="ignore")
        if f"FileVersion', '{version_plain}'" not in info:
            errors.append("version_info.txt FileVersion does not match APP_VERSION")
        if f"ProductVersion', '{version_plain}'" not in info:
            errors.append("version_info.txt ProductVersion does not match APP_VERSION")
        compact = info.replace(" ", "")
        if f"filevers=({y}, {m}, {d}, {n})" not in info and f"filevers=({y},{m},{d},{n})" not in compact:
            errors.append("version_info.txt filevers does not match APP_VERSION")
        if f"prodvers=({y}, {m}, {d}, {n})" not in info and f"prodvers=({y},{m},{d},{n})" not in compact:
            errors.append("version_info.txt prodvers does not match APP_VERSION")
    return errors


def check_language_keys():
    lang_dir = ROOT / "lang"
    errors = []
    payloads = {}
    for path in sorted(lang_dir.glob("*.json")):
        try:
            payloads[path.stem] = json.loads(path.read_text(encoding="utf-8"))
        except Exception as exc:
            errors.append(f"{path.name} cannot be loaded: {exc}")
    if not payloads:
        return ["No language JSON files found"]
    base_name = "zh_CN" if "zh_CN" in payloads else sorted(payloads)[0]
    base_keys = set(payloads[base_name])
    for name, payload in payloads.items():
        keys = set(payload)
        missing = sorted(base_keys - keys)
        extra = sorted(keys - base_keys)
        if missing:
            errors.append(f"{name} missing language keys: {', '.join(missing[:20])}")
        if extra:
            errors.append(f"{name} has extra language keys: {', '.join(extra[:20])}")
    return errors


def check_icon_assets():
    assets = ROOT / "assets"
    required = ["16", "20", "24", "30", "32", "40", "48", "60", "64", "72", "80", "96", "128", "256"]
    errors = []
    if not (assets / "nexaoffice.ico").exists():
        errors.append("assets/nexaoffice.ico missing")
    missing_png = [size for size in required if not (assets / f"nexaoffice_icon_{size}.png").exists()]
    if missing_png:
        errors.append("missing icon PNG sizes: " + ", ".join(missing_png))
    if not (assets / "logo_nexa_mark.png").exists():
        errors.append("assets/logo_nexa_mark.png missing")
    if not (assets / "logo_nexa_mark_256.png").exists():
        errors.append("assets/logo_nexa_mark_256.png missing")
    return errors


def check_build_scripts():
    required = [
        "build_exe.bat",
        "build_exe.ps1",
        "nexaoffice_worksuite.spec",
        "run_from_source.bat",
        "tools/desktop_preflight.py",
        "tools/build_result_check.py",
        "tools/build_manifest.py",
        "tools/package_trust_report.py",
        "tools/security_preflight.py",
        "tools/layout_adaptation_check.py",
        "tools/sign_windows_build.ps1",
        "tools/windows_defender_scan.ps1",
        "tools/release_policy_check.py",
        "tools/bump_release_version.py",
        "tools/indonesian_title_case_check.py",
        "tools/ui_title_case_check.py",
        "tools/copy_style_guard_check.py",
        "tools/system_server_config_check.py",
        "tools/network_readiness_check.py",
        "tools/network_deployment_plan_check.py",
        "tools/server_deployment_readiness_check.py",
        "tools/sync_queue_repair_check.py",
        "tools/data_integrity_check.py",
        "tools/preflight_checkpoint_throttle_check.py",
        "tools/offline_maintenance_check.py",
        "tools/puid_privacy_check.py",
        "tools/login_adaptation_check.py",
        "tools/login_layout_guard_check.py",
        "tools/language_selector_display_check.py",
        "tools/files_module_guard_check.py",
        "tools/files_smoothness_check.py",
        "tools/files_toolbar_i18n_check.py",
        "tools/dialog_centering_check.py",
        "tools/profile_dialog_guard_check.py",
        "tools/registration_sticker_guard_check.py",
        "tools/personal_enterprise_isolation_check.py",
        "tools/package_cleanliness_check.py",
        "tools/package_structure_check.py",
        "tools/create_release_zip.py",
    ]
    errors = [f"{item} missing" for item in required if not (ROOT / item).exists()]
    bat = ROOT / "build_exe.bat"
    if bat.exists():
        text = bat.read_text(encoding="utf-8", errors="ignore")
        for marker in ["dist_new_", "build_new_", "build_backups", "build_logs", "release_policy_check.py", "security_preflight.py", "package_trust_report.py"]:
            if marker not in text:
                errors.append(f"build_exe.bat missing robust build marker: {marker}")
    return errors



def check_tk_variable_attrs():
    """Catch Tk variable/textvariable attributes that are used before existing anywhere.

    This specifically prevents silent packaged-EXE failures where a page appears
    to freeze because Tkinter callback exceptions are hidden from the user.
    """
    errors = []
    for path in sorted((ROOT / "pages").glob("*.py")):
        try:
            tree = ast.parse(path.read_text(encoding="utf-8"))
        except Exception as exc:
            errors.append(f"{path.relative_to(ROOT)} could not be parsed for Tk vars: {exc}")
            continue
        assigned = set()
        used = []

        class Visitor(ast.NodeVisitor):
            def _record_target(self, node):
                if (
                    isinstance(node, ast.Attribute)
                    and isinstance(node.value, ast.Name)
                    and node.value.id == "self"
                ):
                    assigned.add(node.attr)

            def visit_Assign(self, node):
                for target in node.targets:
                    self._record_target(target)
                self.generic_visit(node)

            def visit_AnnAssign(self, node):
                self._record_target(node.target)
                self.generic_visit(node)

            def visit_AugAssign(self, node):
                self._record_target(node.target)
                self.generic_visit(node)

            def visit_keyword(self, node):
                if node.arg in {"variable", "textvariable"}:
                    value = node.value
                    if (
                        isinstance(value, ast.Attribute)
                        and isinstance(value.value, ast.Name)
                        and value.value.id == "self"
                    ):
                        used.append((value.attr, getattr(node, "lineno", 0), node.arg))
                self.generic_visit(node)

        Visitor().visit(tree)
        for attr, lineno, keyword in used:
            if attr not in assigned:
                errors.append(f"{path.relative_to(ROOT)}:{lineno} uses self.{attr} as {keyword} but never assigns it")
    return errors




def check_chat_brand_safe_terms():
    """Keep chat labels generic and avoid third-party product names in UI/docs."""
    errors = []
    banned = ["微信", "企业微信", "WeChat", "WeCom"]
    scan_roots = [ROOT / "lang", ROOT / "pages", ROOT / "docs"]
    for root in scan_roots:
        if not root.exists():
            continue
        for path in root.rglob("*"):
            if not path.is_file() or path.suffix.lower() not in {".py", ".json", ".md", ".txt"}:
                continue
            text = path.read_text(encoding="utf-8", errors="ignore")
            for term in banned:
                if term in text:
                    errors.append(f"{path.relative_to(ROOT)} contains third-party chat brand term: {term}")
                    break
    return errors


def _run_tool_main(script_name: str, label: str) -> list[str]:
    """Run a local tool's main() in-process to avoid slow/hanging subprocess chains."""
    script = ROOT / "tools" / script_name
    if not script.exists():
        return [f"tools/{script_name} missing"]
    old_argv = sys.argv[:]
    try:
        import importlib.util
        spec = importlib.util.spec_from_file_location(f"nexa_selfcheck_{script.stem}", script)
        module = importlib.util.module_from_spec(spec)
        assert spec and spec.loader
        spec.loader.exec_module(module)
        sys.argv = [str(script), "--write-report"]
        result = module.main()
    except SystemExit as exc:
        result = int(exc.code or 0)
    except Exception as exc:
        return [f"{label} check could not run: {exc}"]
    finally:
        sys.argv = old_argv
    if result != 0:
        return [f"{label} check failed with exit code {result}"]
    return []


def check_release_policy():
    return _run_tool_main("release_policy_check.py", "release policy")



def check_security_preflight():
    return _run_tool_main("security_preflight.py", "security preflight")


def check_layout_adaptation():
    return _run_tool_main("layout_adaptation_check.py", "layout adaptation")




def check_year_view_fit():
    return _run_tool_main("year_view_fit_check.py", "year view fit")



def check_performance_guard():
    return _run_tool_main("performance_guard_check.py", "performance guard")



def check_indonesian_title_case():
    return _run_tool_main("indonesian_title_case_check.py", "Indonesian title case")



def check_ui_title_case():
    return _run_tool_main("ui_title_case_check.py", "Global UI title case")


def check_copy_style_guard():
    return _run_tool_main("copy_style_guard_check.py", "Copy style guard")


def check_network_deployment_plan():
    return _run_tool_main("network_deployment_plan_check.py", "Network deployment plan guard")


def check_server_deployment_readiness():
    return _run_tool_main("server_deployment_readiness_check.py", "Server deployment readiness")


def check_sync_queue_repair_guard():
    return _run_tool_main("sync_queue_repair_check.py", "Sync queue repair guard")


def check_data_integrity_guard():
    return _run_tool_main("data_integrity_check.py", "Data integrity guard")


def check_preflight_checkpoint_throttle_guard():
    return _run_tool_main("preflight_checkpoint_throttle_check.py", "Preflight checkpoint throttle guard")


def check_offline_maintenance_guard():
    return _run_tool_main("offline_maintenance_check.py", "Offline maintenance guard")


def check_system_server_config():
    return _run_tool_main("system_server_config_check.py", "system server config")


def check_network_readiness():
    return _run_tool_main("network_readiness_check.py", "network readiness")


def check_puid_privacy():
    script = ROOT / "tools" / "puid_privacy_check.py"
    if not script.exists():
        return ["tools/puid_privacy_check.py missing"]
    try:
        import importlib.util
        spec = importlib.util.spec_from_file_location("nexa_puid_privacy_check", script)
        module = importlib.util.module_from_spec(spec)
        assert spec and spec.loader
        spec.loader.exec_module(module)
        report = module.build_report()
        module.REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
        module.REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception as exc:
        return [f"PUID privacy check could not run: {exc}"]
    if report.get("status") != "OK":
        return ["PUID privacy check failed: " + json.dumps(report.get("problems", [])[:10], ensure_ascii=False)[:800]]
    return []

def check_login_layout_guard():
    return _run_tool_main("login_layout_guard_check.py", "Login layout guard")


def check_login_adaptation():
    script = ROOT / "tools" / "login_adaptation_check.py"
    if not script.exists():
        return ["tools/login_adaptation_check.py missing"]
    try:
        import importlib.util
        spec = importlib.util.spec_from_file_location("nexa_login_adaptation_check", script)
        module = importlib.util.module_from_spec(spec)
        assert spec and spec.loader
        spec.loader.exec_module(module)
        app = (ROOT / "app.py").read_text(encoding="utf-8", errors="ignore")
        issues = []
        required = {
            "real viewport width": "view_w = int(canvas.winfo_width()",
            "real viewport height": "view_h = int(canvas.winfo_height()",
            "compact login mode": "compact_login = cw < 1180 or ch < 720",
            "login widget font sync": "def configure_login_widget_fonts():",
            "compact login branch": "if compact_login:",
        }
        for label, marker in required.items():
            if marker not in app:
                issues.append(f"Missing login adaptation marker: {label}")
        if "cw = max(canvas.winfo_width(), 900)" in app:
            issues.append("Old login fake 900px width floor is still present")
        if "s = min(cw / 1672.0, ch / 941.0)" in app:
            issues.append("Old login 1672x941-only scale is still present")
        report = {
            "time": __import__("datetime").datetime.now().isoformat(timespec="seconds"),
            "version": getattr(module, "_version", lambda: "unknown")(),
            "status": "OK" if not issues else "FAILED",
            "issues": issues,
        }
        (ROOT / "data" / "login_adaptation_latest.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception as exc:
        return [f"login adaptation check could not run: {exc}"]
    if issues:
        return ["login adaptation check failed: " + json.dumps(issues[:10], ensure_ascii=False)[:800]]
    return []



def check_language_selector_display():
    script = ROOT / "tools" / "language_selector_display_check.py"
    if not script.exists():
        return ["tools/language_selector_display_check.py missing"]
    try:
        import importlib.util
        spec = importlib.util.spec_from_file_location("nexa_language_selector_display_check", script)
        module = importlib.util.module_from_spec(spec)
        assert spec and spec.loader
        spec.loader.exec_module(module)
        report = module.build_report()
        module.REPORT_PATH.parent.mkdir(parents=True, exist_ok=True)
        module.REPORT_PATH.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception as exc:
        return [f"language selector display check could not run: {exc}"]
    if report.get("status") != "OK":
        return ["language selector display check failed: " + json.dumps(report.get("issues", [])[:10], ensure_ascii=False)[:800]]
    return []


def check_files_module_guard():
    return _run_tool_main("files_module_guard_check.py", "Files module guard")


def check_files_smoothness_guard():
    return _run_tool_main("files_smoothness_check.py", "Files smoothness guard")


def check_files_toolbar_i18n_guard():
    return _run_tool_main("files_toolbar_i18n_check.py", "Files toolbar i18n guard")

def check_dialog_centering():
    return _run_tool_main("dialog_centering_check.py", "Dialog centering")


def check_profile_dialog_guard():
    return _run_tool_main("profile_dialog_guard_check.py", "Profile dialog guard")

def check_registration_sticker_guard():
    return _run_tool_main("registration_sticker_guard_check.py", "Registration/sticker guard")

def check_personal_enterprise_isolation():
    return _run_tool_main("personal_enterprise_isolation_check.py", "Personal/Enterprise isolation")


def check_package_cleanliness():
    return _run_tool_main("package_cleanliness_check.py", "Package cleanliness")

def check_package_structure():
    return _run_tool_main("package_structure_check.py", "Package structure")

def main() -> int:
    py_errors = []
    for path in ROOT.rglob("*.py"):
        if "__pycache__" in path.parts:
            continue
        try:
            py_compile.compile(str(path), doraise=True)
        except Exception as exc:  # pragma: no cover - diagnostic script
            py_errors.append((str(path.relative_to(ROOT)), str(exc)))

    json_errors = []
    for folder in (ROOT / "lang", ROOT / "data"):
        if not folder.exists():
            continue
        for path in folder.glob("*.json"):
            try:
                json.loads(path.read_text(encoding="utf-8"))
            except Exception as exc:
                json_errors.append((str(path.relative_to(ROOT)), str(exc)))

    required_data = ["notes.json", "todos.json", "documents.json", "enterprise.json", "reports.json", "attendance.json"]
    missing_data = [name for name in required_data if not (ROOT / "data" / name).exists()]
    version_errors = check_version_consistency()
    language_errors = check_language_keys()
    icon_errors = check_icon_assets()
    build_script_errors = check_build_scripts()
    tk_variable_errors = check_tk_variable_attrs()
    chat_brand_errors = check_chat_brand_safe_terms()
    release_policy_errors = check_release_policy()
    security_preflight_errors = check_security_preflight()
    layout_adaptation_errors = check_layout_adaptation()
    year_view_fit_errors = check_year_view_fit()
    performance_guard_errors = check_performance_guard()
    indonesian_title_case_errors = check_indonesian_title_case()
    ui_title_case_errors = check_ui_title_case()
    copy_style_guard_errors = check_copy_style_guard()
    system_server_config_errors = check_system_server_config()
    network_readiness_errors = check_network_readiness()
    network_deployment_plan_errors = check_network_deployment_plan()
    server_deployment_readiness_errors = check_server_deployment_readiness()
    sync_queue_repair_guard_errors = check_sync_queue_repair_guard()
    data_integrity_guard_errors = check_data_integrity_guard()
    preflight_checkpoint_throttle_errors = check_preflight_checkpoint_throttle_guard()
    offline_maintenance_errors = check_offline_maintenance_guard()
    puid_privacy_errors = check_puid_privacy()
    login_adaptation_errors = check_login_adaptation()
    login_layout_guard_errors = check_login_layout_guard()
    language_selector_display_errors = check_language_selector_display()
    files_module_guard_errors = check_files_module_guard()
    files_smoothness_guard_errors = check_files_smoothness_guard()
    files_toolbar_i18n_errors = check_files_toolbar_i18n_guard()
    dialog_centering_errors = check_dialog_centering()
    profile_dialog_guard_errors = check_profile_dialog_guard()
    registration_sticker_guard_errors = check_registration_sticker_guard()
    personal_enterprise_isolation_errors = check_personal_enterprise_isolation()
    package_cleanliness_errors = check_package_cleanliness()
    package_structure_errors = check_package_structure()

    print("NexaOffice WorkSuite Self Check")
    print("- Python files:", "OK" if not py_errors else "FAILED")
    for item in py_errors:
        print("  ", item[0], item[1])
    print("- JSON files:", "OK" if not json_errors else "FAILED")
    for item in json_errors:
        print("  ", item[0], item[1])
    print("- Required data files:", "OK" if not missing_data else "MISSING")
    for name in missing_data:
        print("  ", name)
    print("- Version consistency:", "OK" if not version_errors else "FAILED")
    for err in version_errors:
        print("  ", err)
    print("- Language key consistency:", "OK" if not language_errors else "FAILED")
    for err in language_errors:
        print("  ", err)
    print("- Icon assets:", "OK" if not icon_errors else "FAILED")
    for err in icon_errors:
        print("  ", err)
    print("- Build scripts:", "OK" if not build_script_errors else "FAILED")
    for err in build_script_errors:
        print("  ", err)
    print("- Tk variable bindings:", "OK" if not tk_variable_errors else "FAILED")
    for err in tk_variable_errors:
        print("  ", err)
    print("- Chat brand-safe labels:", "OK" if not chat_brand_errors else "FAILED")
    for err in chat_brand_errors:
        print("  ", err)
    print("- Release policy:", "OK" if not release_policy_errors else "FAILED")
    for err in release_policy_errors:
        print("  ", err)
    print("- Security preflight:", "OK" if not security_preflight_errors else "FAILED")
    for err in security_preflight_errors:
        print("  ", err)
    print("- Layout adaptation:", "OK" if not layout_adaptation_errors else "FAILED")
    for err in layout_adaptation_errors:
        print("  ", err)
    print("- Year view fit:", "OK" if not year_view_fit_errors else "FAILED")
    for err in year_view_fit_errors:
        print("  ", err)
    print("- Performance guard:", "OK" if not performance_guard_errors else "FAILED")
    for err in performance_guard_errors:
        print("  ", err)
    print("- Indonesian title case:", "OK" if not indonesian_title_case_errors else "FAILED")
    for err in indonesian_title_case_errors:
        print("  ", err)
    print("- Global UI title case:", "OK" if not ui_title_case_errors else "FAILED")
    for err in ui_title_case_errors:
        print("  ", err)
    print("- Copy style guard:", "OK" if not copy_style_guard_errors else "FAILED")
    for err in copy_style_guard_errors:
        print("  ", err)
    print("- System server config:", "OK" if not system_server_config_errors else "FAILED")
    for err in system_server_config_errors:
        print("  ", err)
    print("- Network deployment plan guard:", "OK" if not network_deployment_plan_errors else "FAILED")
    for err in network_deployment_plan_errors:
        print("  ", err)
    print("- Server deployment readiness:", "OK" if not server_deployment_readiness_errors else "FAILED")
    for err in server_deployment_readiness_errors:
        print("  ", err)
    print("- Sync queue repair guard:", "OK" if not sync_queue_repair_guard_errors else "FAILED")
    for err in sync_queue_repair_guard_errors:
        print("  ", err)
    print("- Data integrity guard:", "OK" if not data_integrity_guard_errors else "FAILED")
    for err in data_integrity_guard_errors:
        print("  ", err)
    print("- Preflight checkpoint throttle guard:", "OK" if not preflight_checkpoint_throttle_errors else "FAILED")
    for err in preflight_checkpoint_throttle_errors:
        print("  ", err)
    print("- Offline maintenance guard:", "OK" if not offline_maintenance_errors else "FAILED")
    for err in offline_maintenance_errors:
        print("  ", err)
    print("- Network readiness:", "OK" if not network_readiness_errors else "FAILED")
    for err in network_readiness_errors:
        print("  ", err)
    print("- PUID privacy:", "OK" if not puid_privacy_errors else "FAILED")
    for err in puid_privacy_errors:
        print("  ", err)
    print("- Login adaptation:", "OK" if not login_adaptation_errors else "FAILED")
    for err in login_adaptation_errors:
        print("  ", err)
    print("- Login layout guard:", "OK" if not login_layout_guard_errors else "FAILED")
    for err in login_layout_guard_errors:
        print("  ", err)
    print("- Language selector display:", "OK" if not language_selector_display_errors else "FAILED")
    for err in language_selector_display_errors:
        print("  ", err)
    print("- Files module guard:", "OK" if not files_module_guard_errors else "FAILED")
    for err in files_module_guard_errors:
        print("  ", err)
    print("- Files smoothness guard:", "OK" if not files_smoothness_guard_errors else "FAILED")
    for err in files_smoothness_guard_errors:
        print("  ", err)
    print("- Files toolbar i18n guard:", "OK" if not files_toolbar_i18n_errors else "FAILED")
    for err in files_toolbar_i18n_errors:
        print("  ", err)
    print("- Dialog centering:", "OK" if not dialog_centering_errors else "FAILED")
    for err in dialog_centering_errors:
        print("  ", err)
    print("- Profile dialog guard:", "OK" if not profile_dialog_guard_errors else "FAILED")
    for err in profile_dialog_guard_errors:
        print("  ", err)
    print("- Registration/sticker guard:", "OK" if not registration_sticker_guard_errors else "FAILED")
    for err in registration_sticker_guard_errors:
        print("  ", err)
    print("- Personal/Enterprise isolation:", "OK" if not personal_enterprise_isolation_errors else "FAILED")
    for err in personal_enterprise_isolation_errors:
        print("  ", err)
    print("- Package cleanliness:", "OK" if not package_cleanliness_errors else "FAILED")
    for err in package_cleanliness_errors:
        print("  ", err)
    print("- Package structure:", "OK" if not package_structure_errors else "FAILED")
    for err in package_structure_errors:
        print("  ", err)
    print("- Project root:", ROOT)
    failed = any([py_errors, json_errors, missing_data, version_errors, language_errors, icon_errors, build_script_errors, tk_variable_errors, chat_brand_errors, release_policy_errors, security_preflight_errors, layout_adaptation_errors, year_view_fit_errors, performance_guard_errors, indonesian_title_case_errors, ui_title_case_errors, system_server_config_errors, network_readiness_errors, network_deployment_plan_errors, server_deployment_readiness_errors, sync_queue_repair_guard_errors, data_integrity_guard_errors, preflight_checkpoint_throttle_errors, offline_maintenance_errors, puid_privacy_errors, login_adaptation_errors, login_layout_guard_errors, language_selector_display_errors, files_module_guard_errors, files_smoothness_guard_errors, files_toolbar_i18n_errors, dialog_centering_errors, profile_dialog_guard_errors, registration_sticker_guard_errors, personal_enterprise_isolation_errors, package_cleanliness_errors, package_structure_errors])
    return 1 if failed else 0


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