#!/usr/bin/env python3
"""Merge host keys learned by automation into the owner's shared, Nextcloud-synced
~/.ssh/known_hosts, in ONE atomic write, only when something is new.

Why: crystalcave and bastion run agents that ssh to many hosts. Writing the shared
file directly (ssh appends in place) made two machines rewrite it constantly and once
uploaded a truncated copy to Nextcloud. Instead /etc/ssh/ssh_config.d/50-agent-known-hosts.conf
on these hosts makes ssh WRITE new keys to ~/.ssh-agent-known_hosts (local, not synced)
while still READING the shared file. This job copies new keys into the shared file so
they reach every device. Owner wants one shared file (2026-10-30).
"""
import os, sys, tempfile

HOME = os.path.expanduser("~")
SHARED = os.path.join(HOME, ".ssh", "known_hosts")
AGENT = os.path.join(HOME, ".ssh-agent-known_hosts")


def lines(path):
    try:
        s = open(path, errors="replace").read()
    except FileNotFoundError:
        return []
    out = s.split("\n")
    if s and not s.endswith("\n"):
        out = out[:-1]  # never trust a truncated last line
    return [l for l in out if l.strip()]


def key_of(line):
    """(hostfield, keytype) for a plain or hashed entry; None for comments/markers."""
    p = line.split()
    if not p or p[0].startswith("#"):
        return None
    if p[0].startswith("@"):
        return (p[0] + " " + p[1], p[2]) if len(p) > 3 else None
    return (p[0], p[1]) if len(p) > 2 else None


def main():
    shared = lines(SHARED)
    agent = lines(AGENT)
    if not agent:
        return 0
    if len(shared) < 50:
        print("shared known_hosts looks truncated/missing; refusing to write", file=sys.stderr)
        return 1
    have = set(shared)
    agent_by_key = {}
    for l in agent:
        k = key_of(l)
        if k:
            agent_by_key[k] = l
    new = [l for l in agent if l not in have and key_of(l)]
    if not new:
        return 0
    # A host whose key the agent re-learned (rebuilt host): replace the old shared entry
    # for the same host+type instead of keeping both.
    replaced = 0
    merged = []
    for l in shared:
        k = key_of(l)
        if k in agent_by_key and agent_by_key[k] != l:
            replaced += 1
            continue
        merged.append(l)
    merged_set = set(merged)
    merged += [l for l in new if l not in merged_set]
    d = os.path.dirname(os.path.realpath(SHARED))
    fd, tmp = tempfile.mkstemp(prefix=".known_hosts.", suffix="~", dir=d)
    with os.fdopen(fd, "w") as f:
        f.write("\n".join(merged) + "\n")
    os.chmod(tmp, 0o600)
    os.replace(tmp, os.path.realpath(SHARED))
    print(f"merged {len(new)} new key line(s), replaced {replaced} changed key(s)")
    return 0


if __name__ == "__main__":
    sys.exit(main())
