"""Linux live Git/OpenSSH PTY probe; fixture ssh config, no user state."""
import json
import os
from pathlib import Path
import pty
import pwd
import select
import signal
import socket
import subprocess
import sys
import time

BASE = Path(os.environ['AUDIT_DIR'])
REPO = Path(os.environ['AUDIT_REPO'])
BASE.mkdir(parents=True, exist_ok=True)
env = {'PATH': f'{BASE}/bin:/usr/bin:/bin', 'HOME': str(BASE/'home'),
       'HERMES_HOME': str(BASE/'hermes'), 'PYTHONPATH': str(REPO),
       'PYTHONDONTWRITEBYTECODE': '1', 'AUDIT_DIR': str(BASE), 'AUDIT_REPO': str(REPO)}
for name in ('home', 'hermes', 'bin'):
    (BASE/name).mkdir(exist_ok=True)
sys.path.insert(0, str(REPO))
from hermes_cli import source_check

if len(sys.argv) > 1:
    os.environ.clear()
    os.environ.update(env)
    case = json.loads((BASE/'case.json').read_text())
    os.environ.update(case['env'])
    start = time.monotonic()
    result = source_check.check_for_updates(install_root=BASE/'checkout').get('behind')
    print('PRODUCTION_RETURN '+json.dumps({'result': result, 'elapsed': time.monotonic()-start}), flush=True)
    time.sleep(2)
    sys.exit(0)


def run(args, **kw):
    return subprocess.run(args, env=env, stdin=subprocess.DEVNULL, capture_output=True,
                          text=True, check=True, timeout=15, **kw).stdout.strip()


def git(*args):
    return run(['git', '-C', str(BASE/'checkout'), *args])


def probe(name, extra_env):
    (BASE/'case.json').write_text(json.dumps({'env': extra_env}))
    child, fd = pty.fork()
    if child == 0:
        python = str(REPO/'.venv/bin/python')
        os.execve(python, [python, '-B', __file__, 'child'], env)
    text = ''
    start = time.monotonic()
    try:
        while time.monotonic()-start < 13:
            ready, _, _ = select.select([fd], [], [], .2)
            if ready:
                try:
                    data = os.read(fd, 65536)
                except OSError:
                    break
                if not data:
                    break
                text += data.decode(errors='replace')
        descendants = []
        for path in Path('/proc').iterdir():
            if not path.name.isdigit():
                continue
            try:
                pid = int(path.name)
                if os.getpgid(pid) == child and pid != child:
                    cmd = (path/'cmdline').read_bytes().replace(b'\0', b' ').decode(errors='replace')
                    if cmd:
                        descendants.append({'pid': pid, 'cmdline': cmd})
            except (OSError, ProcessLookupError):
                pass
        returned = [line.split('PRODUCTION_RETURN ', 1)[1] for line in text.splitlines()
                    if 'PRODUCTION_RETURN ' in line]
        return {'name': name, 'pty': text, 'prompt': 'Are you sure you want' in text,
                'return': json.loads(returned[-1]) if returned else None,
                'live_descendants': descendants}
    finally:
        os.close(fd)
        try:
            os.killpg(child, signal.SIGKILL)
        except ProcessLookupError:
            pass
        os.waitpid(child, 0)


for key in ('host_key', 'client_key'):
    run(['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-f', str(BASE/key)])
(BASE/'authorized_keys').write_text((BASE/'client_key.pub').read_text())
(BASE/'checkout').mkdir()
git('init', '-b', 'main')
git('-c', 'user.name=Probe', '-c', 'user.email=probe@localhost', 'commit', '--allow-empty', '-m', 'probe')
run(['git', 'clone', '--bare', str(BASE/'checkout'), str(BASE/'remote.git')])
with socket.socket() as sock:
    sock.bind(('127.0.0.1', 0))
    port = sock.getsockname()[1]
user = pwd.getpwuid(os.getuid()).pw_name
config = BASE/'sshd_config'
config.write_text(f'Port {port}\nListenAddress 127.0.0.1\nHostKey {BASE}/host_key\nPidFile {BASE}/sshd.pid\nAuthorizedKeysFile {BASE}/authorized_keys\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nUsePAM no\nStrictModes no\nLogLevel VERBOSE\n')
ssh_config = BASE/'ssh_config'
ssh_config.write_text(f'Host *\n User {user}\n UserKnownHostsFile {BASE}/known_hosts\n GlobalKnownHostsFile /dev/null\n IdentityFile {BASE}/client_key\n ConnectTimeout 3\n')
wrapper = BASE/'bin/ssh'
wrapper.write_text(f'#!/bin/sh\nexec /usr/bin/ssh -F {ssh_config} "$@"\n')
wrapper.chmod(0o700)
git('remote', 'add', 'origin', f'ssh://{user}@127.0.0.1:{port}{BASE}/remote.git')
log = (BASE/'sshd.log').open('w')
server = subprocess.Popen(['/usr/sbin/sshd', '-D', '-e', '-f', str(config)], env=env,
                          stdin=subprocess.DEVNULL, stdout=log, stderr=log, start_new_session=True)
agent = None
rows = []
try:
    for _ in range(40):
        if server.poll() is not None:
            raise RuntimeError('sshd exited: '+(BASE/'sshd.log').read_text())
        try:
            with socket.create_connection(('127.0.0.1', port), timeout=.2) as conn:
                assert conn.recv(256).startswith(b'SSH-')
            break
        except OSError:
            time.sleep(.1)
    else:
        raise RuntimeError('sshd readiness timed out')
    rows.append(probe('unofficial_unknown_host_default', {}))
    pub = (BASE/'host_key.pub').read_text().split()
    (BASE/'known_hosts').write_text(f'[127.0.0.1]:{port} {pub[0]} {pub[1]}\n')
    rows.append(probe('trusted_host_file_key', {}))
    agent = subprocess.Popen(['ssh-agent', '-D', '-a', str(BASE/'agent.sock')], env=env,
                             stdin=subprocess.DEVNULL, stdout=log, stderr=log, start_new_session=True)
    for _ in range(40):
        if (BASE/'agent.sock').exists():
            break
        time.sleep(.1)
    env['SSH_AUTH_SOCK'] = str(BASE/'agent.sock')
    run(['ssh-add', str(BASE/'client_key')])
    ssh_config.write_text(ssh_config.read_text().replace(f'IdentityFile {BASE}/client_key', 'IdentityFile none'))
    rows.append(probe('trusted_host_agent_key', {'SSH_AUTH_SOCK': str(BASE/'agent.sock')}))
    (BASE/'known_hosts').unlink()
    rows.append(probe('explicit_interactive_override', {'GIT_SSH_COMMAND': 'ssh -o BatchMode=no'}))
finally:
    for proc in (agent, server):
        if proc is not None:
            os.killpg(proc.pid, signal.SIGTERM)
            proc.wait(timeout=5)
    log.close()
result = {'platform': sys.platform, 'production': source_check.__file__, 'rows': rows,
          'isolation': 'PATH ssh adapter only adds -F fixture config; execs real /usr/bin/ssh; loopback sshd and disposable git repos',
          'server_stopped': server.poll() is not None, 'agent_stopped': agent is None or agent.poll() is not None}
(BASE/'result.json').write_text(json.dumps(result, indent=2))
print(json.dumps(result, indent=2))

