#!/usr/bin/env python3
"""Parse PM2 jlist JSON even when the CLI prefixes version warnings."""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import re
import stat
import sys


def extract_processes(raw: str) -> list[dict]:
    decoder = json.JSONDecoder()
    for offset, character in enumerate(raw):
        if character != "[":
            continue
        try:
            value, _end = decoder.raw_decode(raw[offset:])
        except json.JSONDecodeError:
            continue
        if isinstance(value, list) and all(isinstance(item, dict) for item in value):
            return value
    raise ValueError("PM2 jlist output contained no process JSON array")


def process_name(process: dict) -> str | None:
    environment = process.get("pm2_env") or {}
    return environment.get("name") or process.get("name")


def _diagnose_script(processes: list[dict], *, name: str, script_path: str) -> None:
    """Emit bounded PM2 failure metadata without printing application logs."""

    expected_script = os.path.realpath(script_path)
    matches = []
    for process in processes:
        environment = process.get("pm2_env") or {}
        if (
            process_name(process) == name
            and os.path.realpath(str(environment.get("pm_exec_path") or ""))
            == expected_script
        ):
            matches.append(process)
    if len(matches) != 1:
        raise SystemExit(
            f"expected one PM2 app {name!r} at the exact script; found {len(matches)}"
        )

    process = matches[0]
    environment = process.get("pm2_env") or {}
    log_path = str(environment.get("pm_err_log_path") or "")
    log_present = False
    log_size = 0
    log_sha256 = hashlib.sha256(b"").hexdigest()
    exception_counts: dict[str, int] = {}
    frames: list[dict[str, object]] = []
    last_exception_class = ""
    last_exception_line_sha256 = hashlib.sha256(b"").hexdigest()
    missing_modules: set[str] = set()
    if log_path:
        try:
            metadata = os.lstat(log_path)
        except FileNotFoundError:
            metadata = None
        if metadata is not None:
            if not stat.S_ISREG(metadata.st_mode):
                raise SystemExit("PM2 error log must be one regular non-symlink file")
            if metadata.st_uid != os.getuid():
                raise SystemExit("PM2 error log is not owned by the deployment account")
            log_present = True
            log_size = int(metadata.st_size)
            with open(log_path, "rb") as error_log:
                if log_size > 262_144:
                    error_log.seek(log_size - 262_144)
                raw = error_log.read(262_144)
            log_sha256 = hashlib.sha256(raw).hexdigest()
            text = raw.decode("utf-8", errors="replace")
            exception_pattern = re.compile(
                r"(?:^|\s)([A-Za-z_][A-Za-z0-9_.]*(?:Error|Exception|Exit|Interrupt))(?::|$)"
            )
            missing_module_pattern = re.compile(
                r"ModuleNotFoundError:\s+No module named ['\"]([A-Za-z_][A-Za-z0-9_.]*)['\"]"
            )
            frame_pattern = re.compile(
                r'^\s*File\s+"([^"]+)",\s+line\s+([0-9]+),\s+in\s+([A-Za-z_][A-Za-z0-9_]*)'
            )
            for line in text.splitlines():
                exception_match = exception_pattern.search(line)
                if exception_match:
                    exception_class = exception_match.group(1).split(".")[-1]
                    exception_counts[exception_class] = (
                        exception_counts.get(exception_class, 0) + 1
                    )
                    last_exception_class = exception_class
                    last_exception_line_sha256 = hashlib.sha256(
                        line.encode("utf-8", errors="replace")
                    ).hexdigest()
                missing_module_match = missing_module_pattern.search(line)
                if missing_module_match:
                    missing_modules.add(missing_module_match.group(1))
                frame_match = frame_pattern.match(line)
                if frame_match:
                    frames.append(
                        {
                            "file": os.path.basename(frame_match.group(1)),
                            "line": int(frame_match.group(2)),
                            "function": frame_match.group(3),
                        }
                    )
                    frames = frames[-20:]

    with open(expected_script, "rb") as script_file:
        script_sha256 = hashlib.sha256(script_file.read()).hexdigest()

    payload = {
        "diagnostic": "phase1_engine_consumer_pm2",
        "name": name,
        "status": str(environment.get("status") or ""),
        "pid": int(process.get("pid") or 0),
        "restart_count": int(environment.get("restart_time") or 0),
        "unstable_restart_count": int(environment.get("unstable_restarts") or 0),
        "exit_code": int(environment.get("exit_code") or 0),
        "script_sha256": script_sha256,
        "error_log_present": log_present,
        "error_log_size": log_size,
        "error_log_tail_sha256": log_sha256,
        "exception_class_counts": dict(sorted(exception_counts.items())),
        "last_exception_class": last_exception_class,
        "last_exception_line_sha256": last_exception_line_sha256,
        "missing_modules": sorted(missing_modules),
        "traceback_frames": frames,
        "raw_log_emitted": False,
        "credentials_emitted": False,
        "mutation_executed": False,
        "process_changed": False,
    }
    print(json.dumps(payload, sort_keys=True, separators=(",", ":")))


