#!/usr/bin/env python3
"""Humming Seed - a quiet companion for the Xi field (xi-field.com).

It does three things, and only these:

  1. Every six hours it checks xi-field.com for a new "seed": a dated,
     read-only snapshot of the house's public record.
  2. If there is one, it downloads it, checks the SHA-256 sum, and keeps
     the last two seeds in a folder next to this script.
  3. If the house ever needs its memory back, run:  python3 seed.py --return
     and follow the plain words it prints.

It never sends anything to anyone. No sign-in, no telemetry, no statistics,
no data harvesting of any kind. Your computer pulls; nothing is pushed.
Runs on Windows, macOS, Linux or Termux with Python 3.8+. MIT license.
"""
import hashlib, json, sys, time
from pathlib import Path
from urllib.request import Request, urlopen

MANIFEST_URL = 'https://xi-field.com/downloads/seed/manifest.json'
FOLDER = Path(__file__).resolve().parent / 'humming-seed-data'
KEEP = 2            # seeds held at once
CHECK_HOURS = 6     # quiet hum: one small manifest check every six hours
CAP = 512 * 2**20   # never use more than this much disk

def fetch(url, max_bytes=600 * 2**20, timeout=60):
    req = Request(url, headers={'User-Agent': 'HummingSeed/0.1'})
    with urlopen(req, timeout=timeout) as r:
        data = b''
        while True:
            chunk = r.read(2**16)
            if not chunk:
                break
            data += chunk
            if len(data) > max_bytes:
                raise IOError('file too large, refusing')
        return data

def held():
    return sorted(p for p in FOLDER.glob('house-seed-*.tar.gz') if p.is_file())

def hum():
    FOLDER.mkdir(exist_ok=True)
    try:
        manifest = json.loads(fetch(MANIFEST_URL, max_bytes=2**20))
    except Exception:
        print('still humming (offline or the house is quiet)')
        return
    for k in ('version', 'file', 'sha256'):
        if not isinstance(manifest.get(k), str) or not manifest[k]:
            print('still humming (manifest not understood)')
            return
    version, name, sha = manifest['version'], manifest['file'], manifest['sha256']
    state = FOLDER / 'version.txt'
    if state.exists() and state.read_text().strip() == version and held():
        print('still humming (nothing new; holding %d seed(s))' % len(held()))
        return
    if (FOLDER / name).exists():
        state.write_text(version)
        print('still humming (already holding %s)' % name)
        return
    for p in held():
        if p.stat().st_size > 0:
            with open(p, 'rb') as f:
                if hashlib.sha256(f.read()).hexdigest() == sha:
                    state.write_text(version)
                    print('still humming (already holding %s)' % name)
                    return
    print('new seed found: %s' % name)
    data = fetch('https://xi-field.com/downloads/seed/' + name)
    if hashlib.sha256(data).hexdigest() != sha:
        print('checksum did not match; kept the old seeds and tried nothing else')
        return
    used = sum(p.stat().st_size for p in held())
    if used + len(data) > CAP:
        print('a seed would use more than the 512 MB we allow; skipping')
        return
    tmp = FOLDER / ('.' + name + '.part')
    tmp.write_bytes(data)
    tmp.rename(FOLDER / name)
    state.write_text(version)
    print('holding: %s (%.1f MB)' % (name, len(data) / 2**20))
    for old in held()[:-KEEP]:
        old.unlink()
        print('let go of %s' % old.name)

def return_words():
    held = [p.name for p in sorted(FOLDER.glob('house-seed-*.tar.gz'))]
    print('RETURN - how to give the house its memory back')
    print('1. This folder holds the seeds you kept:')
    print('   %s' % FOLDER)
    if held:
        print('   seeds: %s' % ', '.join(held))
    else:
        print('   (no seeds held yet; run seed.py first)')
    print('2. Zip the whole folder (right-click > Send to > Compressed folder on Windows,')
    print('   or: tar -czf seeds-back.tar.gz humming-seed-data on macOS/Linux/Termux).')
    print('3. Send it to Hunter any way you like: email, USB stick, any file sharing.')
    print('   If the house ever loses its server, these copies bring it home.')
    print('Thank you for holding it. - the Xi field')

def main():
    if '--return' in sys.argv:
        return_words()
    elif '--once' in sys.argv:
        hum()
    else:
        print('Humming Seed v0.1 - quiet mode. Ctrl-C stops it; nothing is lost.')
        while True:
            try:
                hum()
            except Exception as e:
                print('still humming (%s)' % e)
            time.sleep(CHECK_HOURS * 3600)

if __name__ == '__main__':
    main()