def process_environment_value(process: dict, name: str) -> str | None:
    environment = process.get("pm2_env") or {}
    value = environment.get(name)
    if value is None and isinstance(environment.get("env"), dict):
        value = environment["env"].get(name)
    return str(value) if value is not None else None


def process_restart_count(process: dict) -> int:
    environment = process.get("pm2_env") or {}
    return int(environment.get("restart_time") or 0)


def process_state(process: dict) -> str:
    return ":".join(
        str(value)
        for value in (
            int(process.get("pm_id") or 0),
            int(process.get("pid") or 0),
            process_restart_count(process),
        )
    )


def read_null_delimited(path: str) -> list[str]:
    with open(path, "rb") as stream:
        return [
            value.decode("utf-8", errors="strict")
            for value in stream.read().split(b"\0")
            if value
        ]


def attest_process_filesystem(
    process: dict,
    *,
    proc_root: str,
    script_path: str,
    interpreter_path: str,
    cwd: str,
    virtual_env: str,
    path_prefix: str,
) -> None:
    pid = int(process.get("pid") or 0)
    process_dir = os.path.join(proc_root, str(pid))
    if not os.path.isdir(process_dir):
        raise SystemExit("tt_response process filesystem entry is absent")

    live_cwd = os.path.realpath(os.path.join(process_dir, "cwd"))
    if live_cwd != os.path.realpath(cwd):
        raise SystemExit("tt_response live working directory is incorrect")

    command = read_null_delimited(os.path.join(process_dir, "cmdline"))
    if not command or os.path.normpath(command[0]) != os.path.normpath(
        interpreter_path
    ):
        raise SystemExit("tt_response live interpreter is not the virtualenv Python")
    if not any(
        os.path.realpath(argument) == os.path.realpath(script_path)
        for argument in command[1:]
        if argument.startswith("/")
    ):
        raise SystemExit("tt_response live command does not contain the exact script")

    environment_entries = read_null_delimited(os.path.join(process_dir, "environ"))
    live_environment = dict(
        entry.split("=", 1) for entry in environment_entries if "=" in entry
    )
    if live_environment.get("VIRTUAL_ENV") != virtual_env:
        raise SystemExit("tt_response live VIRTUAL_ENV is incorrect")
    live_path = live_environment.get("PATH", "").split(os.pathsep)
    if not live_path or live_path[0] != path_prefix:
        raise SystemExit("tt_response live PATH does not start with the virtualenv")


def fingerprint_processes(processes: list[dict], *, exclude_name: str) -> str:
    stable = []
    for process in processes:
        name = process_name(process)
        if name == exclude_name:
            continue
        environment = process.get("pm2_env") or {}
        stable.append(
            {
                "exec_interpreter": environment.get("exec_interpreter"),
                "name": name,
                "pid": int(process.get("pid") or 0),
                "pm_cwd": environment.get("pm_cwd"),
                "pm_exec_path": environment.get("pm_exec_path"),
                "pm_id": int(process.get("pm_id") or 0),
                "restart_time": process_restart_count(process),
                "status": environment.get("status"),
            }
        )
    canonical = json.dumps(
        sorted(stable, key=lambda item: (str(item["name"]), item["pm_id"])),
        sort_keys=True,
        separators=(",", ":"),
    ).encode("utf-8")
    return hashlib.sha256(canonical).hexdigest()


def main() -> int:
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers(dest="command", required=True)
    find_main = subparsers.add_parser("find-main")
    find_main.add_argument("--app-dir", required=True)
    find_main.add_argument("--exclude-name", action="append", required=True)
    count = subparsers.add_parser("count")
    count.add_argument("--name", required=True)
    attest_script = subparsers.add_parser("attest-script")
    attest_script.add_argument("--name", required=True)
    attest_script.add_argument("--script-path", required=True)
    attest_script.add_argument("--interpreter")
    attest_script.add_argument("--cwd")
    attest_script.add_argument("--interpreter-path")
    attest_script.add_argument("--virtual-env")
    attest_script.add_argument("--path-prefix")
    attest_script.add_argument("--proc-root")
    attest_script.add_argument("--expected-pm-id", type=int)
    attest_script.add_argument("--expected-pid", type=int)
    attest_script.add_argument("--expected-restart-count", type=int)
    attest_script.add_argument("--print-state", action="store_true")
    attest_script_config = subparsers.add_parser("attest-script-config")
    attest_script_config.add_argument("--name", required=True)
    attest_script_config.add_argument("--script-path", required=True)
    diagnose_script = subparsers.add_parser("diagnose-script")
    diagnose_script.add_argument("--name", required=True)
    diagnose_script.add_argument("--script-path", required=True)
    fingerprint = subparsers.add_parser("fingerprint-excluding")
    fingerprint.add_argument("--name", required=True)
    options = parser.parse_args()

    processes = extract_processes(sys.stdin.read())
    if options.command == "count":
        print(sum(1 for process in processes if process_name(process) == options.name))
        return 0

    if options.command == "attest-script-config":
        expected_script = os.path.realpath(options.script_path)
        matches = [
            process
            for process in processes
            if process_name(process) == options.name
            and os.path.realpath(
                str((process.get("pm2_env") or {}).get("pm_exec_path") or "")
            )
            == expected_script
        ]
        if len(matches) != 1:
            raise SystemExit(
                f"expected one configured PM2 app {options.name!r} at the exact script; "
                f"found {len(matches)}"
            )
        print(options.name)
        return 0

    if options.command == "attest-script":
        expected_script = os.path.realpath(options.script_path)
        named_processes = [
            process for process in processes if process_name(process) == options.name
        ]
        if len(named_processes) != 1:
            raise SystemExit(
                f"expected exactly one PM2 app named {options.name!r}; "
                f"found {len(named_processes)}"
            )
        process = named_processes[0]
        environment = process.get("pm2_env") or {}
        if os.path.realpath(str(environment.get("pm_exec_path") or "")) != (
            expected_script
        ):
            raise SystemExit(f"PM2 app {options.name!r} does not use the exact script")
        if environment.get("status") != "online" or int(process.get("pid") or 0) <= 0:
            raise SystemExit(f"PM2 app {options.name!r} is not online with a live PID")
        if options.expected_pm_id is not None and int(
            process.get("pm_id") or 0
        ) != options.expected_pm_id:
            raise SystemExit(f"PM2 app {options.name!r} changed PM2 identity")
        if options.expected_pid is not None and int(
            process.get("pid") or 0
        ) != options.expected_pid:
            raise SystemExit(f"PM2 app {options.name!r} changed PID during stability check")
        if (
            options.expected_restart_count is not None
            and process_restart_count(process) != options.expected_restart_count
        ):
            raise SystemExit(
                f"PM2 app {options.name!r} restarted during the stability check"
            )

        if options.interpreter and os.path.realpath(
            str(environment.get("exec_interpreter") or "")
        ) != os.path.realpath(options.interpreter):
            raise SystemExit("tt_response PM2 interpreter is not the expected Python")
        if options.interpreter and options.cwd and os.path.realpath(
            str(environment.get("pm_cwd") or "")
        ) != os.path.realpath(options.cwd):
            raise SystemExit("tt_response PM2 working directory is incorrect")

        strong_runtime_requested = any(
            (
                options.interpreter_path,
                options.virtual_env,
                options.path_prefix,
                options.proc_root,
            )
        )
        runtime_arguments = (
            options.interpreter_path,
            options.cwd,
            options.virtual_env,
            options.path_prefix,
        )
        if strong_runtime_requested and not all(runtime_arguments):
            raise SystemExit(
                "runtime attestation requires interpreter, cwd, VIRTUAL_ENV, and PATH prefix"
            )
        if strong_runtime_requested:
            if os.path.normpath(str(environment.get("exec_interpreter") or "")) != (
                os.path.normpath(options.interpreter_path)
            ):
                raise SystemExit("tt_response PM2 interpreter is not the virtualenv Python")
            if os.path.realpath(str(environment.get("pm_cwd") or "")) != os.path.realpath(
                options.cwd
            ):
                raise SystemExit("tt_response PM2 working directory is incorrect")
            if process_environment_value(process, "VIRTUAL_ENV") != options.virtual_env:
                raise SystemExit("tt_response PM2 VIRTUAL_ENV is incorrect")
            path_value = process_environment_value(process, "PATH") or ""
            if not path_value.split(os.pathsep) or path_value.split(os.pathsep)[0] != (
                options.path_prefix
            ):
                raise SystemExit("tt_response PM2 PATH does not start with the virtualenv")
            if options.proc_root:
                attest_process_filesystem(
                    process,
                    proc_root=options.proc_root,
                    script_path=options.script_path,
                    interpreter_path=options.interpreter_path,
                    cwd=options.cwd,
                    virtual_env=options.virtual_env,
                    path_prefix=options.path_prefix,
                )
        print(process_state(process) if options.print_state else options.name)
        return 0

    if options.command == "fingerprint-excluding":
        print(fingerprint_processes(processes, exclude_name=options.name))
        return 0

    if options.command == "diagnose-script":
        _diagnose_script(
            processes,
            name=options.name,
            script_path=options.script_path,
        )
        return 0

    app_dir = os.path.realpath(options.app_dir)
    names = []
    for process in processes:
        environment = process.get("pm2_env") or {}
        name = process_name(process)
        cwd = environment.get("pm_cwd")
        if name not in options.exclude_name and cwd and os.path.realpath(cwd) == app_dir:
            names.append(name)
    if len(names) != 1:
        raise SystemExit(f"expected one Timetabler PM2 app at checkout; found {len(names)}")
    print(names[0])
    return 0


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